Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 11 Sep 2013 21:48:20 -0400
changeset 146698 a98569f21abe10fc486631e4ad40e20999ea5d25
parent 146655 2f11fad2f3070b0d1ea93d8585dbe1261d713d2c (current diff)
parent 146697 bdf495121827fb5a7de36b3cf9896c1da7652151 (diff)
child 146699 a4e9c9c9dbf94494b508ca9108f30f1941a70d4a
child 146737 306bb31682d243064e515eac81f28c9db1f13e98
child 146770 7579426b08f7d8bb3ff1af8341d7ad6baa0f7ad3
child 146776 32f916dd9329222fb1b34137d5cd08edfc072166
child 155738 2493bb98e584383d51e5d1ac8efde1186fcb667b
child 161425 3bd00b0909c795ebcf9974d37ba321c8ac3e3603
push id25268
push userryanvm@gmail.com
push dateThu, 12 Sep 2013 01:54:59 +0000
treeherdermozilla-central@a98569f21abe [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c.
gfx/layers/composite/APZCTreeManager.cpp
js/xpconnect/idl/xpccomponents.idl
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/XPCComponents.cpp
js/xpconnect/src/xpcprivate.h
mobile/android/base/tests/testAwesomebarSwipes.java.in
modules/libpref/src/init/all.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1062,17 +1062,18 @@ pref("devtools.errorconsole.enabled", fa
 
 // Developer toolbar and GCLI preferences
 pref("devtools.toolbar.enabled", true);
 pref("devtools.toolbar.visible", false);
 pref("devtools.gcli.allowSet", false);
 pref("devtools.commands.dir", "");
 
 // Disable the app manager
-pref("devtools.appmanager.enabled", false);
+pref("devtools.appmanager.enabled", true);
+pref("devtools.appmanager.simulatorInstallPage", "https://addons.mozilla.org/firefox/addon/firefox-os-simulator/");
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["paintflashing toggle","tilt toggle","scratchpad","resize toggle"]');
 pref("devtools.toolbox.sideEnabled", true);
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -41,17 +41,17 @@
                 label="&shareLinkCmd.label;"
                 accesskey="&shareLinkCmd.accesskey;"
                 oncommand="gContextMenu.shareLink();"/>
       <menuitem id="context-savelink"
                 label="&saveLinkCmd.label;"
                 accesskey="&saveLinkCmd.accesskey;"
                 oncommand="gContextMenu.saveLink();"/>
       <menu id="context-marklinkMenu" label="&social.marklinkMenu.label;"
-            accesskey="&social.marklink.accesskey;">
+            accesskey="&social.marklinkMenu.accesskey;">
         <menupopup/>
       </menu>
       <menuitem id="context-copyemail"
                 label="&copyEmailCmd.label;"
                 accesskey="&copyEmailCmd.accesskey;"
                 oncommand="gContextMenu.copyEmail();"/>
       <menuitem id="context-copylink"
                 label="&copyLinkCmd.label;"
@@ -247,17 +247,17 @@
                 label="&sharePageCmd.label;"
                 accesskey="&sharePageCmd.accesskey;"
                 oncommand="SocialShare.sharePage();"/>
       <menuitem id="context-savepage"
                 label="&savePageCmd.label;"
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
       <menu id="context-markpageMenu" label="&social.markpageMenu.label;"
-            accesskey="&social.markpage.accesskey;">
+            accesskey="&social.markpageMenu.accesskey;">
         <menupopup/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
                 oncommand="gContextMenu.viewBGImage(event);"
                 onclick="checkForMiddleClick(this, event);"/>
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -1653,22 +1653,22 @@ SocialMarks = {
     // remove all previous entries by class
     let menus = [m for (m of document.getElementsByClassName("context-socialmarks"))];
     [m.parentNode.removeChild(m) for (m of menus)];
 
     let contextMenus = [
       {
         type: "link",
         id: "context-marklinkMenu",
-        label: "social.marklink.label"
+        label: "social.marklinkMenu.label"
       },
       {
         type: "page",
         id: "context-markpageMenu",
-        label: "social.markpage.label"
+        label: "social.markpageMenu.label"
       }
     ];
     for (let cfg of contextMenus) {
       this._populateContextPopup(cfg, providers);
     }
   },
 
   MENU_LIMIT: 3, // adjustable for testing
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -247,22 +247,22 @@ function checkSocialUI(win) {
   if (provider) {
     for (let id of ["menu_socialSidebar", "menu_socialAmbientMenu"])
       _is(document.getElementById(id).getAttribute("label"), Social.provider.name, "element has the provider name");
 
     let contextMenus = [
       {
         type: "link",
         id: "context-marklinkMenu",
-        label: "social.marklink.label"
+        label: "social.marklinkMenu.label"
       },
       {
         type: "page",
         id: "context-markpageMenu",
-        label: "social.markpage.label"
+        label: "social.markpageMenu.label"
       }
     ];
 
     for (let c of contextMenus) {
       let leMenu = document.getElementById(c.id);
       let parent, menus;
       let markProviders = SocialMarks.getProviders();
       if (markProviders.length > SocialMarks.MENU_LIMIT) {
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -85,16 +85,18 @@ static RedirEntry kRedirMap[] = {
   { "preferences", "chrome://browser/content/preferences/in-content/preferences.xul",
     nsIAboutModule::ALLOW_SCRIPT },
   { "downloads", "chrome://browser/content/downloads/contentAreaDownloadsView.xul",
     nsIAboutModule::ALLOW_SCRIPT },
 #ifdef MOZ_SERVICES_HEALTHREPORT
   { "healthreport", "chrome://browser/content/abouthealthreport/abouthealth.xhtml",
     nsIAboutModule::ALLOW_SCRIPT },
 #endif
+  { "app-manager", "chrome://browser/content/devtools/app-manager/index.xul",
+    nsIAboutModule::ALLOW_SCRIPT },
 };
 static const int kRedirTotal = NS_ARRAY_LENGTH(kRedirMap);
 
 static nsAutoCString
 GetAboutModuleName(nsIURI *aURI)
 {
   nsAutoCString path;
   aURI->GetPath(path);
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -104,16 +104,17 @@ static const mozilla::Module::ContractID
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "home", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "newtab", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "permissions", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "preferences", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "downloads", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_HEALTHREPORT
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "healthreport", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "app-manager", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #if defined(XP_WIN)
     { NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
 #elif defined(XP_MACOSX)
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { nullptr }
 };
 
--- a/browser/components/sessionstore/src/SessionStore.jsm
+++ b/browser/components/sessionstore/src/SessionStore.jsm
@@ -673,22 +673,22 @@ let SessionStoreInternal = {
       case "TabShow":
         this.onTabShow(win, aEvent.originalTarget);
         break;
       case "TabHide":
         this.onTabHide(win, aEvent.originalTarget);
         break;
       case "TabPinned":
         // If possible, update cached data without having to invalidate it
-        TabStateCache.update(aEvent.originalTarget, "pinned", true);
+        TabStateCache.updateField(aEvent.originalTarget, "pinned", true);
         this.saveStateDelayed(win);
         break;
       case "TabUnpinned":
         // If possible, update cached data without having to invalidate it
-        TabStateCache.update(aEvent.originalTarget, "pinned", false);
+        TabStateCache.updateField(aEvent.originalTarget, "pinned", false);
         this.saveStateDelayed(win);
         break;
     }
     this._clearRestoringWindows();
   },
 
   /**
    * If it's the first window load since app start...
@@ -1325,32 +1325,32 @@ let SessionStoreInternal = {
       TabRestoreQueue.hiddenToVisible(aTab);
 
       // let's kick off tab restoration again to ensure this tab gets restored
       // with "restore_hidden_tabs" == false (now that it has become visible)
       this.restoreNextTab();
     }
 
     // If possible, update cached data without having to invalidate it
-    TabStateCache.update(aTab, "hidden", false);
+    TabStateCache.updateField(aTab, "hidden", false);
 
     // Default delay of 2 seconds gives enough time to catch multiple TabShow
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   onTabHide: function ssi_onTabHide(aWindow, aTab) {
     // If the tab hasn't been restored yet, move it into the right bucket
     if (aTab.linkedBrowser.__SS_restoreState &&
         aTab.linkedBrowser.__SS_restoreState == TAB_STATE_NEEDS_RESTORE) {
       TabRestoreQueue.visibleToHidden(aTab);
     }
 
     // If possible, update cached data without having to invalidate it
-    TabStateCache.update(aTab, "hidden", true);
+    TabStateCache.updateField(aTab, "hidden", true);
 
     // Default delay of 2 seconds gives enough time to catch multiple TabHide
     // events due to changing groups in Panorama.
     this.saveStateDelayed(aWindow);
   },
 
   /* ........ nsISessionStore API .............. */
 
@@ -1660,50 +1660,60 @@ let SessionStoreInternal = {
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       // If the tab hasn't been fully restored, get the data from the to-be-restored data
       data = aTab.linkedBrowser.__SS_data.extData;
     }
     return data[aKey] || "";
   },
 
   setTabValue: function ssi_setTabValue(aTab, aKey, aStringValue) {
-    TabStateCache.delete(aTab);
     // If the tab hasn't been restored, then set the data there, otherwise we
     // could lose newly added data.
     let saveTo;
     if (aTab.__SS_extdata) {
       saveTo = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       saveTo = aTab.linkedBrowser.__SS_data.extData;
     }
     else {
       aTab.__SS_extdata = {};
       saveTo = aTab.__SS_extdata;
     }
+
     saveTo[aKey] = aStringValue;
+    TabStateCache.updateField(aTab, "extData", saveTo);
     this.saveStateDelayed(aTab.ownerDocument.defaultView);
   },
 
   deleteTabValue: function ssi_deleteTabValue(aTab, aKey) {
-    TabStateCache.delete(aTab);
     // We want to make sure that if data is accessed early, we attempt to delete
     // that data from __SS_data as well. Otherwise we'll throw in cases where
     // data can be set or read.
     let deleteFrom;
     if (aTab.__SS_extdata) {
       deleteFrom = aTab.__SS_extdata;
     }
     else if (aTab.linkedBrowser.__SS_data && aTab.linkedBrowser.__SS_data.extData) {
       deleteFrom = aTab.linkedBrowser.__SS_data.extData;
     }
 
-    if (deleteFrom && deleteFrom[aKey])
+    if (deleteFrom && aKey in deleteFrom) {
       delete deleteFrom[aKey];
-    this.saveStateDelayed(aTab.ownerDocument.defaultView);
+
+      // Keep the extData object only if it is not empty, to save
+      // a little disk space when serializing the tab state later.
+      if (Object.keys(deleteFrom).length) {
+        TabStateCache.updateField(aTab, "extData", deleteFrom);
+      } else {
+        TabStateCache.removeField(aTab, "extData");
+      }
+
+      this.saveStateDelayed(aTab.ownerDocument.defaultView);
+    }
   },
 
   persistTabAttribute: function ssi_persistTabAttribute(aName) {
     if (TabAttributes.persist(aName)) {
       TabStateCache.clear();
       this.saveStateDelayed();
     }
   },
@@ -4634,25 +4644,41 @@ let TabStateCache = {
   /**
    * Update in place a piece of data.
    *
    * @param {XULElement} aKey The tab or the associated browser.
    * If the tab/browser is not present, do nothing.
    * @param {string} aField The field to update.
    * @param {*} aValue The new value to place in the field.
    */
-  update: function(aKey, aField, aValue) {
+  updateField: function(aKey, aField, aValue) {
     let key = this._normalizeToBrowser(aKey);
     let data = this._data.get(key);
     if (data) {
       data[aField] = aValue;
     }
     TabStateCacheTelemetry.recordAccess(!!data);
   },
 
+  /**
+   * Remove a given field from a cached tab state.
+   *
+   * @param {XULElement} aKey The tab or the associated browser.
+   * If the tab/browser is not present, do nothing.
+   * @param {string} aField The field to remove.
+   */
+  removeField: function(aKey, aField) {
+    let key = this._normalizeToBrowser(aKey);
+    let data = this._data.get(key);
+    if (data && aField in data) {
+      delete data[aField];
+    }
+    TabStateCacheTelemetry.recordAccess(!!data);
+  },
+
   _normalizeToBrowser: function(aKey) {
     let nodeName = aKey.localName;
     if (nodeName == "tab") {
       return aKey.linkedBrowser;
     }
     if (nodeName == "browser") {
       return aKey;
     }
--- a/browser/devtools/app-manager/app-projects.js
+++ b/browser/devtools/app-manager/app-projects.js
@@ -1,13 +1,14 @@
 const {Cc,Ci,Cu} = require("chrome");
 const ObservableObject = require("devtools/shared/observable-object");
 const promise = require("sdk/core/promise");
 
 const {EventEmitter} = Cu.import("resource:///modules/devtools/shared/event-emitter.js");
+const {generateUUID} = Cc['@mozilla.org/uuid-generator;1'].getService(Ci.nsIUUIDGenerator);
 
 /**
  * IndexedDB wrapper that just save project objects
  *
  * The only constraint is that project objects have to have
  * a unique `location` object.
  */
 
@@ -91,17 +92,23 @@ IDB.open().then(function (projects) {
   store.object.projects = projects;
   AppProjects.emit("ready", store.object.projects);
 });
 
 const AppProjects = {
   addPackaged: function(folder) {
     let project = {
       type: "packaged",
-      location: folder.path
+      location: folder.path,
+      // We need a unique id, that is the app origin,
+      // in order to identify the app when being installed on the device.
+      // The packaged app local path is a valid id, but only on the client.
+      // This origin will be used to generate the true id of an app:
+      // its manifest URL.
+      packagedAppOrigin: generateUUID().toString().slice(1, -1)
     };
     return IDB.add(project).then(function () {
       store.object.projects.push(project);
       // return the added objects (proxified)
       return store.object.projects[store.object.projects.length - 1];
     });
   },
 
--- a/browser/devtools/app-manager/content/connection-footer.js
+++ b/browser/devtools/app-manager/content/connection-footer.js
@@ -2,22 +2,24 @@
  * 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/. */
 
 const Cu = Components.utils;
 const Ci = Components.interfaces;
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 
+const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm")
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
 
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const ConnectionStore = require("devtools/app-manager/connection-store");
 const DeviceStore = require("devtools/app-manager/device-store");
+const simulatorsStore = require("devtools/app-manager/simulators-store");
 
 let UI = {
   init: function() {
     this.useFloatingScrollbarsIfNeeded();
     let connections = ConnectionManager.connections;
     if (connections.length > 0) {
       let hash = window.location.hash;
       if (hash) {
@@ -38,16 +40,17 @@ let UI = {
     }
 
     window.location.hash = "cid=" + this.connection.uid;
     window.parent.postMessage(JSON.stringify({name:"connection",cid:this.connection.uid}), "*");
 
     this.store = Utils.mergeStores({
       "device": new DeviceStore(this.connection),
       "connection": new ConnectionStore(this.connection),
+      "simulators": simulatorsStore,
     });
 
     let pre = document.querySelector("#logs > pre");
     pre.textContent = this.connection.logs;
     pre.scrollTop = pre.scrollTopMax;
     this.connection.on(Connection.Events.NEW_LOG, (event, str) => {
       pre.textContent += "\n" + str;
       pre.scrollTop = pre.scrollTopMax;
@@ -91,9 +94,52 @@ let UI = {
     document.querySelector("#connect-button").focus();
     let host = document.querySelector("input.host").value;
     let port = document.querySelector("input.port").value;
     this.connection.port = port;
     this.connection.host = host;
     Services.prefs.setCharPref("devtools.debugger.remote-host", host);
     Services.prefs.setIntPref("devtools.debugger.remote-port", port);
   },
+
+  showSimulatorList: function() {
+    document.body.classList.add("show-simulators");
+  },
+
+  cancelShowSimulatorList: function() {
+    document.body.classList.remove("show-simulators");
+  },
+
+  installSimulator: function() {
+    let url = Services.prefs.getCharPref("devtools.appmanager.simulatorInstallPage");
+    window.open(url);
+  },
+
+  startSimulator: function(version) {
+    let port = ConnectionManager.getFreeTCPPort();
+    let simulator = Simulator.getByVersion(version);
+    if (!simulator) {
+      this.connection.log("Error: can't find simulator: " + version);
+      return;
+    }
+    if (!simulator.launch) {
+      this.connection.log("Error: invalid simulator: " + version);
+      return;
+    }
+    this.connection.log("Found simulator: " + version);
+    this.connection.log("Starting simulator...");
+
+    this.simulator = simulator;
+    this.simulator.launch({ port: port })
+      .then(() => {
+        this.connection.log("Simulator ready. Connecting.");
+        this.connection.port = port;
+        this.connection.host = "localhost";
+        this.connection.once("connected", function() {
+          this.connection.log("Connected to simulator.");
+          this.connection.keepConnecting = false;
+        });
+        this.connection.keepConnecting = true;
+        this.connection.connect();
+      });
+    document.body.classList.remove("show-simulators");
+  },
 }
--- a/browser/devtools/app-manager/content/connection-footer.xhtml
+++ b/browser/devtools/app-manager/content/connection-footer.xhtml
@@ -33,19 +33,19 @@
         <!-- Disconnected -->
         <div id="banner-disconnected" class="banner">
           <div class="connected-indicator"></div>
           <div class="banner-box">
             <div class="banner-content">
               <span>&connection.notConnected;</span>
               <button class="action-primary left" onclick="UI.connect()" id="connect-button" template='{"type":"localizedContent","property":"connection.connectTo","paths":["connection.host","connection.port"]}'></button>
               <button class="right" onclick="UI.editConnectionParameters()">&connection.changeHostAndPort;</button>
-              <div id="start-simulator-box" template='{"type":"attribute","path":"simulators.versions.length","name":"simulators-count"}'>
+              <div id="start-simulator-box">
                 <span>&connection.or;</span>
-                <button id="start-simulator-button" class="action-primary" onclick="UI.startSimulator()">&connection.startSimulator;</button>
+                <button id="start-simulator-button" class="action-primary" onclick="UI.showSimulatorList()">&connection.startSimulator;</button>
               </div>
             </div>
           </div>
         </div>
 
         <!-- Connecting -->
         <div id="banner-connecting" class="banner">
           <div class="connected-indicator"></div>
@@ -76,23 +76,50 @@
                 <input class="host" template='{"type":"attribute","path":"connection.host","name":"value"}'></input>
                 <input class="port" pattern="\d+" template='{"type":"attribute","path":"connection.port","name":"value"}' type="number"></input>
                 <button type="submit">&connection.saveConnectionInfo;</button>
               </form>
             </div>
           </div>
         </div>
 
+        <!-- Simulator -->
+        <div id="banner-simulators" class="banner" template='{"type":"attribute","path":"simulators.versions.length","name":"simulator-count"}'>
+          <div class="connected-indicator"></div>
+          <div class="banner-box">
+            <div class="banner-content">
+              <div class="no-simulator">
+                <span>&connection.noSimulatorInstalled;</span>
+                <button class="action-primary" onclick="UI.installSimulator()">&connection.installFirstSimulator;</button>
+              </div>
+              <div class="found-simulator">
+                <span template-loop='{"arrayPath":"simulators.versions","childSelector":"#simulator-item-template"}'></span>
+                <button class="action-primary" onclick="UI.installSimulator()">&connection.installAnotherSimulator;</button>
+              </div>
+              <button class="action-cancel" onclick="UI.cancelShowSimulatorList()">&connection.cancel;</button>
+            </div>
+          </div>
+        </div>
+
+
         <!-- Logs -->
         <div id="banner-logs">
         <div id="logs" class="banner-box">
           <pre></pre>
         </div>
         </div>
 
       </div>
     </div>
   </body>
 
+  <template id="simulator-item-template">
+  <span>
+    <button class="simulator-item" onclick="UI.startSimulator(this.dataset.version)" template='{"type":"attribute","path":"version","name":"data-version"}'>
+      <span template='{"type":"textContent", "path":"version"}'></span>
+    </button>
+  </span>
+  </template>
+
   <script type="application/javascript;version=1.8" src="utils.js"></script>
   <script type="application/javascript;version=1.8" src="template.js"></script>
   <script type="application/javascript;version=1.8" src="connection-footer.js"></script>
 </html>
--- a/browser/devtools/app-manager/content/device.js
+++ b/browser/devtools/app-manager/content/device.js
@@ -162,17 +162,23 @@ let UI = {
         });
       }
     });
     return deferred.promise;
   },
 
   openToolbox: function(manifest) {
     this._getTargetForApp(manifest).then((target) => {
-      gDevTools.showToolbox(target, "webconsole", devtools.Toolbox.HostType.WINDOW);
+      gDevTools.showToolbox(target,
+                            null,
+                            devtools.Toolbox.HostType.WINDOW).then(toolbox => {
+        this.connection.once(Connection.Events.DISCONNECTED, () => {
+          toolbox.destroy();
+        });
+      });
     }, console.error);
   },
 
   startApp: function(manifest) {
     let deferred = promise.defer();
 
     if (!this.listTabsResponse) {
       deferred.reject();
--- a/browser/devtools/app-manager/content/projects.js
+++ b/browser/devtools/app-manager/content/projects.js
@@ -9,16 +9,18 @@ const Cr = Components.results;
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {require} = devtools;
 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
 const {AppProjects} = require("devtools/app-manager/app-projects");
 const {AppValidator} = require("devtools/app-manager/app-validator");
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
+const {installHosted, installPackaged} = require("devtools/app-actor-front");
+
 const promise = require("sdk/core/promise");
 
 window.addEventListener("message", function(event) {
   try {
     let json = JSON.parse(event.data);
     if (json.name == "connection") {
       let cid = parseInt(json.cid);
       for (let c of ConnectionManager.connections) {
@@ -158,16 +160,49 @@ let UI = {
   _getProjectManifestURL: function (project) {
     if (project.type == "packaged") {
       return "app://" + project.packagedAppOrigin + "/manifest.webapp";
     } else if (project.type == "hosted") {
       return project.location;
     }
   },
 
+  install: function(button, location) {
+    button.dataset.originalTextContent = button.textContent;
+    button.textContent = Utils.l10n("project.installing");
+    button.disabled = true;
+    let project = AppProjects.get(location);
+    let install;
+    if (project.type == "packaged") {
+      install = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin);
+    } else {
+      let manifestURLObject = Services.io.newURI(project.location, null, null);
+      let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
+      let appId = origin.host;
+      let metadata = {
+        origin: origin.spec,
+        manifestURL: project.location
+      };
+      install = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest);
+    }
+    install.then(function () {
+      button.disabled = false;
+      button.textContent = Utils.l10n("project.installed");
+      setTimeout(function() {
+        button.textContent = button.dataset.originalTextContent;
+      }, 1500);
+    },
+    function (res) {
+      button.disabled = false;
+      let message = res.error + ": " + res.message;
+      alert(message);
+      this.connection.log(message);
+    });
+  },
+
   start: function(location) {
     let project = AppProjects.get(location);
     let request = {
       to: this.listTabsResponse.webappsActor,
       type: "launch",
       manifestURL: this._getProjectManifestURL(project)
     };
     this.connection.client.request(request, (res) => {
@@ -218,18 +253,21 @@ let UI = {
   },
 
   openToolbox: function(location) {
     let project = AppProjects.get(location);
     let manifest = this._getProjectManifestURL(project);
     this._getTargetForApp(manifest).then((target) => {
       gDevTools.showToolbox(target,
                             null,
-                            devtools.Toolbox.HostType.WINDOW,
-                            this.connection.uid);
+                            devtools.Toolbox.HostType.WINDOW).then(toolbox => {
+        this.connection.once(Connection.Events.DISCONNECTED, () => {
+          toolbox.destroy();
+        });
+      });
     }, console.error);
   },
 
 
   reveal: function(location) {
     let project = AppProjects.get(location);
     if (project.type == "packaged") {
       let projectFolder = FileUtils.File(project.location);
--- a/browser/devtools/app-manager/content/projects.xhtml
+++ b/browser/devtools/app-manager/content/projects.xhtml
@@ -20,17 +20,17 @@
     <aside id="sidebar">
       <div id="project-list" template='{"type":"attribute","path":"projects.length","name":"projects-count"}'>
         <div template-loop='{"arrayPath":"projects","childSelector":"#project-item-template"}'></div>
         <div id="no-project">&projects.noProject;</div>
       </div>
       <div id="new-packaged-project" onclick="UI.addPackaged()">&projects.addPackaged;</div>
       <div id="new-hosted-project">&projects.addHosted;
         <form onsubmit="UI.addHosted(); return false;" id="new-hosted-project-wrapper">
-          <input value="" id="url-input" type="url" pattern="https?://.+" placeholder="&projects.hostedManifestPlaceHolder;" size="50" />
+          <input value="" id="url-input" type="url" pattern="https?://.+" placeholder="&projects.hostedManifestPlaceHolder2;" size="50" />
           <div onclick="UI.addHosted()" id="new-hosted-project-click"></div>
           <input type="submit" hidden="true"></input>
         </form>
       </div>
     </aside>
     <section id="lense"></section>
   </body>
 
@@ -66,24 +66,20 @@
           </div>
           <span template='{"type":"textContent","path":"manifest.developer.name"}'></span>
           <p class="project-location" template='{"type":"textContent","path":"location"}' onclick="UI.reveal(this.textContent)"></p>
           <p class="project-description" template='{"type":"textContent","path":"manifest.description"}'></p>
         </div>
       </div>
       <div class="project-buttons">
         <button class="project-button-refresh" onclick="UI.update(this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}'>&projects.reloadFiles;</button>
-        <!-- Not available until bug 911785 is fixed
         <button class="device-action project-button-install" onclick="UI.install(this, this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}'>&projects.installApp;</button>
-        -->
         <button class="device-action project-button-start" onclick="UI.start(this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}'>&projects.startApp;</button>
         <button class="device-action project-button-stop" onclick="UI.stop(this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}'>&projects.stopApp;</button>
-        <!-- Not available until bug 911785 is fixed
         <button class="device-action project-button-debug" onclick="UI.openToolbox(this.dataset.location)" template='{"type":"attribute","path":"location","name":"data-location"}'>&projects.debugApp;</button>
-        -->
       </div>
       <div class="project-errors" template='{"type":"textContent","path":"errors"}'></div>
       <div class="project-warnings" template='{"type":"textContent","path":"warnings"}'></div>
     </div>
   </div>
   </template>
 
 
new file mode 100644
--- /dev/null
+++ b/browser/devtools/app-manager/simulators-store.js
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu} = require("chrome");
+const ObservableObject = require("devtools/shared/observable-object");
+const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
+
+let store = new ObservableObject({versions:[]});
+
+function feedStore() {
+  store.object.versions = Simulator.availableVersions().map(v => {
+    return {version:v}
+  });
+}
+
+Simulator.on("register", feedStore);
+Simulator.on("unregister", feedStore);
+feedStore();
+
+module.exports = store;
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -373,17 +373,17 @@ let gDevToolsBrowser = {
   openConnectScreen: function(gBrowser) {
     gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/connect.xhtml");
   },
 
   /**
    * Open the App Manager
    */
   openAppManager: function(gBrowser) {
-    gBrowser.selectedTab = gBrowser.addTab("chrome://browser/content/devtools/app-manager/index.xul");
+    gBrowser.selectedTab = gBrowser.addTab("about:app-manager");
   },
 
   /**
    * Add this DevTools's presence to a browser window's document
    *
    * @param {XULDocument} doc
    *        The document to which menuitems and handlers are to be added
    */
--- a/browser/devtools/inspector/test/browser_inspector_select_last_selected.html
+++ b/browser/devtools/inspector/test/browser_inspector_select_last_selected.html
@@ -1,14 +1,21 @@
 <!DOCTYPE html>
-
-<div id="id1"></div>
-<div id="id2"></div>
-<div id="id3">
-	<ul class="aList">
-		<li class="item"></li>
-		<li class="item"></li>
-		<li class="item"></li>
-		<li class="item">
-			<span id="id4"></span>
-		</li>
-	</ul>
-</div>
\ No newline at end of file
+<html lang="en">
+	<head>
+		<meta charset="UTF-8">
+		<title>select last selected test</title>
+	</head>
+	<body>
+		<div id="id1"></div>
+		<div id="id2"></div>
+		<div id="id3">
+			<ul class="aList">
+				<li class="item"></li>
+				<li class="item"></li>
+				<li class="item"></li>
+				<li class="item">
+					<span id="id4"></span>
+				</li>
+			</ul>
+		</div>
+	</body>
+</html>
\ No newline at end of file
--- a/browser/devtools/inspector/test/browser_inspector_select_last_selected.js
+++ b/browser/devtools/inspector/test/browser_inspector_select_last_selected.js
@@ -25,79 +25,102 @@ function test() {
   }, true);
   content.location = page1;
 
   function startTests() {
     testSameNodeSelectedOnPageReload();
   }
 
   function endTests() {
-    toolbox.destroy();
-    toolbox = inspector = page1 = page2 = null;
-    gBrowser.removeCurrentTab();
-    finish();
+    executeSoon(() => {
+      toolbox.destroy();
+      toolbox = inspector = page1 = page2 = null;
+      gBrowser.removeCurrentTab();
+      finish();
+    });
   }
 
-  function testReSelectingAnElement(id, callback) {
+  function loadPageAnd(page, callback) {
+    inspector.once("markuploaded", () => {
+      executeSoon(callback);
+    });
+
+    if (page) {
+      content.location = page;
+    } else {
+      content.location.reload();
+    }
+  }
+
+  function reloadAndReselect(id, callback) {
     let div = content.document.getElementById(id);
-    inspector.selection.setNode(div);
+
     inspector.once("inspector-updated", () => {
       is(inspector.selection.node, div);
-      inspector.once("markuploaded", () => {
+
+      loadPageAnd(false, () => {
         is(inspector.selection.node.id, id, "Node re-selected after reload");
-        callback();
+        executeSoon(callback);
       });
-      content.location.reload();
     });
+
+    inspector.selection.setNode(div);
   }
 
   // Test that nodes selected on the test page remain selected after reload
   function testSameNodeSelectedOnPageReload()
   {
     // Select a few nodes and check they are re-selected after reload of the same
     // page
-    testReSelectingAnElement("id1", () => {
-      testReSelectingAnElement("id2", () => {
-        testReSelectingAnElement("id3", () => {
-          testReSelectingAnElement("id4", testBodySelectedOnNavigate);
+    reloadAndReselect("id1", () => {
+      reloadAndReselect("id2", () => {
+        reloadAndReselect("id3", () => {
+          reloadAndReselect("id4", testBodySelectedOnNavigate);
         });
       });
     });
   }
 
   // Test that since the previously selected node doesn't exist on the new page
   // the body is selected
   function testBodySelectedOnNavigate() {
     // Last node selected was id4, go to a different page and check body is
     // selected
-    inspector.once("markuploaded", () => {
-      is(
-        inspector.selection.node.tagName.toLowerCase(),
-        "body",
-        "Node not found, selecting body"
-      );
-      testSameNodeSelectedOnNavigateAwayAndBack();
+    loadPageAnd(page2, () => {
+      executeSoon(() => {
+        is(
+          inspector.selection.node.tagName.toLowerCase(),
+          "body",
+          "Node not found, body selected"
+        );
+        executeSoon(testSameNodeSelectedOnNavigateAwayAndBack);
+      });
     });
-    content.location = page2;
   }
 
   // Test that the node selected on page 1 gets selected again after a navigation
   // is made to another page and back again
   function testSameNodeSelectedOnNavigateAwayAndBack() {
     // On page2, select id5
     let id = "id5";
     let div = content.document.getElementById(id);
-    inspector.selection.setNode(div);
+
     inspector.once("inspector-updated", () => {
       is(inspector.selection.node.id, id);
-      // go to page1 but do not select anything
-      inspector.once("markuploaded", () => {
-        // go back to page2 and check id5 is still the current selection
-        inspector.once("markuploaded", () => {
-          is(inspector.selection.node.id, id, "Node re-selected after navigation");
-          endTests();
+
+      executeSoon(() => {
+        // go to page1 but do not select anything
+        loadPageAnd(page1, () => {
+
+          executeSoon(() => {
+            // go back to page2 and check id5 is still the current selection
+            loadPageAnd(page2, () => {
+              is(inspector.selection.node.id, id, "Node re-selected after navigation");
+              executeSoon(endTests);
+            });
+          });
         });
-        content.location = page2;
       });
-      content.location = page1;
     });
+
+    inspector.selection.setNode(div);
   }
 }
--- a/browser/devtools/inspector/test/browser_inspector_select_last_selected2.html
+++ b/browser/devtools/inspector/test/browser_inspector_select_last_selected2.html
@@ -1,3 +1,10 @@
 <!DOCTYPE html>
-
-<div id="id5"></div>
\ No newline at end of file
+<html lang="en">
+	<head>
+		<meta charset="UTF-8">
+		<title>select last selected test</title>
+	</head>
+	<body>
+		<div id="id5"></div>
+	</body>
+</html>
\ No newline at end of file
--- a/browser/devtools/responsivedesign/responsivedesign.jsm
+++ b/browser/devtools/responsivedesign/responsivedesign.jsm
@@ -229,19 +229,21 @@ ResponsiveUI.prototype = {
      if (this.touchEnableBefore) {
        this.enableTouch();
      }
    },
 
    onPageUnload: function() {
      if (this.closing)
        return;
-     this.touchEnableBefore = this.touchEventHandler.enabled;
-     this.disableTouch();
-     delete this.touchEventHandler;
+     if (this.touchEventHandler) {
+       this.touchEnableBefore = this.touchEventHandler.enabled;
+       this.disableTouch();
+       delete this.touchEventHandler;
+     }
    },
 
   /**
    * Destroy the nodes. Remove listeners. Reset the style.
    */
   close: function RUI_unload() {
     if (this.closing)
       return;
--- a/browser/devtools/styleinspector/rule-view.js
+++ b/browser/devtools/styleinspector/rule-view.js
@@ -1036,17 +1036,19 @@ CssRuleView.prototype = {
 
     this.element.removeEventListener("copy", this._boundCopy);
     delete this._boundCopy;
 
     if (this.element.parentNode) {
       this.element.parentNode.removeChild(this.element);
     }
 
-    this.elementStyle.destroy();
+    if (this.elementStyle) {
+      this.elementStyle.destroy();
+    }
 
     this.popup.destroy();
   },
 
   /**
    * Update the highlighted element.
    *
    * @param {nsIDOMElement} aElement
@@ -1205,16 +1207,21 @@ CssRuleView.prototype = {
   {
     if (this._showPseudoElements === undefined) {
       this._showPseudoElements =
         Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
     }
     return this._showPseudoElements;
   },
 
+  _getRuleViewHeaderClassName: function(isPseudo) {
+    let baseClassName = "theme-gutter ruleview-header";
+    return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName;
+  },
+
   /**
    * Creates editor UI for each of the rules in _elementStyle.
    */
   _createEditors: function CssRuleView_createEditors()
   {
     // Run through the current list of rules, attaching
     // their editors in order.  Create editors if needed.
     let lastInheritedSource = "";
@@ -1225,36 +1232,39 @@ CssRuleView.prototype = {
       if (rule.domRule.system) {
         continue;
       }
 
       // Only print header for this element if there are pseudo elements
       if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
         seenNormalElement = true;
         let div = this.doc.createElementNS(HTML_NS, "div");
-        div.className = "theme-gutter ruleview-header";
+        div.className = this._getRuleViewHeaderClassName();
         div.textContent = this.selectedElementLabel;
         this.element.appendChild(div);
       }
 
       let inheritedSource = rule.inheritedSource;
       if (inheritedSource != lastInheritedSource) {
         let div = this.doc.createElementNS(HTML_NS, "div");
-        div.className = "theme-gutter ruleview-header";
+        div.className = this._getRuleViewHeaderClassName();
         div.textContent = inheritedSource;
         lastInheritedSource = inheritedSource;
         this.element.appendChild(div);
       }
 
       if (!seenPseudoElement && rule.pseudoElement) {
         seenPseudoElement = true;
 
         let div = this.doc.createElementNS(HTML_NS, "div");
-        div.className = "theme-gutter ruleview-header";
+        div.className = this._getRuleViewHeaderClassName(true);
         div.textContent = this.pseudoElementLabel;
+        div.addEventListener("dblclick", () => {
+          this.togglePseudoElementVisibility(!this.showPseudoElements);
+        }, false);
 
         let twisty = this.pseudoElementTwisty =
           this.doc.createElementNS(HTML_NS, "span");
         twisty.className = "ruleview-expander theme-twisty";
         twisty.addEventListener("click", () => {
           this.togglePseudoElementVisibility(!this.showPseudoElements);
         }, false);
 
--- a/browser/devtools/styleinspector/ruleview.css
+++ b/browser/devtools/styleinspector/ruleview.css
@@ -45,12 +45,16 @@
   display: block;
 }
 
 .ruleview .ruleview-expander {
   vertical-align: middle;
 }
 
 .ruleview-header {
-  vertical-align:middle;
+  vertical-align: middle;
   height: 1.5em;
   line-height: 1.5em;
-}
\ No newline at end of file
+}
+
+.ruleview-header.ruleview-expandable-header {
+  cursor: pointer;
+}
--- a/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
+++ b/browser/devtools/styleinspector/test/browser_ruleview_pseudoelement.js
@@ -43,17 +43,20 @@ function testTopLeft()
 
     // Make sure that clicking on the twisty hides pseudo elements
     let expander = gutters[0].querySelector(".ruleview-expander");
     ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded");
     expander.click();
     ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by twisty");
     expander.click();
     ok (view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are expanded again");
-    expander.click();
+
+    // Make sure that dblclicking on the header container also toggles the pseudo elements
+    EventUtils.synthesizeMouseAtCenter(gutters[0], {clickCount: 2}, inspector.sidebar.getWindowForTab("ruleview"));
+    ok (!view.element.classList.contains("show-pseudo-elements"), "Pseudo Elements are collapsed by dblclicking");
 
     let defaultView = element.ownerDocument.defaultView;
     let elementRule = elementRules[0];
     let elementRuleView = [].filter.call(view.element.children, (e) => {
       return e._ruleEditor && e._ruleEditor.rule === elementRule;
     })[0]._ruleEditor;
 
     let elementAfterRule = afterRules[0];
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -640,19 +640,19 @@ just addresses the organization to follo
 <!ENTITY social.learnMore.label "Learn more…">
 <!ENTITY social.learnMore.accesskey "l">
 <!ENTITY social.closeNotificationItem.label "Not Now">
 
 <!ENTITY social.chatBar.commandkey "c">
 <!ENTITY social.chatBar.label "Focus chats">
 <!ENTITY social.chatBar.accesskey "c">
 
-<!ENTITY social.markpage.accesskey "P">
+<!ENTITY social.markpageMenu.accesskey "P">
 <!ENTITY social.markpageMenu.label "Save Page To…">
-<!ENTITY social.marklink.accesskey "L">
+<!ENTITY social.marklinkMenu.accesskey "L">
 <!ENTITY social.marklinkMenu.label "Save Link To…">
 
 <!ENTITY getUserMedia.selectCamera.label "Camera to share:">
 <!ENTITY getUserMedia.selectCamera.accesskey "C">
 <!ENTITY getUserMedia.selectMicrophone.label "Microphone to share:">
 <!ENTITY getUserMedia.selectMicrophone.accesskey "M">
 
 <!ENTITY webrtcIndicatorButton.label "Camera / Microphone Access">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -425,20 +425,20 @@ service.install.learnmore=Learn More…
 
 # LOCALIZATION NOTE (social.turnOff.label): %S is the name of the social provider
 social.turnOff.label=Turn off %S
 social.turnOff.accesskey=T
 # LOCALIZATION NOTE (social.turnOn.label): %S is the name of the social provider
 social.turnOn.label=Turn on %S
 social.turnOn.accesskey=T
 
-# LOCALIZATION NOTE (social.markpage.label): %S is the name of the social provider
-social.markpage.label=Save Page to %S
-# LOCALIZATION NOTE (social.marklink.label): %S is the name of the social provider
-social.marklink.label=Save Link to %S
+# LOCALIZATION NOTE (social.markpageMenu.label): %S is the name of the social provider
+social.markpageMenu.label=Save Page to %S
+# LOCALIZATION NOTE (social.marklinkMenu.label): %S is the name of the social provider
+social.marklinkMenu.label=Save Link to %S
 
 # LOCALIZATION NOTE (social.error.message): %1$S is brandShortName (e.g. Firefox), %2$S is the name of the social provider
 social.error.message=%1$S is unable to connect with %2$S right now.
 social.error.tryAgain.label=Try Again
 social.error.tryAgain.accesskey=T
 social.error.closeSidebar.label=Close This Sidebar
 social.error.closeSidebar.accesskey=C
 
--- a/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.dtd
@@ -27,23 +27,26 @@
 <!ENTITY connection.notConnected "Not Connected">
 <!ENTITY connection.changeHostAndPort "Change">
 <!ENTITY connection.startSimulator "Start Simulator">
 <!ENTITY connection.saveConnectionInfo "Save">
 <!ENTITY connection.connecting "Connecting…">
 <!ENTITY connection.disconnecting "Disconnecting…">
 <!ENTITY connection.cancel "Cancel">
 <!ENTITY connection.or "or">
+<!ENTITY connection.noSimulatorInstalled "No simulator installed.">
+<!ENTITY connection.installFirstSimulator "Install simulator.">
+<!ENTITY connection.installAnotherSimulator "Add">
 
 <!ENTITY projects.localApps "Local Apps">
 <!ENTITY projects.addApp "Add">
 <!ENTITY projects.addPackaged "Add Packaged App">
 <!ENTITY projects.addHosted "Add Hosted App">
 <!ENTITY projects.title "Local Apps">
 <!ENTITY projects.appDetails "App Details">
 <!ENTITY projects.removeApp "Remove">
 <!ENTITY projects.reloadFiles "Refresh">
 <!ENTITY projects.installApp "Install">
 <!ENTITY projects.startApp "Start">
 <!ENTITY projects.stopApp "Stop">
 <!ENTITY projects.debugApp "Debug">
-<!ENTITY projects.hostedManifestPlaceHolder "http://example.com/app/webapp.manifest">
+<!ENTITY projects.hostedManifestPlaceHolder2 "http://example.com/app/manifest.webapp">
 <!ENTITY projects.noProject "No project linked. Add a new packaged app below (a directory) or a hosted app (link to a manifest file).">
--- a/browser/locales/en-US/chrome/browser/devtools/app-manager.properties
+++ b/browser/locales/en-US/chrome/browser/devtools/app-manager.properties
@@ -6,16 +6,18 @@
 # the device's height, %3$S is the device's pixel density.
 # Example: 800x480 (86 DPI).
 device.deviceSize=Device size: %1$Sx%2$S (%3$S DPI)
 # LOCALIZATION NOTE (connection.connectedToDevice, connection.connectTo):
 # %1$S is the host name, %2$S is the port number.
 connection.connectedToDevice=Connected to %1$S
 connection.connectTo=Connect to %1$S:%2$S
 project.filePickerTitle=Select a webapp folder
+project.installing=Installing...
+project.installed=Installed!
 validator.nonExistingFolder=The project folder doesn't exists
 validator.expectProjectFolder=The project folder ends up being a file
 validator.wrongManifestFileName=Packaged apps require a manifest file that can only be named 'manifest.webapp' at project root folder
 validator.invalidManifestURL=Invalid manifest URL '%S'
 # LOCALIZATION NOTE (validator.invalidManifestJSON, validator.noAccessManifestURL):
 # %1$S is the error message, %2$S is the URI of the manifest.
 validator.invalidManifestJSON=The webapp manifest isn't a valid JSON file: %1$S at: %2$S
 validator.noAccessManifestURL=Unable to read manifest file: %1$S at: %2$S
--- a/browser/metro/base/content/ContextUI.js
+++ b/browser/metro/base/content/ContextUI.js
@@ -15,16 +15,18 @@ var ContextUI = {
   _hidingId: 0,
 
   /*******************************************
    * init
    */
 
   init: function init() {
     Elements.browsers.addEventListener('URLChanged', this, true);
+    Elements.browsers.addEventListener("AlertActive", this, true);
+    Elements.browsers.addEventListener("AlertClose", this, true);
     Elements.tabList.addEventListener('TabSelect', this, true);
     Elements.panelUI.addEventListener('ToolPanelShown', this, false);
     Elements.panelUI.addEventListener('ToolPanelHidden', this, false);
 
     window.addEventListener("touchstart", this, true);
     window.addEventListener("mousedown", this, true);
     window.addEventListener("MozEdgeUIStarted", this, true);
     window.addEventListener("MozEdgeUICanceled", this, true);
@@ -320,16 +322,20 @@ var ContextUI = {
         break;
       case "touchstart":
         this.onDownInput(aEvent);
         break;
       case "ToolPanelShown":
       case "ToolPanelHidden":
         this.dismiss();
         break;
+      case "AlertActive":
+      case "AlertClose":
+        ContentAreaObserver.updateContentArea();
+        break;
       case "touchstart":
         if (!BrowserUI.isStartTabVisible) {
           this.dismiss();
         }
         break;
       case "MozFlyoutPanelShowing":
         if (BrowserUI.isStartTabVisible) {
           this.dismissTabs();
--- a/browser/metro/base/content/bindings/notification.xml
+++ b/browser/metro/base/content/bindings/notification.xml
@@ -48,16 +48,17 @@
           <![CDATA[
             if (aItem == this.currentNotification)
               this.removeCurrentNotification(aSkipAnimation);
             else if (aItem != this._closedNotification)
               this._removeNotificationElement(aItem);
 
             // Fire notification closed event.
             let event = new Event('AlertClose');
+            event.notification = aItem;
             this.dispatchEvent(event);
 
             return aItem;
           ]]>
         </body>
       </method>
       <method name="handleEvent">
         <parameter name="aEvent"/>
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -1138,19 +1138,22 @@ Browser.MainDragger.prototype = {
 
     if (scaleY) {
       this._verticalScrollbar.height = ContentAreaObserver.height * scaleY;
       this._verticalScrollbar.setAttribute("panning", "true");
     }
   },
 
   _hideScrollbars: function _hideScrollbars() {
-    this._scrollScales.x = 0, this._scrollScales.y = 0;
+    this._scrollScales.x = 0;
+    this._scrollScales.y = 0;
     this._horizontalScrollbar.removeAttribute("panning");
     this._verticalScrollbar.removeAttribute("panning");
+    this._horizontalScrollbar.removeAttribute("width");
+    this._verticalScrollbar.removeAttribute("height");
     this._horizontalScrollbar.style.MozTransform = "";
     this._verticalScrollbar.style.MozTransform = "";
   }
 };
 
 
 
 const OPEN_APPTAB = 100; // Hack until we get a real API
--- a/browser/metro/base/content/downloads.js
+++ b/browser/metro/base/content/downloads.js
@@ -52,16 +52,17 @@ var Downloads = {
 
     Services.obs.addObserver(this, "dl-start", true);
     Services.obs.addObserver(this, "dl-done", true);
     Services.obs.addObserver(this, "dl-run", true);
     Services.obs.addObserver(this, "dl-failed", true);
     Services.obs.addObserver(this, "dl-request", true);
 
     this._notificationBox = Browser.getNotificationBox();
+    this._notificationBox.addEventListener('AlertClose', this.handleEvent, true);
 
     this._progress = new DownloadProgressListener(this);
     this.manager.addListener(this._progress);
 
     this._downloadProgressIndicator = document.getElementById("download-progress");
 
     if (this.manager.activeDownloadCount) {
       setTimeout (this._restartWithActiveDownloads.bind(this), 0);
@@ -220,16 +221,17 @@ var Downloads = {
           Downloads.manager.retryDownload(aDownload.id);
         }
       },
       {
         label: cancelButtonText,
         accessKey: "",
         callback: function() {
           Downloads.cancelDownload(aDownload);
+          Downloads._downloadProgressIndicator.reset();
         }
       }
     ];
     this.showNotification("download-failed", message, buttons,
       this._notificationBox.PRIORITY_WARNING_HIGH);
   },
 
   _showDownloadCompleteNotification: function (aDownload) {
@@ -392,16 +394,17 @@ var Downloads = {
 
       let buttons = [
         {
           isDefault: false,
           label: cancelButtonText,
           accessKey: "",
           callback: function() {
             Downloads.cancelDownloads();
+            Downloads._downloadProgressIndicator.reset();
           }
         }
       ];
 
       this._progressNotification =
         this.showNotification("download-progress", message, buttons,
         this._notificationBox.PRIORITY_WARNING_LOW);
     } else {
@@ -426,16 +429,27 @@ var Downloads = {
       this._progressNotificationInfo.set(aDownload.guid, {});
     }
     if (!this._progressAlert) {
       this._progressAlert = new AlertDownloadProgressListener();
       this.manager.addListener(this._progressAlert);
     }
   },
 
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "AlertClose":
+        if (aEvent.notification.value == "download-complete" &&
+            !Downloads._notificationBox.getNotificationWithValue("download-complete")) {
+          Downloads._downloadProgressIndicator.reset();
+        }
+        break;
+    }
+  },
+
   observe: function (aSubject, aTopic, aData) {
     let message = "";
     let msgTitle = "";
 
     switch (aTopic) {
       case "dl-run":
         let file = aSubject.QueryInterface(Ci.nsIFile);
         this._runDownloadBooleanMap.set(file.path, (aData == 'true'));
@@ -454,27 +468,25 @@ var Downloads = {
         }
 
         this._runDownloadBooleanMap.delete(download.targetFile.path);
         if (this._downloadsInProgress == 0) {
           if (this._downloadCount > 1 || !runAfterDownload) {
             this._showDownloadCompleteToast(download);
             this._showDownloadCompleteNotification(download);
           }
-          this._downloadProgressIndicator.reset();
           this._progressNotificationInfo.clear();
           this._downloadCount = 0;
           this._notificationBox.removeNotification(this._progressNotification);
           this._progressNotification = null;
         }
         break;
       case "dl-failed":
         download = aSubject.QueryInterface(Ci.nsIDownload);
         this._showDownloadFailedNotification(download);
-        this._downloadProgressIndicator.reset();
         break;
       case "dl-request":
         setTimeout(function() {
           ContextUI.displayNavbar();
         }, 1000);
         break;
     }
   },
--- a/browser/themes/linux/devtools/ruleview.css
+++ b/browser/themes/linux/devtools/ruleview.css
@@ -41,17 +41,18 @@
 .ruleview-rule + .ruleview-rule {
   border-top-width: 1px;
   border-top-style: dotted;
 }
 
 .ruleview-warning {
   background: url("chrome://browser/skin/devtools/alerticon-warning.png");
   -moz-margin-start: 5px;
-  vertical-align: middle;
+  display: inline-block;
+  vertical-align: top;
   width: 13px;
   height: 12px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;
 }
 
--- a/browser/themes/osx/devtools/ruleview.css
+++ b/browser/themes/osx/devtools/ruleview.css
@@ -45,17 +45,18 @@
 .ruleview-rule + .ruleview-rule {
   border-top-width: 1px;
   border-top-style: dotted;
 }
 
 .ruleview-warning {
   background: url("chrome://browser/skin/devtools/alerticon-warning.png");
   -moz-margin-start: 5px;
-  vertical-align: middle;
+  display: inline-block;
+  vertical-align: top;
   width: 13px;
   height: 12px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;
 }
 
--- a/browser/themes/shared/devtools/app-manager/connection-footer.css
+++ b/browser/themes/shared/devtools/app-manager/connection-footer.css
@@ -28,20 +28,22 @@
 
 #connection-footer[status="connected"]     #banner-connected,
 #connection-footer[status="connecting"]    #banner-connecting,
 #connection-footer[status="disconnected"]  #banner-disconnected,
 #connection-footer[status="disconnecting"] #banner-disconnecting {
   display: flex;
 }
 
+body.show-simulators .banner,
 body.edit-connection .banner {
   display: none !important;
 }
 
+body.show-simulators #banner-simulators,
 body.edit-connection #banner-editing {
   display: flex !important;
 }
 
 #banner-logs {
   width: 40%;
   display: flex;
 }
@@ -65,20 +67,16 @@ body.edit-connection #banner-editing {
 #banner-connected > .banner-box {
   align-items: flex-start;
 }
 
 #start-simulator-box {
   display: inline;
 }
 
-#start-simulator-box[simulators-count="0"] {
-  display: none;
-}
-
 /************** PIXELS **************/
 
 * {
   margin: 0;
   padding: 0;
   -moz-box-sizing: border-box;
   font-size: 0.9rem;
 }
@@ -188,13 +186,23 @@ button.action-cancel {
   flex: 0 0 10px;
 }
 
 #banner-connected .connected-indicator,
 #banner-connecting .connected-indicator {
   background: linear-gradient(to bottom, #69B8FF, #339FFF );
 }
 
+#banner-simulators .connected-indicator,
 #banner-disconnected .connected-indicator,
 #banner-editing .connected-indicator,
 #banner-disconnecting .connected-indicator {
   background: linear-gradient(to bottom, #375A87, #1C4375 );
 }
+
+#banner-simulators .banner-content > * {
+  display: inline-block;
+}
+
+#banner-simulators[simulator-count="0"] .found-simulator,
+#banner-simulators:not([simulator-count="0"]) .no-simulator {
+  display: none;
+}
--- a/browser/themes/windows/devtools/ruleview.css
+++ b/browser/themes/windows/devtools/ruleview.css
@@ -41,17 +41,18 @@
 .ruleview-rule + .ruleview-rule {
   border-top-width: 1px;
   border-top-style: dotted;
 }
 
 .ruleview-warning {
   background: url("chrome://browser/skin/devtools/alerticon-warning.png");
   -moz-margin-start: 5px;
-  vertical-align: middle;
+  display: inline-block;
+  vertical-align: top;
   width: 13px;
   height: 12px;
 }
 
 .ruleview-ruleopen {
   -moz-padding-end: 5px;
 }
 
--- a/caps/tests/mochitest/test_bug470804.html
+++ b/caps/tests/mochitest/test_bug470804.html
@@ -16,20 +16,18 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 470804
     Passing a null targetURL to checkLoadURIWithPrincipal shouldn't crash
  **/
 
-netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-const nsIScriptSecurityManager = Components.interfaces.nsIScriptSecurityManager;
-var secMan = SpecialPowers.Cc["@mozilla.org/scriptsecuritymanager;1"]
-                       .getService(nsIScriptSecurityManager);
+const nsIScriptSecurityManager = SpecialPowers.Ci.nsIScriptSecurityManager;
+var secMan = SpecialPowers.Services.scriptSecurityManager;
 var principal = SpecialPowers.wrap(document).nodePrincipal;
 isnot(principal, undefined, "Should have a principal");
 isnot(principal, null, "Should have a non-null principal");
 is(secMan.isSystemPrincipal(principal), false,
    "Shouldn't have system principal here");
 try {
   secMan.checkLoadURIWithPrincipal(principal, null,
                                    nsIScriptSecurityManager.STANDARD);
--- a/content/base/test/copypaste.js
+++ b/content/base/test/copypaste.js
@@ -25,31 +25,28 @@ function modifySelection(s) {
 function getLoadContext() {
   var Ci = SpecialPowers.Ci;
   return SpecialPowers.wrap(window).QueryInterface(Ci.nsIInterfaceRequestor)
                                    .getInterface(Ci.nsIWebNavigation)
                                    .QueryInterface(Ci.nsILoadContext);
 }
 
 function testCopyPaste (isXHTML) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
   var suppressUnicodeCheckIfHidden = !!isXHTML;
   var suppressHTMLCheck = !!isXHTML;
 
-  var webnav = window.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+  var webnav = SpecialPowers.wrap(window).QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
                      .getInterface(SpecialPowers.Ci.nsIWebNavigation)
 
   var docShell = webnav.QueryInterface(SpecialPowers.Ci.nsIDocShell);
 
   var documentViewer = docShell.contentViewer
                                .QueryInterface(SpecialPowers.Ci.nsIContentViewerEdit);
 
-  var clipboard = SpecialPowers.Cc["@mozilla.org/widget/clipboard;1"]
-                            .getService(SpecialPowers.Ci.nsIClipboard);
+  var clipboard = SpecialPowers.Services.clipboard;
 
   var textarea = SpecialPowers.wrap(document.getElementById('input'));
 
   function copySelectionToClipboard(suppressUnicodeCheck) {
     documentViewer.copySelection();
     if (!suppressUnicodeCheck)
       ok(clipboard.hasDataMatchingFlavors(["text/unicode"], 1,1), "check text/unicode");
     if (!suppressHTMLCheck)
@@ -83,24 +80,24 @@ function testCopyPaste (isXHTML) {
     copySelectionToClipboard();
   }
   function getClipboardData(mime) {
     var transferable = SpecialPowers.Cc['@mozilla.org/widget/transferable;1']
                                     .createInstance(SpecialPowers.Ci.nsITransferable);
     transferable.init(getLoadContext());
     transferable.addDataFlavor(mime);
     clipboard.getData(transferable, 1);
-    var data = {};
+    var data = SpecialPowers.createBlankObject();
     transferable.getTransferData(mime, data, {}) ;
     return data;
   }
   function testClipboardValue(mime, expected) {
     if (suppressHTMLCheck && mime == "text/html")
       return null;
-    var data = getClipboardData(mime);
+    var data = SpecialPowers.wrap(getClipboardData(mime));
     is (data.value == null ? data.value :
         data.value.QueryInterface(SpecialPowers.Ci.nsISupportsString).data,
       expected,
       mime + " value in the clipboard");
     return data.value;
   }
   function testPasteText(expected) {
     textarea.value="";
--- a/content/base/test/file_bug557892.html
+++ b/content/base/test/file_bug557892.html
@@ -7,21 +7,18 @@ function run() {
   classList = window.frames[0].document.documentElement.classList;
   window.frames[0].location.reload();
   interval = setInterval(function(aClassList) {aClassList.toggle('a'); forcegc();}, 10, classList);
   // Let the interval run for awhile and close the window after 2 seconds.
   setTimeout(function() { clearInterval(interval); window.opener.done(); window.close(); }, 2000);
 }
 
 function forcegc(){
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  Components.utils.forceGC();
-  var wu =  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
-  wu.garbageCollect();
+  SpecialPowers.forceGC();
+  SpecialPowers.gc();
 }
 
     </script>
   </head>
   <body onload="run()">
     <iframe></iframe>
   </body>
 </html>
--- a/content/base/test/test_XHRSendData.html
+++ b/content/base/test/test_XHRSendData.html
@@ -33,18 +33,16 @@ testDoc1 = xhr.responseXML;
 is(testDoc1.inputEncoding, "windows-1252", "wrong encoding");
 
 testDoc2 = document.implementation.createDocument("", "", null);
 testDoc2.appendChild(testDoc2.createComment(" doc 2 "));
 testDoc2.appendChild(testDoc2.createElement("res"));
 testDoc2.documentElement.appendChild(testDoc2.createTextNode("text"));
 is(testDoc2.inputEncoding, null, "wrong encoding");
 
-netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
 var testData = "blahblahblahblahblahblahblaaaaaaaah. blah.";
 var extensions = [".txt",".png",".jpg",".gif",".xml", "noext"];
 var fileTypes = ["text/plain", "image/png", "image/jpeg", "image/gif", "text/xml", null];
 var testFiles = new Array;
 var testDOMFiles = new Array;
 
 // arraybuffer test objects
 var shortArray = new ArrayBuffer(1);
@@ -67,24 +65,23 @@ var longInt8View1 = new Int8Array(longAr
 var longInt8View2 = new Int8Array(longArray, 256, 128)
 
 extensions.forEach(
     function (extension) {
       var testFile = createFileWithDataExt(testData, extension);
       testFiles.push(testFile);
 
       var fileList = document.getElementById('fileList');
-      fileList.value = testFile.path;
+      SpecialPowers.wrap(fileList).value = testFile.path;
       testDOMFiles.push(fileList.files[0]);
     }
 );
 
 function createFileWithDataExt(fileData, extension) {
-  var dirSvc = SpecialPowers.Cc["@mozilla.org/file/directory_service;1"].getService(SpecialPowers.Ci.nsIProperties);
-  var testFile = dirSvc.get("ProfD", SpecialPowers.Ci.nsIFile);
+  var testFile = SpecialPowers.Services.dirsvc.get("ProfD", SpecialPowers.Ci.nsIFile);
   testFile.append("testfile" + extension);
   var outStream = SpecialPowers.Cc["@mozilla.org/network/file-output-stream;1"].createInstance(SpecialPowers.Ci.nsIFileOutputStream);
   outStream.init(testFile, 0x02 | 0x08 | 0x20, 0666, 0);
   outStream.write(fileData, fileData.length);
   outStream.close();
 
   return testFile;
 }
--- a/content/base/test/test_bug218236.html
+++ b/content/base/test/test_bug218236.html
@@ -60,18 +60,16 @@ function doAbort2() {
 }
 
 /* Utility functions */
 
 function runNextTest() {
   if (tests.length > 0) {
     var test = tests.shift();
 
-    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
     // Initialize state variables
     testName = test[0]
     currentState = 0;
     currentSequence = [];
     expectedSequence = test[2];
     currentCallback = test[3];
     postSendCallback = test[4];
 
--- a/content/base/test/test_bug382871.html
+++ b/content/base/test/test_bug382871.html
@@ -28,20 +28,17 @@ function loadHandler(evt) {
 function runTest() {
   var xhr = new XMLHttpRequest();
   xhr.onload = loadHandler;
   xhr.randomProperty = true;
   xhr.upload.randomProperty = true;
   xhr.open("GET", "test_bug382871.html");
   xhr.send();
   xhr = null;
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
+  SpecialPowers.gc();
 }
 
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(runTest);
 
 </script>
 </pre>
 </body>
--- a/content/base/test/test_bug403852.html
+++ b/content/base/test/test_bug403852.html
@@ -16,24 +16,21 @@ https://bugzilla.mozilla.org/show_bug.cg
   <canvas id="canvas"></canvas>
 </p>
 <div id="content" style="display: none">
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
-netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
-var dirSvc = SpecialPowers.Cc["@mozilla.org/file/directory_service;1"].getService(SpecialPowers.Ci.nsIProperties);
-var testFile = dirSvc.get("ProfD", SpecialPowers.Ci.nsIFile);
+var testFile = SpecialPowers.Services.dirsvc.get("ProfD", SpecialPowers.Ci.nsIFile);
 testFile.append("prefs.js");
 
 var fileList = document.getElementById('fileList');
-fileList.value = testFile.path;
+SpecialPowers.wrap(fileList).value = testFile.path;
 
 // Make sure the file is accessible with indexed notation
 var domFile = fileList.files[0];
 
 is(domFile.name, "prefs.js", "fileName should be prefs.js");
 
 ok("lastModifiedDate" in domFile, "lastModifiedDate must be present");
 
--- a/content/base/test/test_bug416383.html
+++ b/content/base/test/test_bug416383.html
@@ -16,26 +16,23 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <div id="test_parent"><div someattr="foo"></div></div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 416383 **/
 
 function runTest() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   var testParent = document.getElementById("test_parent");
   var attrs = testParent.firstChild.attributes;
   ok(attrs != null, "Element should have attributes!");
   var attr = testParent.firstChild.getAttributeNode("someattr");
   ok(attr.value == "foo", "The value of the attribute should be 'foo'!");
   testParent.removeChild(testParent.firstChild);
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
+  SpecialPowers.gc();
   ok(true, "Browser should not crash!")
 
 }
 
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(runTest);
 addLoadEvent(SimpleTest.finish);
 
--- a/content/base/test/test_bug421602.html
+++ b/content/base/test/test_bug421602.html
@@ -18,40 +18,33 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 421602 **/
 SimpleTest.waitForExplicitFinish();
 
 var img1loaded = false;
 var img1errored = false;
 
-function gc() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
-}
-
 // Our test image
 function loadTestImage() {
   var img1 = new Image();
   img1.onload = function() {
     img1loaded = true;
     finishTest();
   }
   img1.onerror = function() {
     img1errored = true;
     finishTest();
   }
   img1.src = window.location.href + "?image1=true";
 }
 loadTestImage();
 
 // Probably overkill to gc() more than once, but let's be safe
-gc(); gc(); gc();
+SpecialPowers.gc(); SpecialPowers.gc(); SpecialPowers.gc();
 
 function finishTest() {
   is(img1errored, true, "Image 1 should error");
   is(img1loaded, false, "Image 1 should not load");
   SimpleTest.finish();
 }
 </script>
 </pre>
--- a/content/base/test/test_bug448993.html
+++ b/content/base/test/test_bug448993.html
@@ -14,35 +14,28 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 448993 **/
 
-function gc() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
-}
-
 function runTest() {
   var a = document.getElementById("a");
   var b = document.getElementById("b");
   var c = document.getElementById("c")
   var r = document.createRange();
   r.setStart(b, 0); 
   r.setEnd(a, 2); 
   c.appendChild(b);
   r.extractContents();
   var s = document.createRange();
   s.setEnd(b, 0); 
-  gc();
+  SpecialPowers.gc();
   s.deleteContents();
   ok(true, "test did not crash");
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(runTest);
 
--- a/content/base/test/test_bug456262.html
+++ b/content/base/test/test_bug456262.html
@@ -15,24 +15,21 @@ https://bugzilla.mozilla.org/show_bug.cg
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 456262 **/
 
 function runTest() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   document.expando = document;
   document.documentElement.expando = document;
   document.documentElement.setAttribute("foo", "bar");
   document.documentElement.getAttributeNode("foo").expando = document;
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
+  SpecialPowers.gc();
   ok(document.expando, "Should have preserved the expando!");
   ok(document.documentElement.expando, "Should have preserved the expando!");
   ok(document.documentElement.getAttributeNode('foo').expando,
      "Should have preserved the expando!");
 }
 
 runTest();
 
--- a/content/base/test/test_bug459424.html
+++ b/content/base/test/test_bug459424.html
@@ -15,24 +15,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 459424 **/
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(function() {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  var viewer =
-    window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-          .getInterface(Components.interfaces.nsIWebNavigation)
-          .QueryInterface(Components.interfaces.nsIDocShell)
-          .contentViewer
-          .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);
-  viewer.fullZoom = 2;
+  SpecialPowers.setFullZoom(window, 2);
   is(true, true, "Gotta test something");
-  viewer.fullZoom = 1;
+  SpecialPowers.setFullZoom(window, 1);
   SimpleTest.finish();
 });
 </script>
 </pre>
 </body>
 </html>
--- a/content/base/test/test_bug465767.html
+++ b/content/base/test/test_bug465767.html
@@ -14,29 +14,22 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="content" style="display: none">
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 465767 **/
 
-function CC() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
-}
-
 function runTest() {
   var node = document.createElement("div");
   var e = null;
   try {
     document.implementation.createDocument("", "", null).adoptNode(node);
-    CC();
+    SpecialPowers.gc();
     document.implementation.createDocument("", "", null).adoptNode(node);
   } catch(ex) {
     e = ex;
   }
   is(e, null, ".adoptNode didn't succeed!");
   SimpleTest.finish();
 }
 
--- a/content/base/test/test_bug592829.html
+++ b/content/base/test/test_bug592829.html
@@ -14,22 +14,20 @@ https://bugzilla.mozilla.org/show_bug.cg
 <script type="application/javascript">
 /** Test for Bug 592829 **/
 
 // NOTE! It's imperative that we don't call .init() here. Otherwise we're not
 // testing what happens if parsing fails.
 // If we ever change how DOMParser initilization works, just update this code
 // to create a DOMParser which is not allowed to parse XUL.
 
-netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
 var isXUL = true;
 try {
   var x =
-    Components.classes["@mozilla.org/xmlextras/domparser;1"]
+    SpecialPowers.Cc
       .getService(Components.interfaces.nsIDOMParser)
       .parseFromString('<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>', "text/xml");
   isXUL = x.documentElement.namespaceURI ==
     "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 }
 catch (ex) {
   isXUL = false;
 }
--- a/content/base/test/test_mozfiledataurl.html
+++ b/content/base/test/test_mozfiledataurl.html
@@ -204,20 +204,18 @@ var basePath = "";
 function getFile(name) {
   if (!basePath) {
     let xhr = new XMLHttpRequest;
     xhr.open("GET", "/dynamic/getMyDirectory.sjs", false);
     xhr.send();
     basePath = xhr.responseText;
   }
 
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-
   var fileList = document.getElementById('fileList');
-  fileList.value = basePath + name;
+  SpecialPowers.wrap(fileList).value = basePath + name;
 
   return fileList.files[0];
 }
 
 </script>
 </pre>
 </body>
 </html>
--- a/content/canvas/test/webgl/test_webgl_conformance_test_suite.html
+++ b/content/canvas/test/webgl/test_webgl_conformance_test_suite.html
@@ -85,20 +85,17 @@ function start() {
   var kOS = null;
   var kOSVersion = null;
   var kGLDriver = null;
 
   if (navigator.platform.indexOf('Win') == 0) {
     kOS = OS_WINDOWS;
     
     // code borrowed from browser/modules/test/browser_taskbar_preview.js
-    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-    var version = SpecialPowers.Cc['@mozilla.org/system-info;1']
-                               .getService(SpecialPowers.Ci.nsIPropertyBag2)
-                               .getProperty('version');
+    var version = SpecialPowers.Services.sysinfo.getProperty('version');
     kOSVersion = parseFloat(version);
     // Version 6.0 is Vista, 6.1 is 7.
   } else if (navigator.platform.indexOf('Mac') == 0) {
     kOS = OS_MAC;
     
     var versionMatch = /Mac OS X (\d+.\d+)/.exec(navigator.userAgent);
     kOSVersion = versionMatch ? parseFloat(versionMatch[1]) : null;
   } else if (navigator.appVersion.indexOf('Android') != -1) {
@@ -134,17 +131,16 @@ function start() {
     info('GL driver not detected.');
   }
   
   var requestLongerTimeoutLen = 3;
   if (kOS == OS_ANDROID)
     requestLongerTimeoutLen = 6;
 
   function getEnv(env) {
-    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
     var envsvc = SpecialPowers.Cc["@mozilla.org/process/environment;1"].getService(SpecialPowers.Ci.nsIEnvironment);
     var val = envsvc.get(env);
     if (val == "")
       return null;
     return val;
   }
 
   var reportType = WebGLTestHarnessModule.TestHarness.reportType;
--- a/content/events/test/bug426082.html
+++ b/content/events/test/bug426082.html
@@ -44,18 +44,17 @@ function isRectContainedInRectFromRegion
     return rect.left >= r.left &&
            rect.top >= r.top &&
            rect.right <= r.right &&
            rect.bottom <= r.bottom;
   });
 }
 
 function paintListener(e) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  if (isRectContainedInRectFromRegion(buttonRect(), e.clientRects)) {
+  if (isRectContainedInRectFromRegion(buttonRect(), SpecialPowers.wrap(e).clientRects)) {
     gNeedsPaint = false;
     currentSnapshot = takeSnapshot();
   }
 }
 
 var gNeedsPaint = false;
 function executeTests() {
   var testYielder = tests();
--- a/content/events/test/bug656379-1.html
+++ b/content/events/test/bug656379-1.html
@@ -60,18 +60,17 @@ function isRectContainedInRectFromRegion
     return rect.left >= r.left &&
            rect.top >= r.top &&
            rect.right <= r.right &&
            rect.bottom <= r.bottom;
   });
 }
 
 function paintListener(e) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  if (isRectContainedInRectFromRegion(buttonRect(), e.clientRects)) {
+  if (isRectContainedInRectFromRegion(buttonRect(), SpecialPowers.wrap(e).clientRects)) {
     gNeedsPaint = false;
     currentSnapshot = takeSnapshot();
   }
 }
 
 var gNeedsPaint = false;
 function executeTests() {
   var testYielder = tests();
--- a/content/events/test/test_bug402089.html
+++ b/content/events/test/test_bug402089.html
@@ -47,19 +47,17 @@ function clickHandler(e) {
   e.stopPropagation();
   e.preventDefault();
   window.removeEventListener("click", clickHandler, true);
   setTimeout(testCachedEvent, 10);
 }
 
 function doTest() {
   window.addEventListener("click", clickHandler, true);
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-  var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
-                     getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var utils = SpecialPowers.getDOMWindowUtils(window);
   utils.sendMouseEvent("mousedown", 1, 1, 0, 1, 0);
   utils.sendMouseEvent("mouseup", 1, 1, 0, 1, 0);
 
 }
 
 SimpleTest.waitForExplicitFinish();
 </script>
 </pre>
--- a/content/events/test/test_bug448602.html
+++ b/content/events/test/test_bug448602.html
@@ -146,18 +146,17 @@ function runTests() {
     ok(false, "Should have thrown an exception.");
   } catch (ex) {
     ok(true, "We should be still running.");
   }
   setTimeout(testAllListener, 0);
 }
 
 function dispatchTrusted(t, o) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  t.dispatchEvent(new Event("testevent", o));
+  SpecialPowers.dispatchEvent(window, t, new Event("testevent", o));
 }
 
 function testAllListener() {
   els = SpecialPowers.wrap(els);
   var results = [];
   var expectedResults =
     [ { target: "testlevel3", phase: 3, trusted: false },
       { target: "testlevel3", phase: 3, trusted: false },
--- a/content/events/test/test_bug450876.html
+++ b/content/events/test/test_bug450876.html
@@ -15,24 +15,21 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 450876 **/
 
 function doTest() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   is(document.activeElement, document.body, "body element should be focused");
   document.getElementById('a').focus();
   is(document.activeElement, document.getElementById('a'), "link should have focus");
-  var wu =  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                .getInterface(Components.interfaces.nsIDOMWindowUtils);
   is(document.hasFocus(), true, "document should be focused");
-  wu.sendKeyEvent('keypress',  9, 0, 0);
+  SpecialPowers.DOMWindowUtils.sendKeyEvent('keypress',  9, 0, 0);
   is(document.activeElement, document.getElementById('a'), "body element should be focused");
   is(document.hasFocus(), false, "document should not be focused");
 
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(doTest);
--- a/content/events/test/test_bug456273.html
+++ b/content/events/test/test_bug456273.html
@@ -18,22 +18,21 @@ https://bugzilla.mozilla.org/show_bug.cg
 <div id="edit456273" contenteditable="true">text</div>
 
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 456273 **/
 
 function doTest() {
-  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-
   var ev = document.createEvent('KeyEvents');
   ev.initKeyEvent("keypress", true, true, null, true, false,
                  false, false, 0, "z".charCodeAt(0));
-  document.getElementById('edit456273').dispatchEvent(ev);
+  SpecialPowers.dispatchEvent(window, document.getElementById('edit456273'), ev);
+
   ok(true, "PASS");
   SimpleTest.finish();
 }
 
 SimpleTest.waitForExplicitFinish();
 addLoadEvent(doTest);
 
 </script>
--- a/content/events/test/test_bug605242.html
+++ b/content/events/test/test_bug605242.html
@@ -16,29 +16,24 @@ https://bugzilla.mozilla.org/show_bug.cg
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 605242 **/
 
 SimpleTest.waitForExplicitFinish();
 
+var utils = SpecialPowers.getDOMWindowUtils(window);
 function sendMouseDown(el) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   var rect = el.getBoundingClientRect();
-  var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                    .getInterface(Components.interfaces.nsIDOMWindowUtils);
   utils.sendMouseEvent('mousedown', rect.left + 5, rect.top + 5, 0, 1, 0);
 }
 
 function sendMouseUp(el) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   var rect = el.getBoundingClientRect();
-  var utils = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                    .getInterface(Components.interfaces.nsIDOMWindowUtils);
   utils.sendMouseEvent('mouseup', rect.left + 5, rect.top + 5, 0, 1, 0);
 }
 
 function runTest() {
   var b = document.getElementById("testbutton");
   sendMouseDown(b);
   var l = document.querySelectorAll(":active");
 
--- a/content/html/content/test/forms/test_pattern_attribute.html
+++ b/content/html/content/test/forms/test_pattern_attribute.html
@@ -29,46 +29,41 @@ var gInvalid = false;
 function invalidEventHandler(e)
 {
   is(e.type, "invalid", "Invalid event type should be invalid");
   gInvalid = true;
 }
 
 function completeValidityCheck(element, alwaysValid, isBarred)
 {
-  if (element.type == 'file') {
-    // Need privileges to set a filename with .value.
-    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  }
-
   // Check when pattern matches.
   if (element.type == 'email') {
     element.pattern = ".*@bar.com";
     element.value = "foo@bar.com";
   } else if (element.type == 'url') {
     element.pattern = "http://.*\\.com$";
     element.value = "http://mozilla.com";
   } else {
     element.pattern = "foo";
-    element.value = "foo";
+    SpecialPowers.wrap(element).value = "foo";
   }
 
   checkValidPattern(element, true, isBarred);
 
   // Check when pattern does not match.
 
   if (element.type == 'email') {
     element.pattern = ".*@bar.com";
     element.value = "foo@foo.com";
   } else if (element.type == 'url') {
     element.pattern = "http://.*\\.com$";
     element.value = "http://mozilla.org";
   } else {
     element.pattern = "foo";
-    element.value = "bar";
+    SpecialPowers.wrap(element).value = "bar";
   }
 
   if (!alwaysValid) {
     checkInvalidPattern(element, true);
   } else {
     checkValidPattern(element, true, isBarred);
   }
 }
--- a/content/html/content/test/forms/test_validation.html
+++ b/content/html/content/test/forms/test_validation.html
@@ -263,20 +263,18 @@ function checkCheckValidity(element)
 }
 
 function checkValidityStateObjectAliveWithoutElement(element)
 {
   // We are creating a temporary element and getting it's ValidityState object.
   // Then, we make sure it is removed by the garbage collector and we check the
   // ValidityState default values (it should not crash).
 
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
   var v = document.createElement(element).validity;
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
-    getInterface(Components.interfaces.nsIDOMWindowUtils).garbageCollect();
+  SpecialPowers.gc();
 
   ok(!v.valueMissing,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.typeMismatch,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.patternMismatch,
     "When the element is not alive, it shouldn't suffer from constraint validation");
   ok(!v.tooLong,
--- a/content/html/content/test/test_bug209275.xhtml
+++ b/content/html/content/test/test_bug209275.xhtml
@@ -72,19 +72,17 @@ function link123HrefIs(href, testNum) {
   is($('link1').href, href, "link1 test " + testNum);
   is($('link2').href, href, "link2 test " + testNum);
   is($('link3').href, href, "link3 test " + testNum);
 }
 
 var gGen;
 
 function visitedDependentComputedStyle(win, elem, property) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  var utils = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
-                getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var utils = SpecialPowers.getDOMWindowUtils(window);
   return utils.getVisitedDependentComputedStyle(elem, "", property);
 }
 
 function getColor(elem) {
   return visitedDependentComputedStyle(document.defaultView, elem, "color");
 }
 
 function getFill(elem) {
--- a/content/html/content/test/test_bug406596.html
+++ b/content/html/content/test/test_bug406596.html
@@ -20,19 +20,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   <div tabindex="0">This text is not editable but is focusable</div>
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 406596 **/
 
 function testTabbing(click, focus, selectionOffset) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  var wu =  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var wu = SpecialPowers.getDOMWindowUtils(window);
 
   var elem = document.getElementById(click);
   var rect = elem.getBoundingClientRect();
   var selection = window.getSelection();
 
   var x = (rect.left + rect.right) / 4;
   var y = (rect.top + rect.bottom) / 2;
   wu.sendMouseEvent("mousedown", x, y, 0, 1, 0);
--- a/content/html/content/test/test_bug421640.html
+++ b/content/html/content/test/test_bug421640.html
@@ -16,19 +16,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   <div><button id="button">Test</button></div> 
 </div>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 
 /** Test for Bug 421640 **/
 
 function test(click, focus, nextFocus) {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  var wu =  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                  .getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var wu = SpecialPowers.getDOMWindowUtils(window);
 
   var selection = window.getSelection();
   var edit = document.getElementById("edit");
   var text = edit.firstChild;
 
   selection.removeAllRanges();
 
   var rect = edit.getBoundingClientRect();
--- a/content/html/content/test/test_bug460568.html
+++ b/content/html/content/test/test_bug460568.html
@@ -31,25 +31,24 @@ function runTest()
 
   function isReallyEditable()
   {
     editor.focus();
     var range = document.createRange();
     range.selectNodeContents(editor);
     var prevStr = range.toString();
 
-    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
-    var docShell =
-      window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-            .getInterface(Components.interfaces.nsIWebNavigation)
-            .QueryInterface(Components.interfaces.nsIDocShell);
+    var docShell = SpecialPowers.wrap(window)
+            .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+            .getInterface(SpecialPowers.Ci.nsIWebNavigation)
+            .QueryInterface(SpecialPowers.Ci.nsIDocShell);
     var controller =
-          docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                  .getInterface(Components.interfaces.nsISelectionDisplay)
-                  .QueryInterface(Components.interfaces.nsISelectionController);
+          docShell.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor)
+                  .getInterface(SpecialPowers.Ci.nsISelectionDisplay)
+                  .QueryInterface(SpecialPowers.Ci.nsISelectionController);
     var sel = controller.getSelection(controller.SELECTION_NORMAL);
     sel.collapse(anchorInEditor, 0);
     synthesizeKey('a', {});
     range.selectNodeContents(editor);
     return prevStr != range.toString();
   }
 
   focused = false;
--- a/content/html/content/test/test_bug481335.xhtml
+++ b/content/html/content/test/test_bug481335.xhtml
@@ -46,20 +46,17 @@ let tests = testIterator();
 function continueTest() {
   tests.next();
 }
 
 function checkLinkColor(aElmId, aExpectedColor, aMessage) {
   // Because link coloring is asynchronous, we wait until we get the right
   // result, or we will time out (resulting in a failure).
   function getColor() {
-    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-    var utils = document.defaultView.
-                  QueryInterface(Components.interfaces.nsIInterfaceRequestor).
-                  getInterface(Components.interfaces.nsIDOMWindowUtils);
+    var utils = SpecialPowers.getDOMWindowUtils(window);
     return utils.getVisitedDependentComputedStyle($(aElmId), "", "color");
   }
   while (getColor() != aExpectedColor) {
     setTimeout(continueTest, 0);
     return false;
   }
   is(getColor(), aExpectedColor, aMessage);
   return true;
--- a/content/html/content/test/test_formSubmission2.html
+++ b/content/html/content/test/test_formSubmission2.html
@@ -147,20 +147,17 @@ function checkSubmission(sub, expected) 
 
 function clickImage(aTarget, aX, aY)
 {
   aTarget.style.position = "absolute";
   aTarget.style.top = "0";
   aTarget.style.left = "0";
   aTarget.offsetTop;
 
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  var wu = aTarget.ownerDocument.defaultView
-    .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-    .getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var wu = SpecialPowers.getDOMWindowUtils(aTarget.ownerDocument.defaultView);
 
   wu.sendMouseEvent('mousedown',  aX, aY, 0, 1, 0);
   wu.sendMouseEvent('mouseup',  aX, aY, 0, 0, 0);
 
   aTarget.style.position = "";
   aTarget.style.top = "";
   aTarget.style.left = "";
 }
--- a/content/html/document/test/test_bug446483.html
+++ b/content/html/document/test/test_bug446483.html
@@ -15,20 +15,17 @@ https://bugzilla.mozilla.org/show_bug.cg
   
 </div>
 <pre id="test">
 <script type="application/javascript">
 
 /** Test for Bug 446483 **/
 
 function gc() {
-  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
-  window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-        .getInterface(Components.interfaces.nsIDOMWindowUtils)
-        .garbageCollect();
+  SpecialPowers.gc();
 }
 
 function runTest() {
   document.getElementById('display').innerHTML =
     '<iframe src="bug446483-iframe.html"><\/iframe>\n' +
     '<iframe src="bug446483-iframe.html"><\/iframe>\n';
 
   setInterval(gc, 1000);
--- a/content/html/document/test/test_viewport.html
+++ b/content/html/document/test/test_viewport.html
@@ -22,20 +22,17 @@ href="https://bugzilla.mozilla.org/show_
 SimpleTest.waitForExplicitFinish();
 
 function testViewport() {
 
   /* We need to access the document headers, which are chrome-only. */
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
   /* Grab Viewport Metadata from the document header. */
-  var iRequester =
-    window.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
-  var windowUtils =
-    iRequester.getInterface(Components.interfaces.nsIDOMWindowUtils);
+  var windowUtils = SpecialPowers.getDOMWindowUtils(window);
   var vpWidth =
     parseInt(windowUtils.getDocumentMetadata("viewport-width"));
   var vpHeight =
     parseInt(windowUtils.getDocumentMetadata("viewport-height"));
   var vpInitialScale = 
     parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
   var vpMaxScale = 
     parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
--- a/js/xpconnect/idl/xpccomponents.idl
+++ b/js/xpconnect/idl/xpccomponents.idl
@@ -115,17 +115,17 @@ interface nsIXPCComponents_utils_Sandbox
 interface ScheduledGCCallback : nsISupports
 {
     void callback();
 };
 
 /**
 * interface of Components.utils
 */
-[scriptable, uuid(3b3b5adc-dce0-4d4e-a04e-06b2c8b5acfe)]
+[scriptable, uuid(c9ccec7a-726c-4479-9438-6f51f6ef4170)]
 interface nsIXPCComponents_Utils : nsISupports
 {
 
     /* reportError is designed to be called from JavaScript only.
      *
      * It will report a JS Error object to the JS console, and return. It
      * is meant for use in exception handler blocks which want to "eat"
      * an exception, but still want to report it to the console.
@@ -151,16 +151,42 @@ interface nsIXPCComponents_Utils : nsISu
      * var thirtyFive = C.u.evalInSandbox("five * seven", s);
      */
     [implicit_jscontext,optional_argc]
     jsval evalInSandbox(in AString source, in jsval sandbox,
                         [optional] in jsval version,
                         [optional] in jsval filename,
                         [optional] in long lineNo);
 
+    /*
+     * getSandboxMetadata is designed to be called from JavaScript only.
+     *
+     * getSandboxMetadata retrieves the metadata associated with
+     * a sandbox object. It will return undefined if there
+     * is no metadata attached to the sandbox.
+     *
+     * var s = C.u.Sandbox(..., { metadata: "metadata" });
+     * var metadata = C.u.getSandboxMetadata(s);
+     */
+    [implicit_jscontext]
+    jsval getSandboxMetadata(in jsval sandbox);
+
+    /*
+     * setSandboxMetadata is designed to be called from JavaScript only.
+     *
+     * setSandboxMetadata sets the metadata associated with
+     * a sandbox object.
+     *
+     * Note that the metadata object will be copied before being used.
+     * The copy will be performed using the structured clone algorithm.
+     * Note that this algorithm does not support reflectors and
+     * it will throw if it encounters them.
+     */
+    [implicit_jscontext]
+    void setSandboxMetadata(in jsval sandbox, in jsval metadata);
 
     /*
      * import is designed to be called from JavaScript only.
      *
      * Synchronously loads and evaluates the js file located at
      * 'registryLocation' with a new, fully privileged global object.
      *
      * If 'targetObj' is specified and equal to null, returns the
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -580,19 +580,21 @@ sandbox_convert(JSContext *cx, HandleObj
     if (type == JSTYPE_OBJECT) {
         vp.set(OBJECT_TO_JSVAL(obj));
         return true;
     }
 
     return JS_ConvertStub(cx, obj, type, vp);
 }
 
+#define XPCONNECT_SANDBOX_CLASS_METADATA_SLOT (XPCONNECT_GLOBAL_EXTRA_SLOT_OFFSET)
+
 static const JSClass SandboxClass = {
     "Sandbox",
-    XPCONNECT_GLOBAL_FLAGS,
+    XPCONNECT_GLOBAL_FLAGS_WITH_EXTRA_SLOTS(1),
     JS_PropertyStub,   JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub,
     sandbox_enumerate, sandbox_resolve, sandbox_convert,  sandbox_finalize,
     NULL, NULL, NULL, NULL, TraceXPCGlobal
 };
 
 static const JSFunctionSpec SandboxFunctions[] = {
     JS_FS("dump",    SandboxDump,    1,0),
     JS_FS("debug",   SandboxDebug,   1,0),
@@ -1037,16 +1039,18 @@ xpc::CreateSandboxObject(JSContext *cx, 
         if (!options.wantXrays && !xpc::WrapperFactory::WaiveXrayAndWrap(cx, vp))
             return NS_ERROR_UNEXPECTED;
     }
 
     // Set the location information for the new global, so that tools like
     // about:memory may use that information
     xpc::SetLocationForGlobal(sandbox, options.sandboxName);
 
+    xpc::SetSandboxMetadata(cx, sandbox, options.metadata);
+
     JS_FireOnNewGlobalObject(cx, sandbox);
 
     return NS_OK;
 }
 
 /* bool call(in nsIXPConnectWrappedNative wrapper,
  *           in JSContextPtr cx,
  *           in JSObjectPtr obj,
@@ -1334,16 +1338,20 @@ ParseOptionsObject(JSContext *cx, jsval 
                                   "sandboxName", options.sandboxName);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = GetObjPropFromOptions(cx, optionsObject,
                                "sameZoneAs", options.sameZoneAs.address());
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = GetDOMConstructorsFromOptions(cx, optionsObject, options);
+
+    bool found;
+    rv = GetPropFromOptions(cx, optionsObject,
+                            "metadata", &options.metadata, &found);
     NS_ENSURE_SUCCESS(rv, rv);
 
     return NS_OK;
 }
 
 static nsresult
 AssembleSandboxMemoryReporterName(JSContext *cx, nsCString &sandboxName)
 {
@@ -1646,8 +1654,45 @@ xpc::NewFunctionForwarder(JSContext *cx,
     if (!fun)
         return false;
 
     JSObject *funobj = JS_GetFunctionObject(fun);
     js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
     vp.setObject(*funobj);
     return true;
 }
+
+
+nsresult
+xpc::GetSandboxMetadata(JSContext *cx, HandleObject sandbox, MutableHandleValue rval)
+{
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(IsSandbox(sandbox));
+
+    RootedValue metadata(cx);
+    {
+      JSAutoCompartment ac(cx, sandbox);
+      metadata = JS_GetReservedSlot(sandbox, XPCONNECT_SANDBOX_CLASS_METADATA_SLOT);
+    }
+
+    if (!JS_WrapValue(cx, metadata.address()))
+        return NS_ERROR_UNEXPECTED;
+
+    rval.set(metadata);
+    return NS_OK;
+}
+
+nsresult
+xpc::SetSandboxMetadata(JSContext *cx, HandleObject sandbox, HandleValue metadataArg)
+{
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(IsSandbox(sandbox));
+
+    RootedValue metadata(cx);
+
+    JSAutoCompartment ac(cx, sandbox);
+    if (!JS_StructuredClone(cx, metadataArg, metadata.address(), NULL, NULL))
+        return NS_ERROR_UNEXPECTED;
+
+    JS_SetReservedSlot(sandbox, XPCONNECT_SANDBOX_CLASS_METADATA_SLOT, metadata);
+
+    return NS_OK;
+}
--- a/js/xpconnect/src/XPCComponents.cpp
+++ b/js/xpconnect/src/XPCComponents.cpp
@@ -2761,16 +2761,56 @@ nsXPCComponents_Utils::EvalInSandbox(con
     RootedValue rval(cx);
     nsresult rv = xpc::EvalInSandbox(cx, sandbox, source, filename.get(), lineNo,
                                      jsVersion, false, &rval);
     NS_ENSURE_SUCCESS(rv, rv);
     *retval = rval;
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsXPCComponents_Utils::GetSandboxMetadata(const JS::Value &sandboxVal,
+                                          JSContext *cx, JS::Value *rval)
+{
+    if (!sandboxVal.isObject())
+        return NS_ERROR_INVALID_ARG;
+
+    RootedObject sandbox(cx, &sandboxVal.toObject());
+    sandbox = js::CheckedUnwrap(sandbox);
+    if (!sandbox || !xpc::IsSandbox(sandbox))
+        return NS_ERROR_INVALID_ARG;
+
+    RootedValue metadata(cx);
+    nsresult rv = xpc::GetSandboxMetadata(cx, sandbox, &metadata);
+    NS_ENSURE_SUCCESS(rv, rv);
+    *rval = metadata;
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsXPCComponents_Utils::SetSandboxMetadata(const JS::Value &sandboxVal,
+                                          const JS::Value &metadataVal,
+                                          JSContext *cx)
+{
+    if (!sandboxVal.isObject())
+        return NS_ERROR_INVALID_ARG;
+
+    RootedObject sandbox(cx, &sandboxVal.toObject());
+    sandbox = js::CheckedUnwrap(sandbox);
+    if (!sandbox || !xpc::IsSandbox(sandbox))
+        return NS_ERROR_INVALID_ARG;
+
+    RootedValue metadata(cx, metadataVal);
+    nsresult rv = xpc::SetSandboxMetadata(cx, sandbox, metadata);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+}
+
 /* JSObject import (in AUTF8String registryLocation,
  *                  [optional] in JSObject targetObj);
  */
 NS_IMETHODIMP
 nsXPCComponents_Utils::Import(const nsACString& registryLocation,
                               const JS::Value& targetObj,
                               JSContext* cx,
                               uint8_t optionalArgc,
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3599,25 +3599,27 @@ struct SandboxOptions {
     };
 
     SandboxOptions(JSContext *cx)
         : wantXrays(true)
         , wantComponents(true)
         , wantExportHelpers(false)
         , proto(xpc_GetSafeJSContext())
         , sameZoneAs(xpc_GetSafeJSContext())
+        , metadata(xpc_GetSafeJSContext())
     { }
 
     bool wantXrays;
     bool wantComponents;
     bool wantExportHelpers;
     JS::RootedObject proto;
     nsCString sandboxName;
     JS::RootedObject sameZoneAs;
     DOMConstructors DOMConstructors;
+    JS::RootedValue metadata;
 };
 
 JSObject *
 CreateGlobalObject(JSContext *cx, const JSClass *clasp, nsIPrincipal *principal,
                    JS::CompartmentOptions& aOptions);
 
 // Helper for creating a sandbox object to use for evaluating
 // untrusted code completely separated from all other code in the
@@ -3642,18 +3644,29 @@ CreateSandboxObject(JSContext *cx, jsval
 // an exception to a string, evalInSandbox will return an NS_ERROR_*
 // result, and cx->exception will be empty.
 nsresult
 EvalInSandbox(JSContext *cx, JS::HandleObject sandbox, const nsAString& source,
               const char *filename, int32_t lineNo,
               JSVersion jsVersion, bool returnStringOnly,
               JS::MutableHandleValue rval);
 
+// Helper for retrieving metadata stored in a reserved slot. The metadata
+// is set during the sandbox creation using the "metadata" option.
+nsresult
+GetSandboxMetadata(JSContext *cx, JS::HandleObject sandboxArg,
+                   JS::MutableHandleValue rval);
+
+nsresult
+SetSandboxMetadata(JSContext *cx, JS::HandleObject sandboxArg,
+                   JS::HandleValue metadata);
+
 } /* namespace xpc */
 
+
 /***************************************************************************/
 // Inlined utilities.
 
 inline bool
 xpc_ForcePropertyResolve(JSContext* cx, JSObject* obj, jsid id);
 
 inline jsid
 GetRTIdByIndex(JSContext *cx, unsigned index);
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_sandbox_metadata.js
@@ -0,0 +1,45 @@
+/* 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/. */
+
+/* See https://bugzilla.mozilla.org/show_bug.cgi?id=898559 */
+
+function run_test()
+{
+  let sandbox = Components.utils.Sandbox("http://www.blah.com", {
+    metadata: "test metadata"
+  });
+
+  do_check_eq(Components.utils.getSandboxMetadata(sandbox), "test metadata");
+
+  let sandbox = Components.utils.Sandbox("http://www.blah.com", {
+    metadata: { foopy: { bar: 2 }, baz: "hi" }
+  });
+
+  let metadata = Components.utils.getSandboxMetadata(sandbox);
+  do_check_eq(metadata.baz, "hi");
+  do_check_eq(metadata.foopy.bar, 2);
+  metadata.baz = "foo";
+
+  metadata = Components.utils.getSandboxMetadata(sandbox);
+  do_check_eq(metadata.baz, "foo");
+
+  metadata = { foo: "bar" };
+  Components.utils.setSandboxMetadata(sandbox, metadata);
+  metadata.foo = "baz";
+  metadata = Components.utils.getSandboxMetadata(sandbox);
+  do_check_eq(metadata.foo, "bar");
+
+  let thrown = false;
+  let reflector = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+                    .createInstance(Components.interfaces.nsIXMLHttpRequest);
+
+  try {
+    Components.utils.setSandboxMetadata(sandbox, { foo: reflector });
+  } catch(e) {
+    thrown = true;
+  }
+
+  do_check_eq(thrown, true);
+}
+
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -46,16 +46,17 @@ fail-if = os == "android"
 # Bug 676965: test fails consistently on Android
 fail-if = os == "android"
 [test_tearoffs.js]
 [test_want_components.js]
 [test_components.js]
 [test_allowedDomains.js]
 [test_allowedDomainsXHR.js]
 [test_nuke_sandbox.js]
+[test_sandbox_metadata.js]
 [test_exportFunction.js]
 [test_textDecoder.js]
 [test_watchdog_enable.js]
 head = head_watchdog.js
 [test_watchdog_disable.js]
 head = head_watchdog.js
 [test_watchdog_toggle.js]
 head = head_watchdog.js
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -950,23 +950,22 @@ public class GeckoAppShell
 
     @GeneratableAndroidBridgeTarget(stubName = "GetExtensionFromMimeTypeWrapper")
     static String getExtensionFromMimeType(String aMimeType) {
         return MimeTypeMap.getSingleton().getExtensionFromMimeType(aMimeType);
     }
 
     @GeneratableAndroidBridgeTarget(stubName = "GetMimeTypeFromExtensionsWrapper")
     static String getMimeTypeFromExtensions(String aFileExt) {
-        MimeTypeMap mtm = MimeTypeMap.getSingleton();
         StringTokenizer st = new StringTokenizer(aFileExt, ".,; ");
         String type = null;
         String subType = null;
         while (st.hasMoreElements()) {
             String ext = st.nextToken();
-            String mt = mtm.getMimeTypeFromExtension(ext);
+            String mt = getMimeTypeFromExtension(ext);
             if (mt == null)
                 continue;
             int slash = mt.indexOf('/');
             String tmpType = mt.substring(0, slash);
             if (!tmpType.equalsIgnoreCase(type))
                 type = type == null ? tmpType : "*";
             String tmpSubType = mt.substring(slash + 1);
             if (!tmpSubType.equalsIgnoreCase(subType))
@@ -1741,16 +1740,24 @@ public class GeckoAppShell
                     Log.i(LOGTAG, "[OPENFILE] " + name + "(" + split[pidColumn] + ") : " + file);
             }
             in.close();
         } catch (Exception e) { }
     }
 
     @GeneratableAndroidBridgeTarget
     public static void scanMedia(String aFile, String aMimeType) {
+        // If the platform didn't give us a mimetype, try to guess one from the filename
+        if (TextUtils.isEmpty(aMimeType)) {
+            int extPosition = aFile.lastIndexOf(".");
+            if (extPosition > 0 && extPosition < aFile.length() - 1) {
+                aMimeType = getMimeTypeFromExtension(aFile.substring(extPosition+1));
+            }
+        }
+
         Context context = getContext();
         GeckoMediaScannerClient.startScan(context, aFile, aMimeType);
     }
 
     @GeneratableAndroidBridgeTarget(stubName = "GetIconForExtensionWrapper")
     public static byte[] getIconForExtension(String aExt, int iconSize) {
         try {
             if (iconSize <= 0)
@@ -1776,20 +1783,24 @@ public class GeckoAppShell
             return buf.array();
         }
         catch (Exception e) {
             Log.w(LOGTAG, "getIconForExtension failed.",  e);
             return null;
         }
     }
 
+    private static String getMimeTypeFromExtension(String ext) {
+        final MimeTypeMap mtm = MimeTypeMap.getSingleton();
+        return mtm.getMimeTypeFromExtension(ext);
+    }
+    
     private static Drawable getDrawableForExtension(PackageManager pm, String aExt) {
         Intent intent = new Intent(Intent.ACTION_VIEW);
-        MimeTypeMap mtm = MimeTypeMap.getSingleton();
-        String mimeType = mtm.getMimeTypeFromExtension(aExt);
+        final String mimeType = getMimeTypeFromExtension(aExt);
         if (mimeType != null && mimeType.length() > 0)
             intent.setType(mimeType);
         else
             return null;
 
         List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
         if (list.size() == 0)
             return null;
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -63,17 +63,17 @@ public final class GeckoProfile {
         if (context instanceof GeckoApp) {
             // Check for a cached profile on this context already
             // TODO: We should not be caching profile information on the Activity context
             if (((GeckoApp)context).mProfile != null) {
                 return ((GeckoApp)context).mProfile;
             }
 
             GeckoProfile guest = GeckoProfile.getGuestProfile(context);
-            // if the guest profile exists and is locked, return it
+            // if the guest profile is locked, return it
             if (guest != null && guest.locked()) {
                 return guest;
             }
 
             // Otherwise, get the default profile for the Activity
             return get(context, ((GeckoApp)context).getDefaultProfileName());
         }
 
@@ -226,42 +226,54 @@ public final class GeckoProfile {
     }
 
     // Warning, Changing the lock file state from outside apis will cause this to become out of sync
     public boolean locked() {
         if (mLocked != LockState.UNDEFINED) {
             return mLocked == LockState.LOCKED;
         }
 
-        File lockFile = new File(getDir(), LOCK_FILE_NAME);
-        boolean res = lockFile.exists();
-        mLocked = res ? LockState.LOCKED : LockState.UNLOCKED;
-        return res;
+        // Don't use getDir() as it will create a dir if none exists
+        if (mDir != null && mDir.exists()) {
+            File lockFile = new File(mDir, LOCK_FILE_NAME);
+            boolean res = lockFile.exists();
+            mLocked = res ? LockState.LOCKED : LockState.UNLOCKED;
+        } else {
+            mLocked = LockState.UNLOCKED;
+        }
+
+        return mLocked == LockState.LOCKED;
     }
 
     public boolean lock() {
         try {
+            // If this dir doesn't exist getDir will create it for us
             File lockFile = new File(getDir(), LOCK_FILE_NAME);
             boolean result = lockFile.createNewFile();
             if (result) {
                 mLocked = LockState.LOCKED;
             } else {
                 mLocked = LockState.UNLOCKED;
             }
             return result;
         } catch(IOException ex) {
             Log.e(LOGTAG, "Error locking profile", ex);
         }
         mLocked = LockState.UNLOCKED;
         return false;
     }
 
     public boolean unlock() {
+        // Don't use getDir() as it will create a dir
+        if (mDir == null || !mDir.exists()) {
+            return true;
+        }
+
         try {
-            File lockFile = new File(getDir(), LOCK_FILE_NAME);
+            File lockFile = new File(mDir, LOCK_FILE_NAME);
             boolean result = delete(lockFile);
             if (result) {
                 mLocked = LockState.UNLOCKED;
             } else {
                 mLocked = LockState.LOCKED;
             }
             return result;
         } catch(IOException ex) {
--- a/mobile/android/base/health/BrowserHealthRecorder.java
+++ b/mobile/android/base/health/BrowserHealthRecorder.java
@@ -684,17 +684,17 @@ public class BrowserHealthRecorder imple
                 recordSearch(message.getString("identifier"), "bartext");
                 return;
             }
             if (EVENT_SEARCH.equals(event)) {
                 if (!message.has("location")) {
                     Log.d(LOG_TAG, "Ignoring search without location.");
                     return;
                 }
-                recordSearch(message.getString("identifier"), message.getString("location"));
+                recordSearch(message.optString("identifier", null), message.getString("location"));
                 return;
             }
         } catch (Exception e) {
             Log.e(LOG_TAG, "Exception handling message \"" + event + "\":", e);
         }
     }
 
     /*
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -474,17 +474,17 @@ pref("devtools.debugger.log", false);
 // Disable remote debugging connections
 pref("devtools.debugger.remote-enabled", false);
 pref("devtools.debugger.remote-port", 6000);
 // Force debugger server binding on the loopback interface
 pref("devtools.debugger.force-local", true);
 // Display a prompt when a new connection starts to accept/reject it
 pref("devtools.debugger.prompt-connection", true);
 // Temporary setting to enable webapps actors
-pref("devtools.debugger.enable-content-actors", false);
+pref("devtools.debugger.enable-content-actors", true);
 
 // view source
 pref("view_source.syntax_highlight", true);
 pref("view_source.wrap_long_lines", false);
 pref("view_source.editor.external", false);
 pref("view_source.editor.path", "");
 // allows to add further arguments to the editor; use the %LINE% placeholder
 // for jumping to a specific line (e.g. "/line:%LINE%" or "--goto %LINE%")
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_about_networking.js
@@ -0,0 +1,52 @@
+/* -*- Mode: Javasript; indent-tab-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+let gDashboard = Cc['@mozilla.org/network/dashboard;1']
+  .getService(Ci.nsIDashboard);
+
+const RESOLVE_DISABLE_IPV6 = (1 << 5);
+
+add_test(function test_http() {
+  gDashboard.requestHttpConnections(function(data) {
+    do_check_neq(data.host.indexOf("example.com"), -1);
+
+    run_next_test();
+  });
+});
+
+add_test(function test_dns() {
+  gDashboard.requestDNSInfo(function(data) {
+    do_check_neq(data.hostname.indexOf("example.com"), -1);
+
+    run_next_test();
+  });
+});
+
+add_test(function test_sockets() {
+  let dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
+  let record = dns.resolve("example.com", RESOLVE_DISABLE_IPV6);
+  let answer = record.getNextAddrAsString();
+
+  gDashboard.requestSockets(function(data) {
+    do_check_neq(data.host.indexOf(answer), -1);
+
+    run_next_test();
+  });
+});
+
+function run_test() {
+  let ioService = Cc["@mozilla.org/network/io-service;1"]
+    .getService(Ci.nsIIOService);
+  let uri = ioService.newURI("http://example.com", null, null);
+  let channel = ioService.newChannelFromURI(uri);
+
+  channel.open();
+
+  run_next_test();
+}
+
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -241,8 +241,9 @@ run-sequentially = Hardcoded hash value 
 run-sequentially = Hardcoded hash value includes port 4444.
 [test_bug826063.js]
 [test_bug812167.js]
 # Bug 675039: intermittent fail on Android-armv6 
 skip-if = os == "android"
 [test_tldservice_nextsubdomain.js]
 [test_about_protocol.js]
 [test_bug856978.js]
+[test_about_networking.js]
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -811,16 +811,20 @@ Download.prototype = {
     if (saver !== "copy") {
       serializable.saver = saver;
     }
 
     if (this.error && ("message" in this.error)) {
       serializable.error = { message: this.error.message };
     }
 
+    if (this.startTime) {
+      serializable.startTime = this.startTime.toJSON();
+    }
+
     // These are serialized unless they are false, null, or empty strings.
     for (let property of kSerializableDownloadProperties) {
       if (property != "error" && this[property]) {
         serializable[property] = this[property];
       }
     }
 
     return serializable;
@@ -848,17 +852,16 @@ Download.prototype = {
 
 /**
  * Defines which properties of the Download object are serializable.
  */
 const kSerializableDownloadProperties = [
   "succeeded",
   "canceled",
   "error",
-  "startTime",
   "totalBytes",
   "hasPartialData",
   "tryToKeepPartialData",
   "launcherPath",
   "launchWhenSucceeded",
   "contentType",
 ];
 
@@ -896,16 +899,23 @@ Download.fromSerializable = function (aS
   }
   if ("saver" in aSerializable) {
     download.saver = DownloadSaver.fromSerializable(aSerializable.saver);
   } else {
     download.saver = DownloadSaver.fromSerializable("copy");
   }
   download.saver.download = download;
 
+  if ("startTime" in aSerializable) {
+    let time = aSerializable.startTime.getTime
+             ? aSerializable.startTime.getTime()
+             : aSerializable.startTime;
+    download.startTime = new Date(time);
+  }
+
   for (let property of kSerializableDownloadProperties) {
     if (property in aSerializable) {
       download[property] = aSerializable[property];
     }
   }
 
   return download;
 };
--- a/toolkit/components/jsdownloads/src/DownloadImport.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadImport.jsm
@@ -101,17 +101,17 @@ this.DownloadImport.prototype = {
             let maxBytes = row.getResultByName("maxBytes");
             let mimeType = row.getResultByName("mimeType");
             let preferredApplication = row.getResultByName("preferredApplication");
             let preferredAction = row.getResultByName("preferredAction");
             let entityID = row.getResultByName("entityID");
 
             let autoResume = false;
             try {
-              autoResume = row.getResultByName("autoResume");
+              autoResume = (row.getResultByName("autoResume") == 1);
             } catch (ex) {
               // autoResume wasn't present in schema version 7
             }
 
             if (!source) {
               throw new Error("Attempted to import a row with an empty " +
                               "source column.");
             }
@@ -148,17 +148,17 @@ this.DownloadImport.prototype = {
               target: {
                 path: targetPath,
                 partFilePath: tempPath,
               },
               saver: {
                 type: "copy",
                 entityID: entityID
               },
-              startTime: startTime,
+              startTime: new Date(startTime / 1000),
               totalBytes: maxBytes,
               hasPartialData: !!tempPath,
               tryToKeepPartialData: true,
               launchWhenSucceeded: launchWhenSucceeded,
               contentType: mimeType,
               launcherPath: preferredApplication
             };
 
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -61,41 +61,16 @@ function promiseDownloadMidway(aDownload
   // case the download already reached the expected progress.
   aDownload.onchange = onchange;
   onchange();
 
   return deferred.promise;
 }
 
 /**
- * Waits for a download to finish, in case it has not finished already.
- *
- * @param aDownload
- *        The Download object to wait upon.
- *
- * @return {Promise}
- * @resolves When the download has finished successfully.
- * @rejects JavaScript exception if the download failed.
- */
-function promiseDownloadStopped(aDownload) {
-  if (!aDownload.stopped) {
-    // The download is in progress, wait for the current attempt to finish and
-    // report any errors that may occur.
-    return aDownload.start();
-  }
-
-  if (aDownload.succeeded) {
-    return Promise.resolve();
-  }
-
-  // The download failed or was canceled.
-  return Promise.reject(aDownload.error || new Error("Download canceled."));
-}
-
-/**
  * Creates and starts a new download, configured to keep partial data, and
  * returns only when the first part of "interruptible_resumable.txt" has been
  * saved to disk.  You must call "continueResponses" to allow the interruptible
  * request to continue.
  *
  * This function uses either DownloadCopySaver or DownloadLegacySaver based on
  * the current test run.
  *
@@ -1620,16 +1595,35 @@ add_task(function test_launchWhenSucceed
 add_task(function test_contentType() {
   let download = yield promiseStartDownload(httpUrl("source.txt"));
   yield promiseDownloadStopped(download);
 
   do_check_eq("text/plain", download.contentType);
 });
 
 /**
+ * Tests that the serialization/deserialization of the startTime Date
+ * object works correctly.
+ */
+add_task(function test_toSerializable_startTime()
+{
+  let download1 = yield promiseStartDownload(httpUrl("source.txt"));
+  yield promiseDownloadStopped(download1);
+
+  let serializable = download1.toSerializable();
+  let reserialized = JSON.parse(JSON.stringify(serializable));
+
+  let download2 = yield Downloads.createDownload(reserialized);
+
+  do_check_eq(download1.startTime.constructor.name, "Date");
+  do_check_eq(download2.startTime.constructor.name, "Date");
+  do_check_eq(download1.startTime.toJSON(), download2.startTime.toJSON());
+});
+
+/**
  * This test will call the platform specific operations within
  * DownloadPlatform::DownloadDone. While there is no test to verify the
  * specific behaviours, this at least ensures that there is no error or crash.
  */
 add_task(function test_platform_integration()
 {
   let downloadFiles = [];
   function cleanup() {
--- a/toolkit/components/jsdownloads/test/unit/head.js
+++ b/toolkit/components/jsdownloads/test/unit/head.js
@@ -460,16 +460,41 @@ function promiseStartExternalHelperAppSe
       },
     }, null);
   }.bind(this)).then(null, do_report_unexpected_exception);
 
   return deferred.promise;
 }
 
 /**
+ * Waits for a download to finish, in case it has not finished already.
+ *
+ * @param aDownload
+ *        The Download object to wait upon.
+ *
+ * @return {Promise}
+ * @resolves When the download has finished successfully.
+ * @rejects JavaScript exception if the download failed.
+ */
+function promiseDownloadStopped(aDownload) {
+  if (!aDownload.stopped) {
+    // The download is in progress, wait for the current attempt to finish and
+    // report any errors that may occur.
+    return aDownload.start();
+  }
+
+  if (aDownload.succeeded) {
+    return Promise.resolve();
+  }
+
+  // The download failed or was canceled.
+  return Promise.reject(aDownload.error || new Error("Download canceled."));
+}
+
+/**
  * Returns a new public DownloadList object.
  *
  * @return {Promise}
  * @resolves The newly created DownloadList object.
  * @rejects JavaScript exception.
  */
 function promiseNewDownloadList() {
   // Force the creation of a new public download list.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/jsdownloads/test/unit/test_DownloadImport.js
@@ -0,0 +1,701 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the DownloadImport object.
+ */
+
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+                                  "resource://gre/modules/Sqlite.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadImport",
+                                  "resource://gre/modules/DownloadImport.jsm");
+
+// Importable states
+const DOWNLOAD_NOTSTARTED = -1;
+const DOWNLOAD_DOWNLOADING = 0;
+const DOWNLOAD_PAUSED = 4;
+const DOWNLOAD_QUEUED = 5;
+
+// Non importable states
+const DOWNLOAD_FAILED = 2;
+const DOWNLOAD_CANCELED = 3;
+const DOWNLOAD_BLOCKED_PARENTAL = 6;
+const DOWNLOAD_SCANNING = 7;
+const DOWNLOAD_DIRTY = 8;
+const DOWNLOAD_BLOCKED_POLICY = 9;
+
+// The TEST_DATA_TAINTED const is a version of TEST_DATA_SHORT in which the
+// beginning of the data was changed (with the TEST_DATA_REPLACEMENT value).
+// We use this to test that the entityID is properly imported and the download
+// can be resumed from where it was paused.
+// For simplification purposes, the test requires that TEST_DATA_SHORT and
+// TEST_DATA_TAINTED have the same length.
+const TEST_DATA_REPLACEMENT = "-changed- ";
+const TEST_DATA_TAINTED = TEST_DATA_REPLACEMENT +
+                          TEST_DATA_SHORT.substr(TEST_DATA_REPLACEMENT.length);
+const TEST_DATA_LENGTH = TEST_DATA_SHORT.length;
+
+// The length of the partial file that we'll write to disk as an existing
+// ongoing download.
+const TEST_DATA_PARTIAL_LENGTH = TEST_DATA_REPLACEMENT.length;
+
+// The value of the "maxBytes" column stored in the DB about the downloads.
+// It's intentionally different than TEST_DATA_LENGTH to test that each value
+// is seen when expected.
+const MAXBYTES_IN_DB = TEST_DATA_LENGTH - 10;
+
+let gDownloadsRowToImport;
+let gDownloadsRowNonImportable;
+
+/**
+ * Creates a database with an empty moz_downloads table and leaves an
+ * open connection to it.
+ *
+ * @param aPath
+ *        String containing the path of the database file to be created.
+ * @param aSchemaVersion
+ *        Number with the version of the database schema to set.
+ *
+ * @return {Promise}
+ * @resolves The open connection to the database.
+ * @rejects If an error occurred during the database creation.
+ */
+function promiseEmptyDatabaseConnection({aPath, aSchemaVersion}) {
+  return Task.spawn(function () {
+    let connection = yield Sqlite.openConnection({ path: aPath });
+
+    yield connection.execute("CREATE TABLE moz_downloads ("
+                             + "id INTEGER PRIMARY KEY,"
+                             + "name TEXT,"
+                             + "source TEXT,"
+                             + "target TEXT,"
+                             + "tempPath TEXT,"
+                             + "startTime INTEGER,"
+                             + "endTime INTEGER,"
+                             + "state INTEGER,"
+                             + "referrer TEXT,"
+                             + "entityID TEXT,"
+                             + "currBytes INTEGER NOT NULL DEFAULT 0,"
+                             + "maxBytes INTEGER NOT NULL DEFAULT -1,"
+                             + "mimeType TEXT,"
+                             + "preferredApplication TEXT,"
+                             + "preferredAction INTEGER NOT NULL DEFAULT 0,"
+                             + "autoResume INTEGER NOT NULL DEFAULT 0,"
+                             + "guid TEXT)");
+
+    yield connection.setSchemaVersion(aSchemaVersion);
+
+    throw new Task.Result(connection);
+  });
+}
+
+/**
+ * Inserts a new entry in the database with the given columns' values.
+ *
+ * @param aConnection
+ *        The database connection.
+ * @param aDownloadRow
+ *        An object representing the values for each column of the row
+ *        being inserted.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes.
+ * @rejects If there's an error inserting the row.
+ */
+function promiseInsertRow(aConnection, aDownloadRow) {
+  // We can't use the aDownloadRow obj directly in the execute statement
+  // because the obj bind code in Sqlite.jsm doesn't allow objects
+  // with extra properties beyond those being binded. So we might as well
+  // use an array as it is simpler.
+  let values = [
+    aDownloadRow.source, aDownloadRow.target, aDownloadRow.tempPath,
+    aDownloadRow.startTime.getTime() * 1000, aDownloadRow.state,
+    aDownloadRow.referrer, aDownloadRow.entityID, aDownloadRow.maxBytes,
+    aDownloadRow.mimeType, aDownloadRow.preferredApplication,
+    aDownloadRow.preferredAction, aDownloadRow.autoResume
+  ];
+
+  return aConnection.execute("INSERT INTO moz_downloads ("
+                            + "name, source, target, tempPath, startTime,"
+                            + "endTime, state, referrer, entityID, currBytes,"
+                            + "maxBytes, mimeType, preferredApplication,"
+                            + "preferredAction, autoResume, guid)"
+                            + "VALUES ("
+                            + "'', ?, ?, ?, ?, " //name,
+                            + "0, ?, ?, ?, 0, "  //endTime, currBytes
+                            + " ?, ?, ?, "       //
+                            + " ?, ?, '')",      //and guid are not imported
+                            values);
+}
+
+/**
+ * Retrieves the number of rows in the moz_downloads table of the
+ * database.
+ *
+ * @param aConnection
+ *        The database connection.
+ *
+ * @return {Promise}
+ * @resolves With the number of rows.
+ * @rejects Never.
+ */
+function promiseTableCount(aConnection) {
+  return aConnection.execute("SELECT COUNT(*) FROM moz_downloads")
+                    .then(res => res[0].getResultByName("COUNT(*)"))
+                    .then(null, Cu.reportError);
+}
+
+/**
+ * Briefly opens a network channel to a given URL to retrieve
+ * the entityID of this url, as generated by the network code.
+ *
+ * @param aUrl
+ *        The URL to retrieve the entityID.
+ *
+ * @return {Promise}
+ * @resolves The EntityID of the given URL.
+ * @rejects When there's a problem accessing the URL.
+ */
+function promiseEntityID(aUrl) {
+  let deferred = Promise.defer();
+  let entityID = "";
+  let channel = NetUtil.newChannel(NetUtil.newURI(aUrl));
+
+  channel.asyncOpen({
+    onStartRequest: function (aRequest) {
+      if (aRequest instanceof Ci.nsIResumableChannel) {
+        entityID = aRequest.entityID;
+      }
+      aRequest.cancel(Cr.NS_BINDING_ABORTED);
+    },
+
+    onStopRequest: function (aRequest, aContext, aStatusCode) {
+      if (aStatusCode == Cr.NS_BINDING_ABORTED) {
+        deferred.resolve(entityID);
+      } else {
+        deferred.reject("Unexpected status code received");
+      }
+    },
+
+    onDataAvailable: function () {}
+  }, null);
+
+  return deferred.promise;
+}
+
+/**
+ * Gets a file path to a temporary writeable download target, in the
+ * correct format as expected to be stored in the downloads database,
+ * which is file:///absolute/path/to/file
+ *
+ * @param aLeafName
+ *        A hint leaf name for the file.
+ *
+ * @return String The path to the download target.
+ */
+function getDownloadTarget(aLeafName) {
+  return NetUtil.newURI(getTempFile(aLeafName)).spec;
+}
+
+/**
+ * Generates a temporary partial file to use as an in-progress
+ * download. The file is written to disk with a part of the total expected
+ * download content pre-written.
+ *
+ * @param aLeafName
+ *        A hint leaf name for the file.
+ * @param aTainted
+ *        A boolean value. When true, the partial content of the file
+ *        will be different from the expected content of the original source
+ *        file. See the declaration of TEST_DATA_TAINTED for more information.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes, and returns a string with the path
+ *           to the generated file.
+ * @rejects If there's an error writing the file.
+ */
+function getPartialFile(aLeafName, aTainted = false) {
+  let tempDownload = getTempFile(aLeafName);
+  let partialContent = aTainted
+                     ? TEST_DATA_TAINTED.substr(0, TEST_DATA_PARTIAL_LENGTH)
+                     : TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH);
+
+  return OS.File.writeAtomic(tempDownload.path,
+                             partialContent,
+                             { bytes: TEST_DATA_PARTIAL_LENGTH })
+                .then(() => tempDownload.path);
+}
+
+/**
+ * Generates a Date object to be used as the startTime for the download rows
+ * in the DB. A date that is obviously different from the current time is
+ * generated to make sure this stored data and a `new Date()` can't collide.
+ *
+ * @param aOffset
+ *        A offset from the base generated date is used to differentiate each
+ *        row in the database.
+ *
+ * @return A Date object.
+ */
+function getStartTime(aOffset) {
+  return new Date(1000000 + (aOffset * 10000));
+}
+
+/**
+ * Performs various checks on an imported Download object to make sure
+ * all properties are properly set as expected from the import procedure.
+ *
+ * @param aDownload
+ *        The Download object to be checked.
+ * @param aDownloadRow
+ *        An object that represents a row from the original database table,
+ *        with extra properties describing expected values that are not
+ *        explictly part of the database.
+ *
+ * @return {Promise}
+ * @resolves When the operation completes
+ * @rejects Never
+ */
+function checkDownload(aDownload, aDownloadRow) {
+  return Task.spawn(function() {
+    do_check_eq(aDownload.source.url, aDownloadRow.source);
+    do_check_eq(aDownload.source.referrer, aDownloadRow.referrer);
+
+    do_check_eq(aDownload.target.path,
+                NetUtil.newURI(aDownloadRow.target)
+                       .QueryInterface(Ci.nsIFileURL).file.path);
+
+    do_check_eq(aDownload.target.partFilePath, aDownloadRow.tempPath);
+
+    if (aDownloadRow.expectedResume) {
+      do_check_true(!aDownload.stopped || aDownload.succeeded);
+      yield promiseDownloadStopped(aDownload);
+
+      do_check_true(aDownload.succeeded);
+      do_check_eq(aDownload.progress, 100);
+      // If the download has resumed, a new startTime will be set.
+      // By calling toJSON we're also testing that startTime is a Date object.
+      do_check_neq(aDownload.startTime.toJSON(),
+                   aDownloadRow.startTime.toJSON());
+    } else {
+      do_check_false(aDownload.succeeded);
+      do_check_eq(aDownload.startTime.toJSON(),
+                  aDownloadRow.startTime.toJSON());
+    }
+
+    do_check_eq(aDownload.stopped, true);
+
+    let serializedSaver = aDownload.saver.toSerializable();
+    if (typeof(serializedSaver) == "object") {
+      do_check_eq(serializedSaver.type, "copy");
+    } else {
+      do_check_eq(serializedSaver, "copy");
+    }
+
+    if (aDownloadRow.entityID) {
+      do_check_eq(aDownload.saver.entityID, aDownloadRow.entityID);
+    }
+
+    do_check_eq(aDownload.currentBytes, aDownloadRow.expectedCurrentBytes);
+    do_check_eq(aDownload.totalBytes, aDownloadRow.expectedTotalBytes);
+
+    if (aDownloadRow.expectedContent) {
+      let fileToCheck = aDownloadRow.expectedResume
+                        ? aDownload.target.path
+                        : aDownload.target.partFilePath;
+      yield promiseVerifyContents(fileToCheck, aDownloadRow.expectedContent);
+    }
+
+    do_check_eq(aDownload.contentType, aDownloadRow.expectedContentType);
+    do_check_eq(aDownload.launcherPath, aDownloadRow.preferredApplication);
+
+    do_check_eq(aDownload.launchWhenSucceeded,
+                aDownloadRow.preferredAction != Ci.nsIMIMEInfo.saveToDisk);
+  });
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Preparation tasks
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * be imported by the import procedure.
+ */
+add_task(function prepareDownloadsToImport() {
+
+  let sourceUrl = httpUrl("source.txt");
+  let sourceEntityId = yield promiseEntityID(sourceUrl);
+
+  gDownloadsRowToImport = [
+    // Paused download with autoResume and a partial file. By
+    // setting the correct entityID the download can resume from
+    // where it stopped, and to test that this works properly we
+    // intentionally set different data in the beginning of the
+    // partial file to make sure it was not replaced.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress1.txt"),
+      tempPath: yield getPartialFile("inprogress1.txt.part", true),
+      startTime: getStartTime(1),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer1"),
+      entityID: sourceEntityId,
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType1",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication1",
+      autoResume: 1,
+
+      // Even though the information stored in the DB said
+      // maxBytes was MAXBYTES_IN_DB, the download turned out to be
+      // a different length. Here we make sure the totalBytes property
+      // was correctly set with the actual value. The same consideration
+      // applies to the contentType.
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_TAINTED,
+    },
+
+    // Paused download with autoResume and a partial file,
+    // but missing entityID. This means that the download will
+    // start from beginning, and the entire original content of the
+    // source file should replace the different data that was stored
+    // in the partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress2.txt"),
+      tempPath: yield getPartialFile("inprogress2.txt.part", true),
+      startTime: getStartTime(2),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer2"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType2",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication2",
+      autoResume: 1,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Paused download with no autoResume and a partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress3.txt"),
+      tempPath: yield getPartialFile("inprogress3.txt.part"),
+      startTime: getStartTime(3),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer3"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType3",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication3",
+      autoResume: 0,
+
+      // Since this download has not been resumed, the actual data
+      // about its total size and content type is not known.
+      // Therefore, we're going by the information imported from the DB.
+      expectedCurrentBytes: TEST_DATA_PARTIAL_LENGTH,
+      expectedTotalBytes: MAXBYTES_IN_DB,
+      expectedResume: false,
+      expectedContentType: "mimeType3",
+      expectedContent: TEST_DATA_SHORT.substr(0, TEST_DATA_PARTIAL_LENGTH),
+    },
+
+    // Paused download with autoResume and no partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress4.txt"),
+      tempPath: "",
+      startTime: getStartTime(4),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer4"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication4",
+      autoResume: 1,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Paused download with no autoResume and no partial file.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress5.txt"),
+      tempPath: "",
+      startTime: getStartTime(5),
+      state: DOWNLOAD_PAUSED,
+      referrer: httpUrl("referrer4"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useSystemDefault,
+      preferredApplication: "prerredApplication5",
+      autoResume: 0,
+
+      expectedCurrentBytes: 0,
+      expectedTotalBytes: MAXBYTES_IN_DB,
+      expectedResume: false,
+      expectedContentType: "text/plain",
+    },
+
+    // Queued download with no autoResume and no partial file.
+    // Even though autoResume=0, queued downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress6.txt"),
+      tempPath: "",
+      startTime: getStartTime(6),
+      state: DOWNLOAD_QUEUED,
+      referrer: httpUrl("referrer6"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication6",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Notstarted download with no autoResume and no partial file.
+    // Even though autoResume=0, notstarted downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress7.txt"),
+      tempPath: "",
+      startTime: getStartTime(7),
+      state: DOWNLOAD_NOTSTARTED,
+      referrer: httpUrl("referrer7"),
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.useHelperApp,
+      preferredApplication: "prerredApplication7",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_SHORT
+    },
+
+    // Downloading download with no autoResume and a partial file.
+    // Even though autoResume=0, downloading downloads always autoResume.
+    {
+      source: sourceUrl,
+      target: getDownloadTarget("inprogress8.txt"),
+      tempPath: yield getPartialFile("inprogress8.txt.part", true),
+      startTime: getStartTime(8),
+      state: DOWNLOAD_DOWNLOADING,
+      referrer: httpUrl("referrer8"),
+      entityID: sourceEntityId,
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "text/plain",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication8",
+      autoResume: 0,
+
+      expectedCurrentBytes: TEST_DATA_LENGTH,
+      expectedTotalBytes: TEST_DATA_LENGTH,
+      expectedResume: true,
+      expectedContentType: "text/plain",
+      expectedContent: TEST_DATA_TAINTED
+    },
+  ];
+});
+
+/**
+ * Prepares the list of downloads to be added to the database that should
+ * *not* be imported by the import procedure.
+ */
+add_task(function prepareNonImportableDownloads()
+{
+  gDownloadsRowNonImportable = [
+    // Download with no source (should never happen in normal circumstances).
+    {
+      source: "",
+      target: "nonimportable1.txt",
+      tempPath: "",
+      startTime: getStartTime(1),
+      state: DOWNLOAD_PAUSED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType1",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication1",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_FAILED
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable2.txt",
+      tempPath: "",
+      startTime: getStartTime(2),
+      state: DOWNLOAD_FAILED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType2",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication2",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_CANCELED
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable3.txt",
+      tempPath: "",
+      startTime: getStartTime(3),
+      state: DOWNLOAD_CANCELED,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType3",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication3",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_BLOCKED_PARENTAL
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable4.txt",
+      tempPath: "",
+      startTime: getStartTime(4),
+      state: DOWNLOAD_BLOCKED_PARENTAL,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType4",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication4",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_SCANNING
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable5.txt",
+      tempPath: "",
+      startTime: getStartTime(5),
+      state: DOWNLOAD_SCANNING,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType5",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication5",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_DIRTY
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable6.txt",
+      tempPath: "",
+      startTime: getStartTime(6),
+      state: DOWNLOAD_DIRTY,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType6",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication6",
+      autoResume: 1
+    },
+
+    // state = DOWNLOAD_BLOCKED_POLICY
+    {
+      source: httpUrl("source.txt"),
+      target: "nonimportable7.txt",
+      tempPath: "",
+      startTime: getStartTime(7),
+      state: DOWNLOAD_BLOCKED_POLICY,
+      referrer: "",
+      entityID: "",
+      maxBytes: MAXBYTES_IN_DB,
+      mimeType: "mimeType7",
+      preferredAction: Ci.nsIMIMEInfo.saveToDisk,
+      preferredApplication: "prerredApplication7",
+      autoResume: 1
+    },
+  ];
+});
+
+////////////////////////////////////////////////////////////////////////////////
+//// Test
+
+/**
+ * Creates a temporary Sqlite database with download data and perform an
+ * import of that data to the new Downloads API to verify that the import
+ * worked correctly.
+ */
+add_task(function test_downloadImport()
+{
+  let connection = null;
+  let downloadsSqlite = getTempFile("downloads.sqlite").path;
+
+  try {
+    // Set up the database.
+    connection = yield promiseEmptyDatabaseConnection({
+      aPath: downloadsSqlite,
+      aSchemaVersion: 9
+    });
+
+    // Insert both the importable and non-importable
+    // downloads together.
+    for (let downloadRow of gDownloadsRowToImport) {
+      yield promiseInsertRow(connection, downloadRow);
+    }
+
+    for (let downloadRow of gDownloadsRowNonImportable) {
+      yield promiseInsertRow(connection, downloadRow);
+    }
+
+    // Check that every item was inserted.
+    do_check_eq((yield promiseTableCount(connection)),
+                gDownloadsRowToImport.length +
+                gDownloadsRowNonImportable.length);
+  } finally {
+    // Close the connection so that DownloadImport can open it.
+    yield connection.close();
+  }
+
+  // Import items.
+  let list = yield promiseNewDownloadList();
+  yield new DownloadImport(list, downloadsSqlite).import();
+  let items = yield list.getAll();
+
+  do_check_eq(items.length, gDownloadsRowToImport.length);
+
+  for (let i = 0; i < gDownloadsRowToImport.length; i++) {
+    yield checkDownload(items[i], gDownloadsRowToImport[i]);
+  }
+})
--- a/toolkit/components/jsdownloads/test/unit/xpcshell.ini
+++ b/toolkit/components/jsdownloads/test/unit/xpcshell.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 head = head.js
 tail = tail.js
 
 [test_DownloadCore.js]
+[test_DownloadImport.js]
 [test_DownloadIntegration.js]
 [test_DownloadLegacy.js]
 [test_DownloadList.js]
 [test_Downloads.js]
 [test_DownloadStore.js]
--- a/toolkit/components/social/test/browser/Makefile.in
+++ b/toolkit/components/social/test/browser/Makefile.in
@@ -6,16 +6,17 @@ ifdef MOZ_SOCIAL
 # social is turned off for android
 
 MOCHITEST_BROWSER_FILES = \
   head.js \
   data.json \
   echo.sjs \
   worker_xhr.js \
   browser_frameworker.js \
+  browser_frameworker_sandbox.js \
   worker_relative.js \
   relative_import.js \
   browser_workerAPI.js \
   worker_social.js \
   browser_SocialProvider.js \
   browser_notifications.js \
   worker_eventsource.js \
   eventsource.resource \
--- a/toolkit/components/social/test/browser/browser_frameworker.js
+++ b/toolkit/components/social/test/browser/browser_frameworker.js
@@ -1,12 +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/. */
 
+// This file tests message ports and semantics of the frameworker which aren't
+// directly related to the sandbox.  See also browser_frameworker_sandbox.js.
+
 function makeWorkerUrl(runner) {
   let prefix =  "http://example.com/browser/toolkit/components/social/test/browser/echo.sjs?";
   if (typeof runner == "function") {
     runner = "var run=" + runner.toSource() + ";run();";
   }
   return prefix + encodeURI(runner);
 }
 
@@ -131,273 +134,16 @@ let tests = {
         ok(e.data.data.somextrafunction, "have someextrafunction")
         worker.terminate();
         cbnext();
       }
     }
     worker.port.postMessage({topic: "hello", data: [1,2,3]});
   },
 
-  testArrayUsingBuffer: function(cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        port.onmessage = function(e) {
-          if (e.data.topic == "go") {
-            let buffer = new ArrayBuffer(10);
-            // this one has always worked in the past, but worth checking anyway...
-            if (new Uint8Array(buffer).length != 10) {
-              port.postMessage({topic: "result", reason: "first length was not 10"});
-              return;
-            }
-            let reader = new FileReader();
-            reader.onload = function(event) {
-              if (new Uint8Array(buffer).length != 10) {
-                port.postMessage({topic: "result", reason: "length in onload handler was not 10"});
-                return;
-              }
-              // all seems good!
-              port.postMessage({topic: "result", reason: "ok"});
-            }
-            let blob = new Blob([buffer], {type: "binary"});
-            reader.readAsArrayBuffer(blob);
-          }
-        }
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testArray");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "result") {
-        is(e.data.reason, "ok", "check the array worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-    worker.port.postMessage({topic: "go"});
-  },
-
-  testArrayUsingReader: function(cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        port.onmessage = function(e) {
-          if (e.data.topic == "go") {
-            let buffer = new ArrayBuffer(10);
-            let reader = new FileReader();
-            reader.onload = function(event) {
-              try {
-                if (new Uint8Array(reader.result).length != 10) {
-                  port.postMessage({topic: "result", reason: "length in onload handler was not 10"});
-                  return;
-                }
-                // all seems good!
-                port.postMessage({topic: "result", reason: "ok"});
-              } catch (ex) {
-                port.postMessage({topic: "result", reason: ex.toString()});
-              }
-            }
-            let blob = new Blob([buffer], {type: "binary"});
-            reader.readAsArrayBuffer(blob);
-          }
-        }
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testArray");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "result") {
-        is(e.data.reason, "ok", "check the array worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-    worker.port.postMessage({topic: "go"});
-  },
-
-  testXHR: function(cbnext) {
-    // NOTE: this url MUST be in the same origin as worker_xhr.js fetches from!
-    let url = "https://example.com/browser/toolkit/components/social/test/browser/worker_xhr.js";
-    let worker = getFrameWorkerHandle(url, undefined, "testXHR");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check the xhr test worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
-  testLocalStorage: function(cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        try {
-          localStorage.setItem("foo", "1");
-        } catch(e) {
-          port.postMessage({topic: "done", result: "FAILED to set localStorage, " + e.toString() });
-          return;
-        }
-
-        var ok;
-        try {
-          ok = localStorage["foo"] == 1;
-        } catch (e) {
-          port.postMessage({topic: "done", result: "FAILED to read localStorage, " + e.toString() });
-          return;
-        }
-        port.postMessage({topic: "done", result: "ok"});
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testLocalStorage", null, true);
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check the localStorage test worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
-  testNoLocalStorage: function(cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        try {
-          localStorage.setItem("foo", "1");
-        } catch(e) {
-          port.postMessage({topic: "done", result: "ok"});
-          return;
-        }
-
-        port.postMessage({topic: "done", result: "FAILED because localStorage was exposed" });
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testNoLocalStorage");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check that retrieving localStorage fails by default");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
-  testBase64: function (cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        var ok = false;
-        try {
-          ok = btoa("1234") == "MTIzNA==";
-        } catch(e) {
-          port.postMessage({topic: "done", result: "FAILED to call btoa, " + e.toString() });
-          return;
-        }
-        if (!ok) {
-          port.postMessage({topic: "done", result: "FAILED calling btoa"});
-          return;
-        }
-
-        try {
-          ok = atob("NDMyMQ==") == "4321";
-        } catch (e) {
-          port.postMessage({topic: "done", result: "FAILED to call atob, " + e.toString() });
-          return;
-        }
-        if (!ok) {
-          port.postMessage({topic: "done", result: "FAILED calling atob"});
-          return;
-        }
-
-        port.postMessage({topic: "done", result: "ok"});
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testBase64");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check the atob/btoa test worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
-  testTimeouts: function (cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-
-        var timeout;
-        try {
-          timeout = setTimeout(function () {
-            port.postMessage({topic: "done", result: "FAILED cancelled timeout was called"});
-          }, 100);
-        } catch (ex) {
-          port.postMessage({topic: "done", result: "FAILED calling setTimeout: " + ex});
-          return;
-        }
-
-        try {
-          clearTimeout(timeout);
-        } catch (ex) {
-          port.postMessage({topic: "done", result: "FAILED calling clearTimeout: " + ex});
-          return;
-        }
-
-        var counter = 0;
-        try {
-          timeout = setInterval(function () {
-            if (++counter == 2) {
-              clearInterval(timeout);
-              setTimeout(function () {
-                port.postMessage({topic: "done", result: "ok"});
-                return;
-              }, 0);
-            }
-          }, 100);
-        } catch (ex) {
-          port.postMessage({topic: "done", result: "FAILED calling setInterval: " + ex});
-          return;
-        }
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testTimeouts");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check that timeouts worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
-  testWebSocket: function (cbnext) {
-    let run = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-
-        try {
-          var exampleSocket = new WebSocket("ws://mochi.test:8888/socketserver");
-        } catch (e) {
-          port.postMessage({topic: "done", result: "FAILED calling WebSocket constructor: " + e});
-          return;
-        }
-
-        port.postMessage({topic: "done", result: "ok"});
-      }
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testWebSocket");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "done") {
-        is(e.data.result, "ok", "check that websockets worked");
-        worker.terminate();
-        cbnext();
-      }
-    }
-  },
-
   testSameOriginImport: function(cbnext) {
     let run = function() {
       onconnect = function(e) {
         let port = e.ports[0];
         port.onmessage = function(e) {
           if (e.data.topic == "ping") {
             try {
               importScripts("http://foo.bar/error");
@@ -417,17 +163,16 @@ let tests = {
         isnot(e.data.data, null, "check same-origin applied to importScripts");
         worker.terminate();
         cbnext();
       }
     }
     worker.port.postMessage({topic: "ping"})
   },
 
-
   testRelativeImport: function(cbnext) {
     let url = "https://example.com/browser/toolkit/components/social/test/browser/worker_relative.js";
     let worker = getFrameWorkerHandle(url, undefined, "testSameOriginImport");
     worker.port.onmessage = function(e) {
       if (e.data.topic == "done") {
         is(e.data.result, "ok", "check relative url in importScripts");
         worker.terminate();
         cbnext();
@@ -628,73 +373,9 @@ let tests = {
         is(e.data.result.ping, 1, "the worker got the ping");
         is(e.data.result.close, 1, "the worker got 1 close message");
         worker.terminate();
         cbnext();
       }
     }
     worker.port.postMessage({topic: "get-ready"});
   },
-
-  testEventSource: function(cbnext) {
-    let worker = getFrameWorkerHandle("https://example.com/browser/toolkit/components/social/test/browser/worker_eventsource.js", undefined, "testEventSource");
-    worker.port.onmessage = function(e) {
-      let m = e.data;
-      if (m.topic == "eventSourceTest") {
-        if (m.result.ok != undefined)
-          ok(m.result.ok, e.data.result.msg);
-        if (m.result.is != undefined)
-          is(m.result.is, m.result.match, m.result.msg);
-        if (m.result.info != undefined)
-          info(m.result.info);
-      } else if (e.data.topic == "pong") {
-        worker.terminate();
-        cbnext();
-      }
-    }
-    worker.port.postMessage({topic: "ping"})
-  },
-
-
-  testIndexedDB: function(cbnext) {
-    let worker = getFrameWorkerHandle("https://example.com/browser/toolkit/components/social/test/browser/worker_social.js", undefined, "testIndexedDB");
-    worker.port.onmessage = function(e) {
-      let m = e.data;
-      if (m.topic == "social.indexeddb-result") {
-        is(m.data.result, "ok", "created indexeddb");
-        worker.terminate();
-        cbnext();
-      }
-    }
-    worker.port.postMessage({topic: "test-indexeddb-create"})
-  },
-
-  testSubworker: function(cbnext) {
-    // the main "frameworker"...
-    let mainworker = function() {
-      onconnect = function(e) {
-        let port = e.ports[0];
-        port.onmessage = function(e) {
-          if (e.data.topic == "go") {
-            let suburl = e.data.data;
-            let worker = new Worker(suburl);
-            worker.onmessage = function(sube) {
-              port.postMessage({topic: "sub-message", data: sube.data});
-            }
-          }
-        }
-      }
-    }
-
-    // The "subworker" that is actually a real, bona-fide worker.
-    let subworker = function() {
-      postMessage("hello");
-    }
-    let worker = getFrameWorkerHandle(makeWorkerUrl(mainworker), undefined, "testSubWorker");
-    worker.port.onmessage = function(e) {
-      if (e.data.topic == "sub-message" && e.data.data == "hello") {
-        worker.terminate();
-        cbnext();
-      }
-    }
-    worker.port.postMessage({topic: "go", data: makeWorkerUrl(subworker)});
-  }
 }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/social/test/browser/browser_frameworker_sandbox.js
@@ -0,0 +1,347 @@
+/* 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/. */
+
+// This file tests features made available to the frameworker via the sandbox.
+// For other frameworker tests, see browser_frameworker.js
+
+function makeWorkerUrl(runner) {
+  let prefix =  "http://example.com/browser/toolkit/components/social/test/browser/echo.sjs?";
+  if (typeof runner == "function") {
+    runner = "var run=" + runner.toSource() + ";run();";
+  }
+  return prefix + encodeURI(runner);
+}
+
+var getFrameWorkerHandle;
+function test() {
+  waitForExplicitFinish();
+
+  let scope = {};
+  Cu.import("resource://gre/modules/FrameWorker.jsm", scope);
+  getFrameWorkerHandle = scope.getFrameWorkerHandle;
+
+  runTests(tests);
+}
+
+let tests = {
+  testArrayUsingBuffer: function(cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        port.onmessage = function(e) {
+          if (e.data.topic == "go") {
+            let buffer = new ArrayBuffer(10);
+            // this one has always worked in the past, but worth checking anyway...
+            if (new Uint8Array(buffer).length != 10) {
+              port.postMessage({topic: "result", reason: "first length was not 10"});
+              return;
+            }
+            let reader = new FileReader();
+            reader.onload = function(event) {
+              if (new Uint8Array(buffer).length != 10) {
+                port.postMessage({topic: "result", reason: "length in onload handler was not 10"});
+                return;
+              }
+              // all seems good!
+              port.postMessage({topic: "result", reason: "ok"});
+            }
+            let blob = new Blob([buffer], {type: "binary"});
+            reader.readAsArrayBuffer(blob);
+          }
+        }
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testArray");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "result") {
+        is(e.data.reason, "ok", "check the array worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "go"});
+  },
+
+  testArrayUsingReader: function(cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        port.onmessage = function(e) {
+          if (e.data.topic == "go") {
+            let buffer = new ArrayBuffer(10);
+            let reader = new FileReader();
+            reader.onload = function(event) {
+              try {
+                if (new Uint8Array(reader.result).length != 10) {
+                  port.postMessage({topic: "result", reason: "length in onload handler was not 10"});
+                  return;
+                }
+                // all seems good!
+                port.postMessage({topic: "result", reason: "ok"});
+              } catch (ex) {
+                port.postMessage({topic: "result", reason: ex.toString()});
+              }
+            }
+            let blob = new Blob([buffer], {type: "binary"});
+            reader.readAsArrayBuffer(blob);
+          }
+        }
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testArray");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "result") {
+        is(e.data.reason, "ok", "check the array worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "go"});
+  },
+
+  testXHR: function(cbnext) {
+    // NOTE: this url MUST be in the same origin as worker_xhr.js fetches from!
+    let url = "https://example.com/browser/toolkit/components/social/test/browser/worker_xhr.js";
+    let worker = getFrameWorkerHandle(url, undefined, "testXHR");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check the xhr test worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testLocalStorage: function(cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        try {
+          localStorage.setItem("foo", "1");
+        } catch(e) {
+          port.postMessage({topic: "done", result: "FAILED to set localStorage, " + e.toString() });
+          return;
+        }
+
+        var ok;
+        try {
+          ok = localStorage["foo"] == 1;
+        } catch (e) {
+          port.postMessage({topic: "done", result: "FAILED to read localStorage, " + e.toString() });
+          return;
+        }
+        port.postMessage({topic: "done", result: "ok"});
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testLocalStorage", null, true);
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check the localStorage test worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testNoLocalStorage: function(cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        try {
+          localStorage.setItem("foo", "1");
+        } catch(e) {
+          port.postMessage({topic: "done", result: "ok"});
+          return;
+        }
+
+        port.postMessage({topic: "done", result: "FAILED because localStorage was exposed" });
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testNoLocalStorage");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check that retrieving localStorage fails by default");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testBase64: function (cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        var ok = false;
+        try {
+          ok = btoa("1234") == "MTIzNA==";
+        } catch(e) {
+          port.postMessage({topic: "done", result: "FAILED to call btoa, " + e.toString() });
+          return;
+        }
+        if (!ok) {
+          port.postMessage({topic: "done", result: "FAILED calling btoa"});
+          return;
+        }
+
+        try {
+          ok = atob("NDMyMQ==") == "4321";
+        } catch (e) {
+          port.postMessage({topic: "done", result: "FAILED to call atob, " + e.toString() });
+          return;
+        }
+        if (!ok) {
+          port.postMessage({topic: "done", result: "FAILED calling atob"});
+          return;
+        }
+
+        port.postMessage({topic: "done", result: "ok"});
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testBase64");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check the atob/btoa test worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testTimeouts: function (cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+
+        var timeout;
+        try {
+          timeout = setTimeout(function () {
+            port.postMessage({topic: "done", result: "FAILED cancelled timeout was called"});
+          }, 100);
+        } catch (ex) {
+          port.postMessage({topic: "done", result: "FAILED calling setTimeout: " + ex});
+          return;
+        }
+
+        try {
+          clearTimeout(timeout);
+        } catch (ex) {
+          port.postMessage({topic: "done", result: "FAILED calling clearTimeout: " + ex});
+          return;
+        }
+
+        var counter = 0;
+        try {
+          timeout = setInterval(function () {
+            if (++counter == 2) {
+              clearInterval(timeout);
+              setTimeout(function () {
+                port.postMessage({topic: "done", result: "ok"});
+                return;
+              }, 0);
+            }
+          }, 100);
+        } catch (ex) {
+          port.postMessage({topic: "done", result: "FAILED calling setInterval: " + ex});
+          return;
+        }
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testTimeouts");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check that timeouts worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testWebSocket: function (cbnext) {
+    let run = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+
+        try {
+          var exampleSocket = new WebSocket("ws://mochi.test:8888/socketserver");
+        } catch (e) {
+          port.postMessage({topic: "done", result: "FAILED calling WebSocket constructor: " + e});
+          return;
+        }
+
+        port.postMessage({topic: "done", result: "ok"});
+      }
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(run), undefined, "testWebSocket");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "done") {
+        is(e.data.result, "ok", "check that websockets worked");
+        worker.terminate();
+        cbnext();
+      }
+    }
+  },
+
+  testEventSource: function(cbnext) {
+    let worker = getFrameWorkerHandle("https://example.com/browser/toolkit/components/social/test/browser/worker_eventsource.js", undefined, "testEventSource");
+    worker.port.onmessage = function(e) {
+      let m = e.data;
+      if (m.topic == "eventSourceTest") {
+        if (m.result.ok != undefined)
+          ok(m.result.ok, e.data.result.msg);
+        if (m.result.is != undefined)
+          is(m.result.is, m.result.match, m.result.msg);
+        if (m.result.info != undefined)
+          info(m.result.info);
+      } else if (e.data.topic == "pong") {
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "ping"})
+  },
+
+  testIndexedDB: function(cbnext) {
+    let worker = getFrameWorkerHandle("https://example.com/browser/toolkit/components/social/test/browser/worker_social.js", undefined, "testIndexedDB");
+    worker.port.onmessage = function(e) {
+      let m = e.data;
+      if (m.topic == "social.indexeddb-result") {
+        is(m.data.result, "ok", "created indexeddb");
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "test-indexeddb-create"})
+  },
+
+  testSubworker: function(cbnext) {
+    // the main "frameworker"...
+    let mainworker = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        port.onmessage = function(e) {
+          if (e.data.topic == "go") {
+            let suburl = e.data.data;
+            let worker = new Worker(suburl);
+            worker.onmessage = function(sube) {
+              port.postMessage({topic: "sub-message", data: sube.data});
+            }
+          }
+        }
+      }
+    }
+
+    // The "subworker" that is actually a real, bona-fide worker.
+    let subworker = function() {
+      postMessage("hello");
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(mainworker), undefined, "testSubWorker");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "sub-message" && e.data.data == "hello") {
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "go", data: makeWorkerUrl(subworker)});
+  }
+}
--- a/toolkit/components/startup/nsAppStartup.cpp
+++ b/toolkit/components/startup/nsAppStartup.cpp
@@ -358,16 +358,19 @@ nsAppStartup::Quit(uint32_t aMode)
     mozilla::RecordShutdownStartTimeStamp();
     mShuttingDown = true;
     if (!mRestart) {
       mRestart = (aMode & eRestart) != 0;
       gRestartMode = (aMode & 0xF0);
     }
 
     if (mRestart) {
+      // Mark the next startup as a restart.
+      PR_SetEnv("MOZ_APP_RESTART=1");
+
       /* Firefox-restarts reuse the process so regular process start-time isn't
          a useful indicator of startup time anymore. */
       TimeStamp::RecordProcessRestart();
     }
 
     obsService = mozilla::services::GetObserverService();
 
     if (!mAttemptingQuit) {
@@ -518,16 +521,29 @@ nsAppStartup::GetShuttingDown(bool *aRes
 NS_IMETHODIMP
 nsAppStartup::GetRestarting(bool *aResult)
 {
   *aResult = mRestart;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsAppStartup::GetWasRestarted(bool *aResult)
+{
+  char *mozAppRestart = PR_GetEnv("MOZ_APP_RESTART");
+
+  /* When calling PR_SetEnv() with an empty value the existing variable may
+   * be unset or set to the empty string depending on the underlying platform
+   * thus we have to check if the variable is present and not empty. */
+  *aResult = mozAppRestart && (strcmp(mozAppRestart, "") != 0);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsAppStartup::SetInterrupted(bool aInterrupted)
 {
   mInterrupted = aInterrupted;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAppStartup::GetInterrupted(bool *aInterrupted)
--- 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(380618f8-479a-435b-b58e-7398ab937531)]
+[scriptable, uuid(744d6ec0-115f-11e3-9c94-68fd99890b3c)]
 interface nsIAppStartup : nsISupports
 {
     /**
      * Create the hidden window.
      */
     void createHiddenWindow();
 
     /**
@@ -135,16 +135,22 @@ interface nsIAppStartup : nsISupports
      */
     readonly attribute boolean shuttingDown;
 
     /**
      * 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*).
+     */
+    readonly attribute boolean wasRestarted;
+
     /** 
      * Returns an object with main, process, firstPaint, sessionRestored properties.
      * Properties may not be available depending on platform or application
      */
     [implicit_jscontext] jsval getStartupInfo();
 
     /**
      * True if startup was interrupted by an interactive prompt.
--- a/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
+++ b/toolkit/components/thumbnails/BackgroundPageThumbs.jsm
@@ -357,44 +357,43 @@ Capture.prototype = {
     this._done(null);
   },
 
   _done: function (data) {
     // Note that _done will be called only once, by either receiveMessage or
     // notify, since it calls destroy, which cancels the timeout timer and
     // removes the didCapture message listener.
 
-    this.captureCallback(this);
-    this.destroy();
-
     if (data && data.telemetry) {
       // Telemetry is currently disabled in the content process (bug 680508).
       for (let id in data.telemetry) {
         tel(id, data.telemetry[id]);
       }
     }
 
-    let callOnDones = function callOnDonesFn() {
+    let done = () => {
+      this.captureCallback(this);
+      this.destroy();
       for (let callback of this.doneCallbacks) {
         try {
           callback.call(this.options, this.url);
         }
         catch (err) {
           Cu.reportError(err);
         }
       }
-    }.bind(this);
+    };
 
     if (!data) {
-      callOnDones();
+      done();
       return;
     }
 
     PageThumbs._store(this.url, data.finalURL, data.imageData, data.wasErrorResponse)
-              .then(callOnDones);
+              .then(done, done);
   },
 };
 
 Capture.nextID = 0;
 
 /**
  * Adds a value to one of this module's telemetry histograms.
  *
--- a/toolkit/content/tests/chrome/Makefile.in
+++ b/toolkit/content/tests/chrome/Makefile.in
@@ -164,8 +164,12 @@ else
 MOCHITEST_CHROME_FILES += test_autocomplete_mac_caret.xul
 endif
 
 ifeq ($(MOZ_WIDGET_TOOLKIT),windows)
 MOCHITEST_CHROME_FILES += $(filter disabled-temporarily, test_cursorsnap.xul) \
 		window_cursorsnap_dialog.xul \
 		window_cursorsnap_wizard.xul
 endif
+
+MOCHITEST_CHROME_FILES += test_about_networking.html \
+		file_about_networking_wsh.py
+
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/file_about_networking_wsh.py
@@ -0,0 +1,9 @@
+from mod_pywebsocket import msgutil
+
+def web_socket_do_extra_handshake(request):
+	pass
+
+def web_socket_transfer_data(request):
+	while not request.client_terminated:
+		msgutil.receive_message(request)
+
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/chrome/test_about_networking.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=912103
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript">
+
+  SimpleTest.waitForExplicitFinish();
+
+  function runTest() {
+    const Cc = Components.classes;
+    const Ci = Components.interfaces;
+
+    var dashboard = Cc['@mozilla.org/network/dashboard;1']
+                      .getService(Ci.nsIDashboard);
+    dashboard.enableLogging = true;
+
+    var wsURI = "ws://mochi.test:8888/chrome/toolkit/content/tests/chrome/file_about_networking";
+    var websocket = new WebSocket(wsURI);
+
+    websocket.addEventListener("open", function() {
+      dashboard.requestWebsocketConnections(function(data) {
+        isnot(data.hostport.indexOf("mochi.test:8888"), -1,
+              "tested websocket entry not found");
+        websocket.close();
+        SimpleTest.finish();
+      });
+    });
+  }
+
+  window.addEventListener("DOMContentLoaded", function run() {
+    window.removeEventListener("DOMContentLoaded", run);
+    runTest();
+  });
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=912103">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/devtools/Loader.jsm
+++ b/toolkit/devtools/Loader.jsm
@@ -50,16 +50,17 @@ var BuiltinProvider = {
         "source-map": SourceMap,
       },
       paths: {
         "": "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/client": "resource://gre/modules/devtools/client",
 
         "escodegen/escodegen": "resource://gre/modules/devtools/escodegen/escodegen",
         "escodegen/package.json": "resource://gre/modules/devtools/escodegen/package.json",
         "estraverse": "resource://gre/modules/devtools/escodegen/estraverse",
 
         // Allow access to xpcshell test items from the loader.
@@ -90,28 +91,30 @@ var SrcdirProvider = {
     let srcdir = Services.prefs.getComplexValue("devtools.loader.srcdir",
                                                 Ci.nsISupportsString);
     srcdir = OS.Path.normalize(srcdir.data.trim());
     let devtoolsDir = OS.Path.join(srcdir, "browser", "devtools");
     let devtoolsURI = this.fileURI(devtoolsDir);
     let toolkitURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools"));
     let serverURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "server"));
     let webconsoleURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "webconsole"));
+    let appActorURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "apps", "app-actor-front.js"));
     let cssLogicURI = this.fileURI(OS.Path.join(toolkitURI, "styleinspector", "css-logic"));
     let clientURI = this.fileURI(OS.Path.join(srcdir, "toolkit", "devtools", "client"));
     let mainURI = this.fileURI(OS.Path.join(srcdir, "browser", "devtools", "main.js"));
     this.loader = new loader.Loader({
       modules: {
         "toolkit/loader": loader,
         "source-map": SourceMap,
       },
       paths: {
         "": "resource://gre/modules/commonjs/",
         "devtools/server": serverURI,
         "devtools/toolkit/webconsole": webconsoleURI,
+        "devtools/app-actor-front": appActorURI,
         "devtools/client": clientURI,
         "devtools": devtoolsURI,
         "devtools/styleinspector/css-logic": cssLogicURI,
         "main": mainURI
       },
       globals: loaderGlobals
     });
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/apps/Simulator.jsm
@@ -0,0 +1,33 @@
+/* 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";
+
+Components.utils.import("resource:///modules/devtools/shared/event-emitter.js");
+
+const EXPORTED_SYMBOLS = ["Simulator"];
+
+const Simulator = {
+  _simulators: {},
+
+  register: function (version, simulator) {
+    this._simulators[version] = simulator;
+    this.emit("register");
+  },
+
+  unregister: function (version) {
+    delete this._simulators[version];
+    this.emit("unregister");
+  },
+
+  availableVersions: function () {
+    return Object.keys(this._simulators).sort();
+  },
+
+  getByVersion: function (version) {
+    return this._simulators[version];
+  }
+};
+
+EventEmitter.decorate(Simulator);
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/apps/app-actor-front.js
@@ -0,0 +1,216 @@
+const {Ci, Cc, Cu, Cr} = require("chrome");
+Cu.import("resource://gre/modules/osfile.jsm");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
+const promise = require("sdk/core/promise");
+
+// XXX: bug 912476 make this module a real protocol.js front
+// by converting webapps actor to protocol.js
+
+const PR_USEC_PER_MSEC = 1000;
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+const CHUNK_SIZE = 10000;
+
+function addDirToZip(writer, dir, basePath) {
+  let files = dir.directoryEntries;
+
+  while (files.hasMoreElements()) {
+    let file = files.getNext().QueryInterface(Ci.nsIFile);
+
+    if (file.isHidden() ||
+        file.isSymlink() ||
+        file.isSpecial() ||
+        file.equals(writer.file))
+    {
+      continue;
+    }
+
+    if (file.isDirectory()) {
+      writer.addEntryDirectory(basePath + file.leafName + "/",
+                               file.lastModifiedTime * PR_USEC_PER_MSEC,
+                               true);
+      addDirToZip(writer, file, basePath + file.leafName + "/");
+    } else {
+      writer.addEntryFile(basePath + file.leafName,
+                          Ci.nsIZipWriter.COMPRESSION_DEFAULT,
+                          file,
+                          true);
+    }
+  }
+}
+
+/**
+ * Convert an XPConnect result code to its name and message.
+ * We have to extract them from an exception per bug 637307 comment 5.
+ */
+function getResultTest(code) {
+  let regexp =
+    /^\[Exception... "(.*)"  nsresult: "0x[0-9a-fA-F]* \((.*)\)"  location: ".*"  data: .*\]$/;
+  let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
+           createInstance(Ci.nsIXPCException);
+  ex.initialize(null, code, null, null, null, null);
+  let [, message, name] = regexp.exec(ex.toString());
+  return { name: name, message: message };
+}
+
+function zipDirectory(zipFile, dirToArchive) {
+  let deferred = promise.defer();
+  let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
+  writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
+
+  this.addDirToZip(writer, dirToArchive, "");
+
+  writer.processQueue({
+    onStartRequest: function onStartRequest(request, context) {},
+    onStopRequest: (request, context, status) => {
+      if (status == Cr.NS_OK) {
+        writer.close();
+        deferred.resolve(zipFile);
+      }
+      else {
+        let { name, message } = getResultText(status);
+        deferred.reject(name + ": " + message);
+      }
+    }
+  }, null);
+
+  return deferred.promise;
+}
+
+function uploadPackage(client, webappsActor, packageFile) {
+  let deferred = promise.defer();
+
+  let request = {
+    to: webappsActor,
+    type: "uploadPackage"
+  };
+  client.request(request, (res) => {
+    openFile(res.actor);
+  });
+
+  function openFile(actor) {
+    OS.File.open(packageFile.path)
+      .then(function (file) {
+        uploadChunk(actor, file);
+      });
+  }
+  function uploadChunk(actor, file) {
+    file.read(CHUNK_SIZE)
+        .then(function (bytes) {
+          // To work around the fact that JSON.stringify translates the typed
+          // array to object, we are encoding the typed array here into a string
+          let chunk = String.fromCharCode.apply(null, bytes);
+
+          let request = {
+            to: actor,
+            type: "chunk",
+            chunk: chunk
+          };
+          client.request(request, (res) => {
+            if (bytes.length == CHUNK_SIZE) {
+              uploadChunk(actor, file);
+            } else {
+              file.close().then(function () {
+                endsUpload(actor);
+              });
+            }
+          });
+        });
+  }
+  function endsUpload(actor) {
+    let request = {
+      to: actor,
+      type: "done"
+    };
+    client.request(request, (res) => {
+      deferred.resolve(actor);
+    });
+  }
+  return deferred.promise;
+}
+
+function removeServerTemporaryFile(client, fileActor) {
+  let request = {
+    to: fileActor,
+    type: "remove"
+  };
+  client.request(request, function (aResponse) {
+    console.error("Failed removing server side temporary package file", aResponse);
+  });
+}
+
+function installPackaged(client, webappsActor, packagePath, appId) {
+  let deferred = promise.defer();
+  let file = FileUtils.File(packagePath);
+  let packagePromise;
+  if (file.isDirectory()) {
+    let tmpZipFile = FileUtils.getDir("TmpD", [], true);
+    tmpZipFile.append("application.zip");
+    tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+    packagePromise = zipDirectory(tmpZipFile, file)
+  } else {
+    packagePromise = promise.resolve(file);
+  }
+  packagePromise.then((zipFile) => {
+    uploadPackage(client, webappsActor, zipFile)
+        .then((fileActor) => {
+          let request = {
+            to: webappsActor,
+            type: "install",
+            appId: appId,
+            upload: fileActor
+          };
+          client.request(request, (res) => {
+            // If the install method immediatly fails,
+            // reject immediatly the installPackaged promise.
+            // Otherwise, wait for webappsEvent for completion
+            if (res.error) {
+              deferred.reject(res);
+            }
+          });
+          client.addOneTimeListener("webappsEvent", function (aState, aType, aPacket) {
+            if ("error" in aType)
+              deferred.reject({error: aType.error, message: aType.message});
+            else
+              deferred.resolve({appId: aType.appId});
+          });
+          // Ensure deleting the temporary package file, but only if that a temporary
+          // package created when we pass a directory as `packagePath`
+          if (zipFile != file)
+            zipFile.remove(false);
+          // In case of success or error, ensure deleting the temporary package file
+          // also created on the device, but only once install request is done
+          deferred.promise.then(removeServerTemporaryFile, removeServerTemporaryFile);
+        });
+  });
+  return deferred.promise;
+}
+exports.installPackaged = installPackaged;
+
+function installHosted(client, webappsActor, appId, metadata, manifest) {
+  let deferred = promise.defer();
+  let request = {
+    to: webappsActor,
+    type: "install",
+    appId: appId,
+    metadata: metadata,
+    manifest: manifest
+  };
+  client.request(request, (res) => {
+    if (res.error) {
+      deferred.reject(res);
+    }
+  });
+  client.addOneTimeListener("webappsEvent", function (aState, aType, aPacket) {
+    if ("error" in aType)
+      deferred.reject({error: aType.error, message: aType.message});
+    else
+      deferred.resolve({appId: aType.appId});
+  });
+  return deferred.promise;
+}
+exports.installHosted = installHosted;
+
--- a/toolkit/devtools/apps/moz.build
+++ b/toolkit/devtools/apps/moz.build
@@ -1,7 +1,13 @@
 # vim: set filetype=python:
 # 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/.
 
 TEST_DIRS += ['tests']
 
+JS_MODULES_PATH = 'modules/devtools'
+
+EXTRA_JS_MODULES += [
+    'Simulator.jsm',
+    'app-actor-front.js'
+]
--- a/toolkit/devtools/apps/tests/unit/head_apps.js
+++ b/toolkit/devtools/apps/tests/unit/head_apps.js
@@ -93,34 +93,36 @@ function setup() {
   originalPrefValue = Services.prefs.getBoolPref("devtools.debugger.enable-content-actors");
   Services.prefs.setBoolPref("devtools.debugger.enable-content-actors", true);
 }
 
 function do_get_webappsdir() {
   var webappsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   webappsDir.append("test_webapps");
   if (!webappsDir.exists())
-    webappsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    webappsDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("755", 8));
 
   var coreAppsDir = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
   coreAppsDir.append("test_coreapps");
   if (!coreAppsDir.exists())
-    coreAppsDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755);
+    coreAppsDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("755", 8));
+  var tmpDir = Services.dirsvc.get("TmpD", Ci.nsILocalFile);
 
   // Register our own provider for the profile directory.
   // It will return our special docshell profile directory.
   var provider = {
     getFile: function(prop, persistent) {
       persistent.value = true;
       if (prop == "webappsDir") {
         return webappsDir.clone();
       }
       else if (prop == "coreAppsDir") {
         return coreAppsDir.clone();
       }
+      return tmpDir.clone();
       throw Cr.NS_ERROR_FAILURE;
     },
     QueryInterface: function(iid) {
       if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
           iid.equals(Ci.nsISupports)) {
         return this;
       }
       throw Cr.NS_ERROR_NO_INTERFACE;
--- a/toolkit/devtools/apps/tests/unit/test_webappsActor.js
+++ b/toolkit/devtools/apps/tests/unit/test_webappsActor.js
@@ -1,12 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://gre/modules/osfile.jsm");
+const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {require} = devtools;
+const {installHosted, installPackaged} = require("devtools/app-actor-front");
 
 let gAppId = "actor-test";
 const APP_ORIGIN = "app://" + gAppId;
 
 add_test(function testLaunchInexistantApp() {
   let request = {type: "launch", manifestURL: "http://foo.com"};
   webappActorRequest(request, function (aResponse) {
     do_check_eq(aResponse.error, "NO_SUCH_APP");
@@ -172,115 +175,45 @@ add_test(function testUninstall() {
   }, "webapps-uninstall", false);
 
   webappActorRequest(request, function (aResponse) {
     do_check_false("error" in aResponse);
   });
 });
 
 add_test(function testFileUploadInstall() {
-  function createUpload() {
-    let request = {
-      type: "uploadPackage"
-    };
-    webappActorRequest(request, function (aResponse) {
-      getPackageContent(aResponse.actor);
-    });
-  }
-  function getPackageContent(uploadActor) {
-    let packageFile = do_get_file("data/app.zip");
-    OS.File.read(packageFile.path)
-      .then(function (bytes) {
-        // To work around the fact that JSON.stringify translates the typed
-        // array to object, we are encoding the typed array here into a string
-        let content = String.fromCharCode.apply(null, bytes);
-        uploadChunk(uploadActor, content);
-      });
-  }
-  function uploadChunk(uploadActor, content) {
-    let request = {
-      to: uploadActor,
-      type: "chunk",
-      chunk: content
-    };
-    gClient.request(request, function (aResponse) {
-      endsUpload(uploadActor);
+  let packageFile = do_get_file("data/app.zip");
+  installPackaged(gClient, gActor, packageFile.path, gAppId)
+    .then(function ({ appId }) {
+      do_check_eq(appId, gAppId);
+      run_next_test();
+    }, function (e) {
+      do_throw("Failed install uploaded packaged app: " + e.error + ": " + e.message);
     });
-  }
-  function endsUpload(uploadActor, content) {
-    let request = {
-      to: uploadActor,
-      type: "done"
-    };
-    gClient.request(request, function (aResponse) {
-      installApp(uploadActor);
-    });
-  }
-  function installApp(uploadActor) {
-    let request = {type: "install", appId: gAppId, upload: uploadActor};
-    webappActorRequest(request, function (aResponse) {
-      do_check_eq(aResponse.appId, gAppId);
-    });
-    gClient.addOneTimeListener("webappsEvent", function listener(aState, aType, aPacket) {
-      do_check_eq(aType.appId, gAppId);
-      if ("error" in aType) {
-        do_print("Error: " + aType.error);
-      }
-      if ("message" in aType) {
-        do_print("Error message: " + aType.message);
-      }
-      do_check_eq("error" in aType, false);
-
-      removeUpload(uploadActor);
-    });
-  }
-  function removeUpload(uploadActor, content) {
-    let request = {
-      to: uploadActor,
-      type: "remove"
-    };
-    gClient.request(request, function (aResponse) {
-      run_next_test();
-    });
-  }
-  createUpload();
 });
 
 add_test(function testInstallHosted() {
   gAppId = "hosted-app";
-  let request = {
-    type: "install",
-    appId: gAppId,
-    manifest: {
-      name: "My hosted app"
-    },
-    metadata: {
-      origin: "http://foo.com",
-      installOrigin: "http://metadata.foo.com",
-      manifestURL: "http://foo.com/metadata/manifest.webapp"
-    }
+  let metadata = {
+    origin: "http://foo.com",
+    installOrigin: "http://metadata.foo.com",
+    manifestURL: "http://foo.com/metadata/manifest.webapp"
+  };
+  let manifest = {
+    name: "My hosted app"
   };
-  webappActorRequest(request, function (aResponse) {
-    do_check_eq(aResponse.appId, gAppId);
-  });
-
-  // The install request is asynchronous and send back an event to tell
-  // if the installation succeed or failed
-  gClient.addOneTimeListener("webappsEvent", function listener(aState, aType, aPacket) {
-    do_check_eq(aType.appId, gAppId);
-    if ("error" in aType) {
-      do_print("Error: " + aType.error);
+  installHosted(gClient, gActor, gAppId, metadata, manifest).then(
+    function ({ appId }) {
+      do_check_eq(appId, gAppId);
+      run_next_test();
+    },
+    function (e) {
+      do_throw("Failed installing hosted app: " + e.error + ": " + e.message);
     }
-    if ("message" in aType) {
-      do_print("Error message: " + aType.message);
-    }
-    do_check_eq("error" in aType, false);
-
-    run_next_test();
-  });
+  );
 });
 
 add_test(function testCheckHostedApp() {
   let request = {type: "getAll"};
   webappActorRequest(request, function (aResponse) {
     do_check_true("apps" in aResponse);
     let apps = aResponse.apps;
     do_check_true(apps.length > 0);
--- a/toolkit/devtools/client/dbg-client.jsm
+++ b/toolkit/devtools/client/dbg-client.jsm
@@ -2005,42 +2005,60 @@ SourceClient.prototype = {
   /**
    * Get a long string grip for this SourceClient's source.
    */
   source: function SC_source(aCallback) {
     let packet = {
       to: this._form.actor,
       type: "source"
     };
-    this._client.request(packet, function (aResponse) {
+    this._client.request(packet, aResponse => {
+      this._onSourceResponse(aResponse, aCallback)
+    });
+  },
+
+  /**
+   * Pretty print this source's text.
+   */
+  prettyPrint: function SC_prettyPrint(aIndent, aCallback) {
+    const packet = {
+      to: this._form.actor,
+      type: "prettyPrint",
+      indent: aIndent
+    };
+    this._client.request(packet, aResponse => {
+      this._onSourceResponse(aResponse, aCallback);
+    });
+  },
+
+  _onSourceResponse: function SC__onSourceResponse(aResponse, aCallback) {
+    if (aResponse.error) {
+      aCallback(aResponse);
+      return;
+    }
+
+    if (typeof aResponse.source === "string") {
+      aCallback(aResponse);
+      return;
+    }
+
+    let { contentType, source } = aResponse;
+    let longString = this._client.activeThread.threadLongString(
+      source);
+    longString.substring(0, longString.length, function (aResponse) {
       if (aResponse.error) {
         aCallback(aResponse);
         return;
       }
 
-      if (typeof aResponse.source === "string") {
-        aCallback(aResponse);
-        return;
-      }
-
-      let { contentType, source } = aResponse;
-      let longString = this._client.activeThread.threadLongString(
-        source);
-      longString.substring(0, longString.length, function (aResponse) {
-        if (aResponse.error) {
-          aCallback(aResponse);
-          return;
-        }
-
-        aCallback({
-          source: aResponse.substring,
-          contentType: contentType
-        });
+      aCallback({
+        source: aResponse.substring,
+        contentType: contentType
       });
-    }.bind(this));
+    });
   }
 };
 
 /**
  * Breakpoint clients are used to remove breakpoints that are no longer used.
  *
  * @param aClient DebuggerClient
  *        The debugger client parent.
--- a/toolkit/devtools/server/actors/childtab.js
+++ b/toolkit/devtools/server/actors/childtab.js
@@ -7,62 +7,62 @@
 /**
  * Tab actor for documents living in a child process.
  *
  * Depends on BrowserTabActor, defined in webbrowser.js actor.
  */
 
 /**
  * Creates a tab actor for handling requests to the single tab, like
- * attaching and detaching. ContentTabActor respects the actor factories
+ * attaching and detaching. ContentAppActor respects the actor factories
  * registered with DebuggerServer.addTabActor.
  *
  * @param connection DebuggerServerConnection
  *        The conection to the client.
  * @param browser browser
  *        The browser instance that contains this tab.
  */
-function ContentTabActor(connection, browser)
+function ContentAppActor(connection, browser)
 {
   BrowserTabActor.call(this, connection, browser);
 }
 
-ContentTabActor.prototype = Object.create(BrowserTabActor.prototype);
+ContentAppActor.prototype = Object.create(BrowserTabActor.prototype);
 
-ContentTabActor.prototype.constructor = ContentTabActor;
+ContentAppActor.prototype.constructor = ContentAppActor;
 
-Object.defineProperty(ContentTabActor.prototype, "title", {
+Object.defineProperty(ContentAppActor.prototype, "title", {
   get: function() {
     return this.browser.title;
   },
   enumerable: true,
   configurable: false
 });
 
-Object.defineProperty(ContentTabActor.prototype, "url", {
+Object.defineProperty(ContentAppActor.prototype, "url", {
   get: function() {
     return this.browser.document.documentURI;
   },
   enumerable: true,
   configurable: false
 });
 
-Object.defineProperty(ContentTabActor.prototype, "window", {
+Object.defineProperty(ContentAppActor.prototype, "window", {
   get: function() {
     return this.browser;
   },
   enumerable: true,
   configurable: false
 });
 
 // Override grip just to rename this._tabActorPool to this._tabActorPool2
 // in order to prevent it to be cleaned on detach.
-// We have to keep tab actors alive as we keep the ContentTabActor
+// We have to keep tab actors alive as we keep the ContentAppActor
 // alive after detach and reuse it for multiple debug sessions.
-ContentTabActor.prototype.grip = function () {
+ContentAppActor.prototype.grip = function () {
   let response = {
     'actor': this.actorID,
     'title': this.title,
     'url': this.url
   };
 
   // Walk over tab actors added by extensions and add them to a new ActorPool.
   let actorPool = new ActorPool(this.conn);
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2293,21 +2293,29 @@ PauseScopedActor.prototype = {
  * A SourceActor provides information about the source of a script.
  *
  * @param aUrl String
  *        The url of the source we are representing.
  * @param aThreadActor ThreadActor
  *        The current thread actor.
  * @param aSourceMap SourceMapConsumer
  *        Optional. The source map that introduced this source, if available.
+ * @param aGeneratedSource String
+ *        Optional, passed in when aSourceMap is also passed in. The generated
+ *        source url that introduced this source.
  */
-function SourceActor(aUrl, aThreadActor, aSourceMap=null) {
+function SourceActor(aUrl, aThreadActor, aSourceMap=null, aGeneratedSource=null) {
   this._threadActor = aThreadActor;
   this._url = aUrl;
   this._sourceMap = aSourceMap;
+  this._generatedSource = aGeneratedSource;
+
+  this.onSource = this.onSource.bind(this);
+  this._invertSourceMap = this._invertSourceMap.bind(this);
+  this._saveMap = this._saveMap.bind(this);
 }
 
 SourceActor.prototype = {
   constructor: SourceActor,
   actorPrefix: "source",
 
   get threadActor() this._threadActor,
   get url() this._url,
@@ -2316,43 +2324,45 @@ SourceActor.prototype = {
     return {
       actor: this.actorID,
       url: this._url,
       isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url)
       // TODO bug 637572: introductionScript
     };
   },
 
-  disconnect: function LSA_disconnect() {
+  disconnect: function SA_disconnect() {
     if (this.registeredPool && this.registeredPool.sourceActors) {
       delete this.registeredPool.sourceActors[this.actorID];
     }
   },
 
+  _getSourceText: function SA__getSourceText() {
+    let sourceContent = null;
+    if (this._sourceMap) {
+      sourceContent = this._sourceMap.sourceContentFor(this._url);
+    }
+
+    if (sourceContent) {
+      return resolve({
+        content: sourceContent
+      });
+    }
+
+    // XXX bug 865252: Don't load from the cache if this is a source mapped
+    // source because we can't guarantee that the cache has the most up to date
+    // content for this source like we can if it isn't source mapped.
+    return fetch(this._url, { loadFromCache: !this._sourceMap });
+  },
+
   /**
    * Handler for the "source" packet.
    */
   onSource: function SA_onSource(aRequest) {
-    let sourceContent = null;
-    if (this._sourceMap) {
-      sourceContent = this._sourceMap.sourceContentFor(this._url);
-    }
-
-    if (sourceContent) {
-      return {
-        from: this.actorID,
-        source: this.threadActor.createValueGrip(
-          sourceContent, this.threadActor.threadLifetimePool)
-      };
-    }
-
-    // XXX bug 865252: Don't load from the cache if this is a source mapped
-    // source because we can't guarantee that the cache has the most up to date
-    // content for this source like we can if it isn't source mapped.
-    return fetch(this._url, { loadFromCache: !this._sourceMap })
+    return this._getSourceText()
       .then(({ content, contentType }) => {
         return {
           from: this.actorID,
           source: this.threadActor.createValueGrip(
             content, this.threadActor.threadLifetimePool),
           contentType: contentType
         };
       })
@@ -2363,16 +2373,119 @@ SourceActor.prototype = {
           "error": "loadSourceError",
           "message": "Could not load the source for " + this._url + ".\n"
             + safeErrorString(aError)
         };
       });
   },
 
   /**
+   * Handler for the "prettyPrint" packet.
+   */
+  onPrettyPrint: function ({ indent }) {
+    return this._getSourceText()
+      .then(this._parseAST)
+      .then(this._generatePrettyCodeAndMap(indent))
+      .then(this._invertSourceMap)
+      .then(this._saveMap)
+      .then(this.onSource)
+      .then(null, error => ({
+        from: this.actorID,
+        error: "prettyPrintError",
+        message: DevToolsUtils.safeErrorString(error)
+      }));
+  },
+
+  /**
+   * Parse the source content into an AST.
+   */
+  _parseAST: function SA__parseAST({ content}) {
+    return Reflect.parse(content);
+  },
+
+  /**
+   * Take the number of spaces to indent and return a function that takes an AST
+   * and generates code and a source map from the ugly code to the pretty code.
+   */
+  _generatePrettyCodeAndMap: function SA__generatePrettyCodeAndMap(aNumSpaces) {
+    let indent = "";
+    for (let i = 0; i < aNumSpaces; i++) {
+      indent += " ";
+    }
+    return aAST => escodegen.generate(aAST, {
+      format: {
+        indent: {
+          style: indent
+        }
+      },
+      sourceMap: this._url,
+      sourceMapWithCode: true
+    });
+  },
+
+  /**
+   * Invert a source map. So if a source map maps from a to b, return a new
+   * source map from b to a. We need to do this because the source map we get
+   * from _generatePrettyCodeAndMap goes the opposite way we want it to for
+   * debugging.
+   */
+  _invertSourceMap: function SA__invertSourceMap({ code, map }) {
+    const smc = new SourceMapConsumer(map.toJSON());
+    const invertedMap = new SourceMapGenerator({
+      file: this._url
+    });
+
+    smc.eachMapping(m => {
+      if (!m.originalLine || !m.originalColumn) {
+        return;
+      }
+      const invertedMapping = {
+        source: m.source,
+        name: m.name,
+        original: {
+          line: m.generatedLine,
+          column: m.generatedColumn
+        },
+        generated: {
+          line: m.originalLine,
+          column: m.originalColumn
+        }
+      };
+      invertedMap.addMapping(invertedMapping);
+    });
+
+    invertedMap.setSourceContent(this._url, code);
+
+    return {
+      code: code,
+      map: new SourceMapConsumer(invertedMap.toJSON())
+    };
+  },
+
+  /**
+   * Save the source map back to our thread's ThreadSources object so that
+   * stepping, breakpoints, debugger statements, etc can use it. If we are
+   * pretty printing a source mapped source, we need to compose the existing
+   * source map with our new one.
+   */
+  _saveMap: function SA__saveMap({ map }) {
+    if (this._sourceMap) {
+      // Compose the source maps
+      this._sourceMap = SourceMapGenerator.fromSourceMap(this._sourceMap);
+      this._sourceMap.applySourceMap(map, this._url);
+      this._sourceMap = new SourceMapConsumer(this._sourceMap.toJSON());
+      this._threadActor.sources.saveSourceMap(this._sourceMap,
+                                              this._generatedSource);
+    } else {
+      this._sourceMap = map;
+      this._threadActor.sources.saveSourceMap(this._sourceMap, this._url);
+    }
+  },
+
+  /**
    * Handler for the "blackbox" packet.
    */
   onBlackBox: function SA_onBlackBox(aRequest) {
     this.threadActor.sources.blackBox(this.url);
     let packet = {
       from: this.actorID
     };
     if (this.threadActor.state == "paused"
@@ -2392,17 +2505,18 @@ SourceActor.prototype = {
       from: this.actorID
     };
   }
 };
 
 SourceActor.prototype.requestTypes = {
   "source": SourceActor.prototype.onSource,
   "blackbox": SourceActor.prototype.onBlackBox,
-  "unblackbox": SourceActor.prototype.onUnblackBox
+  "unblackbox": SourceActor.prototype.onUnblackBox,
+  "prettyPrint": SourceActor.prototype.onPrettyPrint
 };
 
 
 /**
  * Creates an actor for the specified object.
  *
  * @param aObj Debugger.Object
  *        The debuggee object.
@@ -3481,28 +3595,31 @@ ThreadSources.prototype = {
    *
    * Right now this takes a URL, but in the future it should
    * take a Debugger.Source. See bug 637572.
    *
    * @param String aURL
    *        The source URL.
    * @param optional SourceMapConsumer aSourceMap
    *        The source map that introduced this source, if any.
+   * @param optional String aGeneratedSource
+   *        The generated source url that introduced this source via source map,
+   *        if any.
    * @returns a SourceActor representing the source at aURL or null.
    */
-  source: function TS_source(aURL, aSourceMap=null) {
+  source: function TS_source(aURL, aSourceMap=null, aGeneratedSource=null) {
     if (!this._allow(aURL)) {
       return null;
     }
 
     if (aURL in this._sourceActors) {
       return this._sourceActors[aURL];
     }
 
-    let actor = new SourceActor(aURL, this._thread, aSourceMap);
+    let actor = new SourceActor(aURL, this._thread, aSourceMap, aGeneratedSource);
     this._thread.threadLifetimePool.addActor(actor);
     this._sourceActors[aURL] = actor;
     try {
       this._onNewSource(actor);
     } catch (e) {
       reportError(e);
     }
     return actor;
@@ -3519,17 +3636,17 @@ ThreadSources.prototype = {
   sourcesForScript: function TS_sourcesForScript(aScript) {
     if (!this._useSourceMaps || !aScript.sourceMapURL) {
       return resolve([this.source(aScript.url)].filter(isNotNull));
     }
 
     return this.sourceMap(aScript)
       .then((aSourceMap) => {
         return [
-          this.source(s, aSourceMap) for (s of aSourceMap.sources)
+          this.source(s, aSourceMap, aScript.url) for (s of aSourceMap.sources)
         ];
       })
       .then(null, (e) => {
         reportError(e);
         delete this._sourceMapsByGeneratedSource[aScript.url];
         return [this.source(aScript.url)];
       })
       .then(function (aSources) {
@@ -3541,28 +3658,35 @@ ThreadSources.prototype = {
    * Return a promise of a SourceMapConsumer for the source map for
    * |aScript|; if we already have such a promise extant, return that.
    * |aScript| must have a non-null sourceMapURL.
    */
   sourceMap: function TS_sourceMap(aScript) {
     dbg_assert(aScript.sourceMapURL, "Script should have a sourceMapURL");
     let sourceMapURL = this._normalize(aScript.sourceMapURL, aScript.url);
     let map = this._fetchSourceMap(sourceMapURL, aScript.url)
-      .then((aSourceMap) => {
-        for (let s of aSourceMap.sources) {
-          this._generatedUrlsByOriginalUrl[s] = aScript.url;
-          this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
-        }
-        return aSourceMap;
-      });
+      .then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url));
     this._sourceMapsByGeneratedSource[aScript.url] = map;
     return map;
   },
 
   /**
+   * Save the given source map so that we can use it to query source locations
+   * down the line.
+   */
+  saveSourceMap: function TS_saveSourceMap(aSourceMap, aGeneratedSource) {
+    this._sourceMapsByGeneratedSource[aGeneratedSource] = resolve(aSourceMap);
+    for (let s of aSourceMap.sources) {
+      this._generatedUrlsByOriginalUrl[s] = aGeneratedSource;
+      this._sourceMapsByOriginalSource[s] = resolve(aSourceMap);
+    }
+    return aSourceMap;
+  },
+
+  /**
    * Return a promise of a SourceMapConsumer for the source map located at
    * |aAbsSourceMapURL|, which must be absolute. If there is already such a
    * promise extant, return it.
    *
    * @param string aAbsSourceMapURL
    *        The source map URL, in absolute form, not relative.
    * @param string aScriptURL
    *        When the source map URL is a data URI, there is no sourceRoot on the
--- a/toolkit/devtools/server/child.js
+++ b/toolkit/devtools/server/child.js
@@ -16,17 +16,17 @@ let onConnect = DevToolsUtils.makeInfall
   removeMessageListener("debug:connect", onConnect);
 
   let mm = msg.target;
 
   let prefix = msg.data.prefix + docShell.appId;
 
   let conn = DebuggerServer.connectToParent(prefix, mm);
 
-  let actor = new DebuggerServer.ContentTabActor(conn, content);
+  let actor = new DebuggerServer.ContentAppActor(conn, content);
   let actorPool = new ActorPool(conn);
   actorPool.addActor(actor);
   conn.addActorPool(actorPool);
 
   sendAsyncMessage("debug:actor", {actor: actor.grip(),
                                    appId: docShell.appId,
                                    prefix: prefix});
 });
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -39,16 +39,18 @@ if (this.require) {
 } else {
   let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
   localRequire = id => devtools.require(id);
 }
 
 const DBG_STRINGS_URI = "chrome://global/locale/devtools/debugger.properties";
 
 const nsFile = CC("@mozilla.org/file/local;1", "nsIFile", "initWithPath");
+Cu.import("resource://gre/modules/reflect.jsm");
+Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log");
 const promptConnections = Services.prefs.getBoolPref("devtools.debugger.prompt-connection");
 
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 addDebuggerToGlobal(this);
 
@@ -67,16 +69,17 @@ function loadSubScript(aURL)
 }
 
 let loaderRequire = this.require;
 this.require = null;
 loadSubScript.call(this, "resource://gre/modules/commonjs/sdk/core/promise.js");
 this.require = loaderRequire;
 
 Cu.import("resource://gre/modules/devtools/SourceMap.jsm");
+const escodegen = localRequire("escodegen/escodegen");
 
 loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js");
 
 function dumpn(str) {
   if (wantLogging) {
     dump("DBG-SERVER: " + str + "\n");
   }
 }
@@ -377,17 +380,17 @@ var DebuggerServer = {
     if (!("BrowserTabActor" in this)) {
       this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
       this.addActors("resource://gre/modules/devtools/server/actors/script.js");
       this.addActors("resource://gre/modules/devtools/server/actors/webconsole.js");
       this.addActors("resource://gre/modules/devtools/server/actors/gcli.js");
       this.addActors("resource://gre/modules/devtools/server/actors/styleeditor.js");
       this.registerModule("devtools/server/actors/inspector");
     }
-    if (!("ContentTabActor" in DebuggerServer)) {
+    if (!("ContentAppActor" in DebuggerServer)) {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Listens on the given port or socket file for remote debugger connections.
    *
    * @param aPortOrPath int, string
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -10,21 +10,32 @@ const Cr = Components.results;
 Cu.import("resource://gre/modules/Services.jsm");
 
 // Always log packets when running tests. runxpcshelltests.py will throw
 // the output away anyway, unless you give it the --verbose flag.
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 // Enable remote debugging for the relevant tests.
 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 
-Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
-Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
-Cu.import("resource://gre/modules/devtools/Loader.jsm");
 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm");
 
+function tryImport(url) {
+  try {
+    Cu.import(url);
+  } catch (e) {
+    dump("Error importing " + url + "\n");
+    dump(DevToolsUtils.safeErrorString(e) + "\n");
+    throw e;
+  }
+}
+
+tryImport("resource://gre/modules/devtools/dbg-server.jsm");
+tryImport("resource://gre/modules/devtools/dbg-client.jsm");
+tryImport("resource://gre/modules/devtools/Loader.jsm");
+
 function testExceptionHook(ex) {
   try {
     do_report_unexpected_exception(ex);
   } catch(ex) {
     return {throw: ex}
   }
 }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-01.js
@@ -0,0 +1,42 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+// Test basic pretty printing functionality
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+function evalCode() {
+  gClient.addOneTimeListener("newSource", prettyPrintSource);
+  const code = "" + function main() { let a = 1 + 3; let b = a++; return b + a; };
+  Cu.evalInSandbox(
+    code,
+    gDebuggee,
+    "1.8",
+    "data:text/javascript," + code);
+}
+
+function prettyPrintSource(event, { source }) {
+  gThreadClient.source(source).prettyPrint(4, testPrettyPrinted);
+}
+
+function testPrettyPrinted({ error, source}) {
+  do_check_true(!error);
+  do_check_true(source.contains("\n    "));
+  finishClient(gClient);
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-02.js
@@ -0,0 +1,67 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gSource;
+
+// Test stepping through pretty printed sources.
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+const CODE = "" + function main() { debugger; return 10; };
+const CODE_URL = "data:text/javascript," + CODE;
+
+function evalCode() {
+  gClient.addOneTimeListener("newSource", prettyPrintSource);
+  Cu.evalInSandbox(
+    CODE,
+    gDebuggee,
+    "1.8",
+    CODE_URL,
+    1
+  );
+}
+
+function prettyPrintSource(event, { source }) {
+  gSource = source;
+  gThreadClient.source(gSource).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+  do_check_true(!error);
+  gClient.addOneTimeListener("paused", testDbgStatement);
+  gDebuggee.main();
+}
+
+function testDbgStatement(event, { frame }) {
+  const { url, line, column } = frame.where;
+  do_check_eq(url, CODE_URL);
+  do_check_eq(line, 2);
+  do_check_eq(column, 2);
+  testStepping();
+}
+
+function testStepping() {
+  gClient.addOneTimeListener("paused", (event, { frame }) => {
+    const { url, line, column } = frame.where;
+    do_check_eq(url, CODE_URL);
+    do_check_eq(line, 3);
+    do_check_eq(column, 2);
+    finishClient(gClient);
+  });
+  gThreadClient.stepIn();
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/unit/test_pretty_print-03.js
@@ -0,0 +1,74 @@
+/* -*- Mode: javascript; js-indent-level: 2; -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test pretty printing source mapped sources.
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+var gSource;
+
+Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
+
+function run_test() {
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-pretty-print");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestTabAndResume(gClient, "test-pretty-print", function(aResponse, aTabClient, aThreadClient) {
+      gThreadClient = aThreadClient;
+      evalCode();
+    });
+  });
+  do_test_pending();
+}
+
+const dataUrl = s => "data:text/javascript," + s;
+
+const A = "function a(){b()}";
+const A_URL = dataUrl(A);
+const B = "function b(){debugger}";
+const B_URL = dataUrl(B);
+
+function evalCode() {
+  let { code, map } = (new SourceNode(null, null, null, [
+    new SourceNode(1, 0, A_URL, A),
+    B.split("").map((ch, i) => new SourceNode(1, i, B_URL, ch))
+  ])).toStringWithSourceMap({
+    file: "abc.js"
+  });
+
+  code += "//# sourceMappingURL=data:text/json;base64," + btoa(map.toString());
+
+  gClient.addListener("newSource", waitForB);
+  Components.utils.evalInSandbox(code, gDebuggee, "1.8",
+                                 "http://example.com/foo.js", 1);
+}
+
+function waitForB(event, { source }) {
+  if (source.url !== B_URL) {
+    return;
+  }
+  gClient.removeListener("newSource", waitForB);
+  prettyPrint(source);
+}
+
+function prettyPrint(source) {
+  gThreadClient.source(source).prettyPrint(2, runCode);
+}
+
+function runCode({ error }) {
+  do_check_true(!error);
+  gClient.addOneTimeListener("paused", testDbgStatement);
+  gDebuggee.a();
+}
+
+function testDbgStatement(event, { frame, why }) {
+  do_check_eq(why.type, "debuggerStatement");
+  const { url, line, column } = frame.where;
+  do_check_eq(url, B_URL);
+  do_check_eq(line, 2);
+  do_check_eq(column, 2);
+  finishClient(gClient);
+}
--- a/toolkit/devtools/server/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/server/tests/unit/xpcshell.ini
@@ -148,16 +148,19 @@ reason = bug 820380
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_pause_exceptions-02.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_longstringactor.js]
 [test_longstringgrips-01.js]
 [test_longstringgrips-02.js]
+[test_pretty_print-01.js]
+[test_pretty_print-02.js]
+[test_pretty_print-03.js]
 [test_source-01.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
 [test_breakpointstore.js]
 [test_profiler_actor.js]
 [test_profiler_activation.js]
 skip-if = toolkit == "gonk"
 reason = bug 820380
--- a/toolkit/devtools/sourcemap/SourceMap.jsm
+++ b/toolkit/devtools/sourcemap/SourceMap.jsm
@@ -485,16 +485,17 @@ define('source-map/util', ['require', 'e
       return aDefaultValue;
     } else {
       throw new Error('"' + aName + '" is a required argument.');
     }
   }
   exports.getArg = getArg;
 
   var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+  var dataUrlRegexp = /^data:.+\,.+/;
 
   function urlParse(aUrl) {
     var match = aUrl.match(urlRegexp);
     if (!match) {
       return null;
     }
     return {
       scheme: match[1],
@@ -522,17 +523,17 @@ define('source-map/util', ['require', 'e
     }
     return url;
   }
   exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
-    if (aPath.match(urlRegexp)) {
+    if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
       url.path = aPath;
       return urlGenerate(url);
     }
 
@@ -1077,34 +1078,37 @@ define('source-map/source-map-generator'
    *        If omitted, SourceMapConsumer's file property will be used.
    */
   SourceMapGenerator.prototype.applySourceMap =
     function SourceMapGenerator_applySourceMap(aSourceMapConsumer, aSourceFile) {
       // If aSourceFile is omitted, we will use the file property of the SourceMap
       if (!aSourceFile) {
         aSourceFile = aSourceMapConsumer.file;
       }
+
       var sourceRoot = this._sourceRoot;
       // Make "aSourceFile" relative if an absolute Url is passed.
       if (sourceRoot) {
         aSourceFile = util.relative(sourceRoot, aSourceFile);
       }
+
       // Applying the SourceMap can add and remove items from the sources and
       // the names array.
       var newSources = new ArraySet();
       var newNames = new ArraySet();
 
       // Find mappings for the "aSourceFile"
       this._mappings.forEach(function (mapping) {
         if (mapping.source === aSourceFile && mapping.original) {
           // Check if it can be mapped by the source map, then update the mapping.
           var original = aSourceMapConsumer.originalPositionFor({
             line: mapping.original.line,
             column: mapping.original.column
           });
+
           if (original.source !== null) {
             // Copy mapping
             if (sourceRoot) {
               mapping.source = util.relative(sourceRoot, original.source);
             } else {
               mapping.source = original.source;
             }
             mapping.original.line = original.line;
--- a/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
+++ b/toolkit/devtools/sourcemap/tests/unit/Utils.jsm
@@ -254,16 +254,17 @@ define('lib/source-map/util', ['require'
       return aDefaultValue;
     } else {
       throw new Error('"' + aName + '" is a required argument.');
     }
   }
   exports.getArg = getArg;
 
   var urlRegexp = /([\w+\-.]+):\/\/((\w+:\w+)@)?([\w.]+)?(:(\d+))?(\S+)?/;
+  var dataUrlRegexp = /^data:.+\,.+/;
 
   function urlParse(aUrl) {
     var match = aUrl.match(urlRegexp);
     if (!match) {
       return null;
     }
     return {
       scheme: match[1],
@@ -291,17 +292,17 @@ define('lib/source-map/util', ['require'
     }
     return url;
   }
   exports.urlGenerate = urlGenerate;
 
   function join(aRoot, aPath) {
     var url;
 
-    if (aPath.match(urlRegexp)) {
+    if (aPath.match(urlRegexp) || aPath.match(dataUrlRegexp)) {
       return aPath;
     }
 
     if (aPath.charAt(0) === '/' && (url = urlParse(aRoot))) {
       url.path = aPath;
       return urlGenerate(url);
     }
 
--- a/widget/windows/winrt/MetroWidget.cpp
+++ b/widget/windows/winrt/MetroWidget.cpp
@@ -1246,17 +1246,16 @@ MetroWidget::CSSIntPointToLayoutDeviceIn
   double scale = GetDefaultScale();
   LayoutDeviceIntPoint devPx(int32_t(NS_round(scale * aCSSPoint.x)),
                              int32_t(NS_round(scale * aCSSPoint.y)));
   return devPx;
 }
 
 float MetroWidget::GetDPI()
 {
-  LogFunction();
   if (!mView) {
     return 96.0;
   }
   return mView->GetDPI();
 }
 
 void MetroWidget::ChangedDPI()
 {
--- a/xpcom/ds/TimeStamp.cpp
+++ b/xpcom/ds/TimeStamp.cpp
@@ -25,19 +25,18 @@ TimeStamp::ProcessCreation(bool& aIsInco
     char *mozAppRestart = PR_GetEnv("MOZ_APP_RESTART");
     TimeStamp ts;
 
     /* When calling PR_SetEnv() with an empty value the existing variable may
      * be unset or set to the empty string depending on the underlying platform
      * thus we have to check if the variable is present and not empty. */
     if (mozAppRestart && (strcmp(mozAppRestart, "") != 0)) {
       /* Firefox was restarted, use the first time-stamp we've taken as the new
-       * process startup time and unset MOZ_APP_RESTART. */
+       * process startup time. */
       ts = sFirstTimeStamp;
-      PR_SetEnv("MOZ_APP_RESTART=");
     } else {
       TimeStamp now = Now();
       uint64_t uptime = ComputeProcessUptime();
 
       ts = now - TimeDuration::FromMicroseconds(uptime);
 
       if ((ts > sFirstTimeStamp) || (uptime == 0)) {
         /* If the process creation timestamp was inconsistent replace it with
@@ -52,13 +51,12 @@ TimeStamp::ProcessCreation(bool& aIsInco
   }
 
   return sProcessCreation;
 }
 
 void
 TimeStamp::RecordProcessRestart()
 {
-  PR_SetEnv("MOZ_APP_RESTART=1");
   sProcessCreation = TimeStamp();
 }
 
 } // namespace mozilla