merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 11 Feb 2016 11:47:12 +0100
changeset 283779 b21946a2e9933fa9167456ce6b3866381f6f52c5
parent 283735 ac39fba33c6daf95b2cda71e588ca18e2eb752ab (current diff)
parent 283778 0add7cd89394362d9458cc1a9c3a63d8ac4205e2 (diff)
child 283905 d4d72e7b30da251ad3027e234444251adad5e335
push id29990
push usercbook@mozilla.com
push dateThu, 11 Feb 2016 10:47:29 +0000
treeherdermozilla-central@b21946a2e993 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone47.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 mozilla-central a=merge
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -364,15 +364,15 @@ function showDefaultSnippets()
       links[0].href = DEFAULT_SNIPPETS_URLS[randIndex];
     }
   }
   // Move the default snippet to the snippets element.
   snippetsElt.appendChild(entry);
 }
 
 function fitToWidth() {
-  if (window.scrollMaxX != window.scrollMinX) {
+  if (document.documentElement.scrollWidth > window.innerWidth) {
     document.body.setAttribute("narrow", "true");
   } else if (document.body.hasAttribute("narrow")) {
     document.body.removeAttribute("narrow");
     fitToWidth();
   }
 }
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -26,16 +26,17 @@ nsContextMenu.prototype = {
   initMenu: function CM_initMenu(aXulMenu, aIsShift) {
     // Get contextual info.
     this.setTarget(document.popupNode, document.popupRangeParent,
                    document.popupRangeOffset);
     if (!this.shouldDisplay)
       return;
 
     this.hasPageMenu = false;
+    this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
     if (!aIsShift) {
       if (this.isRemote) {
         this.hasPageMenu =
           PageMenuParent.addToPopup(gContextMenuContentData.customMenuItems,
                                     this.browser, aXulMenu);
       }
       else {
         this.hasPageMenu = PageMenuParent.buildAndAddToPopup(this.target, aXulMenu);
@@ -66,16 +67,18 @@ nsContextMenu.prototype = {
 
     this.isFrameImage = document.getElementById("isFrameImage");
     this.ellipsis = "\u2026";
     try {
       this.ellipsis = gPrefService.getComplexValue("intl.ellipsis",
                                                    Ci.nsIPrefLocalizedString).data;
     } catch (e) { }
 
+    // Reset after "on-build-contextmenu" notification in case selection was
+    // changed during the notification.
     this.isContentSelected = !this.selectionInfo.docSelectionIsCollapsed;
     this.onPlainTextLink = false;
 
     let bookmarkPage = document.getElementById("context-bookmarkpage");
     if (bookmarkPage)
       BookmarkingUI.onCurrentPageContextPopupShowing();
 
     // Initialize (disable/remove) menu items.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2931,16 +2931,72 @@
             if (nextTab)
               this.moveTabTo(this.mCurrentTab, nextTab._tPos);
             else if (this.arrowKeysShouldWrap)
               this.moveTabToStart();
           ]]>
         </body>
       </method>
 
+      <!-- Adopts a tab from another browser window, and inserts it at aIndex -->
+      <method name="adoptTab">
+        <parameter name="aTab"/>
+        <parameter name="aIndex"/>
+        <parameter name="aSelectTab"/>
+        <body>
+        <![CDATA[
+          // Swap the dropped tab with a new one we create and then close
+          // it in the other window (making it seem to have moved between
+          // windows).
+          let newTab = this.addTab("about:blank");
+          let newBrowser = this.getBrowserForTab(newTab);
+          let newURL = aTab.linkedBrowser.currentURI.spec;
+
+          // If we're an e10s browser window, an exception will be thrown
+          // if we attempt to drag a non-remote browser in, so we need to
+          // ensure that the remoteness of the newly created browser is
+          // appropriate for the URL of the tab being dragged in.
+          this.updateBrowserRemotenessByURL(newBrowser, newURL);
+
+          // Stop the about:blank load.
+          newBrowser.stop();
+          // Make sure it has a docshell.
+          newBrowser.docShell;
+
+          let numPinned = this._numPinnedTabs;
+          if (aIndex < numPinned || (aTab.pinned && aIndex == numPinned)) {
+            this.pinTab(newTab);
+          }
+
+          this.moveTabTo(newTab, aIndex);
+
+          // We need to select the tab before calling swapBrowsersAndCloseOther
+          // so that window.content in chrome windows points to the right tab
+          // when pagehide/show events are fired. This is no longer necessary
+          // for any exiting browser code, but it may be necessary for add-on
+          // compatibility.
+          if (aSelectTab) {
+            this.selectedTab = newTab;
+          }
+
+          aTab.parentNode._finishAnimateTabMove();
+          this.swapBrowsersAndCloseOther(newTab, aTab);
+
+          if (aSelectTab) {
+            // Call updateCurrentBrowser to make sure the URL bar is up to date
+            // for our new tab after we've done swapBrowsersAndCloseOther.
+            this.updateCurrentBrowser(true);
+          }
+
+          return newTab;
+        ]]>
+        </body>
+      </method>
+
+
       <method name="moveTabBackward">
         <body>
           <![CDATA[
             let previousTab = this.mCurrentTab.previousSibling;
             while (previousTab && previousTab.hidden)
               previousTab = previousTab.previousSibling;
 
             if (previousTab)
@@ -5786,52 +5842,18 @@
           // actually move the dragged tab
           if ("animDropIndex" in draggedTab._dragData) {
             let newIndex = draggedTab._dragData.animDropIndex;
             if (newIndex > draggedTab._tPos)
               newIndex--;
             this.tabbrowser.moveTabTo(draggedTab, newIndex);
           }
         } else if (draggedTab) {
-          // swap the dropped tab with a new one we create and then close
-          // it in the other window (making it seem to have moved between
-          // windows)
           let newIndex = this._getDropIndex(event, false);
-          let newTab = this.tabbrowser.addTab("about:blank");
-          let newBrowser = this.tabbrowser.getBrowserForTab(newTab);
-          let draggedBrowserURL = draggedTab.linkedBrowser.currentURI.spec;
-
-          // If we're an e10s browser window, an exception will be thrown
-          // if we attempt to drag a non-remote browser in, so we need to
-          // ensure that the remoteness of the newly created browser is
-          // appropriate for the URL of the tab being dragged in.
-          this.tabbrowser.updateBrowserRemotenessByURL(newBrowser,
-                                                       draggedBrowserURL);
-
-          // Stop the about:blank load
-          newBrowser.stop();
-          // make sure it has a docshell
-          newBrowser.docShell;
-
-          let numPinned = this.tabbrowser._numPinnedTabs;
-          if (newIndex < numPinned || draggedTab.pinned && newIndex == numPinned)
-            this.tabbrowser.pinTab(newTab);
-          this.tabbrowser.moveTabTo(newTab, newIndex);
-
-          // We need to select the tab before calling swapBrowsersAndCloseOther
-          // so that window.content in chrome windows points to the right tab
-          // when pagehide/show events are fired.
-          this.tabbrowser.selectedTab = newTab;
-
-          draggedTab.parentNode._finishAnimateTabMove();
-          this.tabbrowser.swapBrowsersAndCloseOther(newTab, draggedTab);
-
-          // Call updateCurrentBrowser to make sure the URL bar is up to date
-          // for our new tab after we've done swapBrowsersAndCloseOther.
-          this.tabbrowser.updateCurrentBrowser(true);
+          this.tabbrowser.adoptTab(draggedTab, newIndex, true);
         } else {
           // Pass true to disallow dropping javascript: or data: urls
           let url;
           try {
             url = browserDragAndDrop.drop(event, { }, true);
           } catch (ex) {}
 
           if (!url)
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -124,16 +124,17 @@ skip-if = os == "linux" # Bug 958026
 support-files =
   content_aboutAccounts.js
 [browser_aboutCertError.js]
 [browser_aboutSupport_newtab_security_state.js]
 [browser_aboutHealthReport.js]
 skip-if = os == "linux" # Bug 924307
 [browser_aboutHome.js]
 skip-if = e10s # Bug 1093153 - no about:home support yet
+[browser_aboutHome_wrapsCorrectly.js]
 [browser_action_keyword.js]
 [browser_action_keyword_override.js]
 [browser_action_searchengine.js]
 [browser_action_searchengine_alias.js]
 [browser_addKeywordSearch.js]
 [browser_search_favicon.js]
 skip-if = e10s # Bug 1212647
 [browser_alltabslistener.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_aboutHome_wrapsCorrectly.js
@@ -0,0 +1,28 @@
+add_task(function* () {
+  let newWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  let resizedPromise = BrowserTestUtils.waitForEvent(newWindow, "resize");
+  newWindow.resizeTo(300, 300);
+  yield resizedPromise;
+
+  yield BrowserTestUtils.openNewForegroundTab(newWindow.gBrowser, "about:home");
+
+  yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+    is(content.document.body.getAttribute("narrow"), "true", "narrow mode");
+  });
+
+  resizedPromise = BrowserTestUtils.waitForContentEvent(newWindow.gBrowser.selectedBrowser, "resize");
+
+
+  yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+    content.window.resizeTo(800, 800);
+  });
+
+  yield resizedPromise;
+
+  yield ContentTask.spawn(newWindow.gBrowser.selectedBrowser, {}, function* () {
+    is(content.document.body.hasAttribute("narrow"), false, "non-narrow mode");
+  });
+
+  yield BrowserTestUtils.closeWindow(newWindow);
+});
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -189,53 +189,49 @@ const CustomizableWidgets = [
       options.queryType = options.QUERY_TYPE_HISTORY;
       options.sortingMode = options.SORT_BY_DATE_DESCENDING;
       options.maxResults = kMaxResults;
       let query = PlacesUtils.history.getNewQuery();
 
       let items = doc.getElementById("PanelUI-historyItems");
       // Clear previous history items.
       while (items.firstChild) {
-        items.removeChild(items.firstChild);
+        items.firstChild.remove();
       }
 
       // Get all statically placed buttons to supply them with keyboard shortcuts.
       let staticButtons = items.parentNode.getElementsByTagNameNS(kNSXUL, "toolbarbutton");
       for (let i = 0, l = staticButtons.length; i < l; ++i)
         CustomizableUI.addShortcut(staticButtons[i]);
 
       PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
                          .asyncExecuteLegacyQueries([query], 1, options, {
         handleResult: function (aResultSet) {
-          let onHistoryVisit = function (aUri, aEvent, aItem) {
-            doc.defaultView.openUILink(aUri, aEvent);
-            CustomizableUI.hidePanelForNode(aItem);
+          let onItemClick = function (aEvent) {
+            let item = aEvent.target;
+            win.openUILink(item.getAttribute("targetURI"), aEvent);
+            CustomizableUI.hidePanelForNode(item);
           };
           let fragment = doc.createDocumentFragment();
-          for (let row, i = 0; (row = aResultSet.getNextRow()); i++) {
-            try {
-              let uri = row.getResultByIndex(1);
-              let title = row.getResultByIndex(2);
-              let icon = row.getResultByIndex(6);
+          let row;
+          while ((row = aResultSet.getNextRow())) {
+            let uri = row.getResultByIndex(1);
+            let title = row.getResultByIndex(2);
+            let icon = row.getResultByIndex(6);
 
-              let item = doc.createElementNS(kNSXUL, "toolbarbutton");
-              item.setAttribute("label", title || uri);
-              item.setAttribute("targetURI", uri);
-              item.setAttribute("class", "subviewbutton");
-              item.addEventListener("click", function (aEvent) {
-                onHistoryVisit(uri, aEvent, item);
-              });
-              if (icon) {
-                let iconURL = "moz-anno:favicon:" + icon;
-                item.setAttribute("image", iconURL);
-              }
-              fragment.appendChild(item);
-            } catch (e) {
-              log.error("Error while showing history subview: " + e);
+            let item = doc.createElementNS(kNSXUL, "toolbarbutton");
+            item.setAttribute("label", title || uri);
+            item.setAttribute("targetURI", uri);
+            item.setAttribute("class", "subviewbutton");
+            item.addEventListener("click", onItemClick);
+            if (icon) {
+              let iconURL = "moz-anno:favicon:" + icon;
+              item.setAttribute("image", iconURL);
             }
+            fragment.appendChild(item);
           }
           items.appendChild(fragment);
         },
         handleError: function (aError) {
           log.debug("History view tried to show but had an error: " + aError);
         },
         handleCompletion: function (aReason) {
           log.debug("History view is being shown!");
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/ext-desktop-runtime.js
@@ -0,0 +1,10 @@
+"use strict";
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("uninstall", (msg, extension) => {
+  if (extension.uninstallURL) {
+    let browser = Services.wm.getMostRecentWindow("navigator:browser").gBrowser;
+    browser.addTab(extension.uninstallURL, { relatedToCurrent: true });
+  }
+});
+
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -608,51 +608,43 @@ extensions.registerSchemaAPI("tabs", nul
           let tab = TabManager.getTab(tabId);
           // Ignore invalid tab ids.
           if (!tab) {
             continue;
           }
 
           // If the window is not specified, use the window from the tab.
           let window = destinationWindow || tab.ownerDocument.defaultView;
-          let windowId = WindowManager.getId(window);
           let gBrowser = window.gBrowser;
 
-          let getInsertionPoint = () => {
-            let point = indexMap.get(window) || index;
-            // If the index is -1 it should go to the end of the tabs.
-            if (point == -1) {
-              point = gBrowser.tabs.length;
-            }
-            indexMap.set(window, point + 1);
-            return point;
-          };
+          let insertionPoint = indexMap.get(window) || index;
+          // If the index is -1 it should go to the end of the tabs.
+          if (insertionPoint == -1) {
+            insertionPoint = gBrowser.tabs.length;
+          }
 
-          if (WindowManager.getId(tab.ownerDocument.defaultView) !== windowId) {
+          // We can only move pinned tabs to a point within, or just after,
+          // the current set of pinned tabs. Unpinned tabs, likewise, can only
+          // be moved to a position after the current set of pinned tabs.
+          // Attempts to move a tab to an illegal position are ignored.
+          let numPinned = gBrowser._numPinnedTabs;
+          let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
+          if (!ok) {
+            continue;
+          }
+
+          indexMap.set(window, insertionPoint + 1);
+
+          if (tab.ownerDocument.defaultView !== window) {
             // If the window we are moving the tab in is different, then move the tab
             // to the new window.
-            let newTab = gBrowser.addTab("about:blank");
-            let newBrowser = gBrowser.getBrowserForTab(newTab);
-            gBrowser.updateBrowserRemotenessByURL(newBrowser, tab.linkedBrowser.currentURI.spec);
-            newBrowser.stop();
-            // This is necessary for getter side-effects.
-            void newBrowser.docShell;
-
-            if (tab.pinned) {
-              gBrowser.pinTab(newTab);
-            }
-
-            gBrowser.moveTabTo(newTab, getInsertionPoint());
-
-            tab.parentNode._finishAnimateTabMove();
-            gBrowser.swapBrowsersAndCloseOther(newTab, tab);
-            tab = newTab;
+            tab = gBrowser.adoptTab(tab, insertionPoint, false);
           } else {
             // If the window we are moving is the same, just move the tab.
-            gBrowser.moveTabTo(tab, getInsertionPoint());
+            gBrowser.moveTabTo(tab, insertionPoint);
           }
           tabsMoved.push(tab);
         }
 
         return Promise.resolve(tabsMoved.map(tab => TabManager.convert(extension, tab)));
       },
     },
   };
--- a/browser/components/extensions/jar.mn
+++ b/browser/components/extensions/jar.mn
@@ -3,11 +3,12 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 browser.jar:
     content/browser/extension.svg
     content/browser/ext-utils.js
     content/browser/ext-contextMenus.js
     content/browser/ext-browserAction.js
     content/browser/ext-pageAction.js
+    content/browser/ext-desktop-runtime.js
     content/browser/ext-tabs.js
     content/browser/ext-windows.js
     content/browser/ext-bookmarks.js
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -16,16 +16,17 @@ support-files =
 [browser_ext_browserAction_disabled.js]
 [browser_ext_pageAction_context.js]
 [browser_ext_pageAction_popup.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_popup_api_injection.js]
 [browser_ext_contextMenus.js]
 [browser_ext_getViews.js]
 [browser_ext_lastError.js]
+[browser_ext_runtime_setUninstallURL.js]
 [browser_ext_tabs_audio.js]
 [browser_ext_tabs_captureVisibleTab.js]
 [browser_ext_tabs_executeScript.js]
 [browser_ext_tabs_executeScript_good.js]
 [browser_ext_tabs_executeScript_bad.js]
 [browser_ext_tabs_insertCSS.js]
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_getCurrent.js]
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -228,49 +228,53 @@ add_task(function* testInvalidIconSizes(
       "browser_action": {},
       "page_action": {},
     },
 
     background: function() {
       browser.tabs.query({ active: true, currentWindow: true }, tabs => {
         let tabId = tabs[0].id;
 
+        let promises = [];
         for (let api of ["pageAction", "browserAction"]) {
           // helper function to run setIcon and check if it fails
           let assertSetIconThrows = function(detail, error, message) {
-            try {
-              detail.tabId = tabId;
-              browser[api].setIcon(detail);
+            detail.tabId = tabId;
+            promises.push(
+              browser[api].setIcon(detail).then(
+                () => {
+                  browser.test.fail("Expected an error on invalid icon size.");
+                  browser.test.notifyFail("setIcon with invalid icon size");
+                },
+                error => {
+                  browser.test.succeed("setIcon with invalid icon size");
+                }));
+          };
 
-              browser.test.fail("Expected an error on invalid icon size.");
-              browser.test.notifyFail("setIcon with invalid icon size");
-              return;
-            } catch (e) {
-              browser.test.succeed("setIcon with invalid icon size");
-            }
-          };
+          let imageData = new ImageData(1, 1);
 
           // test invalid icon size inputs
           for (let type of ["path", "imageData"]) {
-            assertSetIconThrows({ [type]: { "abcdef": "test.png" } });
-            assertSetIconThrows({ [type]: { "48px": "test.png" } });
-            assertSetIconThrows({ [type]: { "20.5": "test.png" } });
-            assertSetIconThrows({ [type]: { "5.0": "test.png" } });
-            assertSetIconThrows({ [type]: { "-300": "test.png" } });
-            assertSetIconThrows({ [type]: {
-              "abc": "test.png",
-              "5": "test.png"
-            }});
+            let img = type == "imageData" ? imageData : "test.png";
+
+            assertSetIconThrows({ [type]: { "abcdef": img } });
+            assertSetIconThrows({ [type]: { "48px": img } });
+            assertSetIconThrows({ [type]: { "20.5": img } });
+            assertSetIconThrows({ [type]: { "5.0": img } });
+            assertSetIconThrows({ [type]: { "-300": img } });
+            assertSetIconThrows({ [type]: { "abc": img, "5": img }});
           }
 
-          assertSetIconThrows({ imageData: { "abcdef": "test.png" }, path: {"5": "test.png"} });
-          assertSetIconThrows({ path: { "abcdef": "test.png" }, imageData: {"5": "test.png"} });
+          assertSetIconThrows({ imageData: { "abcdef": imageData }, path: {"5": "test.png"} });
+          assertSetIconThrows({ path: { "abcdef": "test.png" }, imageData: {"5": imageData} });
         }
 
-        browser.test.notifyPass("setIcon with invalid icon size");
+        Promise.all(promises).then(() => {
+          browser.test.notifyPass("setIcon with invalid icon size");
+        });
       });
     }
   });
 
   yield Promise.all([extension.startup(), extension.awaitFinish("setIcon with invalid icon size")]);
 
   yield extension.unload();
 });
@@ -342,35 +346,34 @@ add_task(function* testSecureURLsDenied(
 
     background: function() {
       browser.tabs.query({ active: true, currentWindow: true }, tabs => {
         let tabId = tabs[0].id;
 
         let urls = ["chrome://browser/content/browser.xul",
                     "javascript:true"];
 
+        let promises = [];
         for (let url of urls) {
           for (let api of ["pageAction", "browserAction"]) {
-            try {
-              browser[api].setIcon({tabId, path: url});
-
-              browser.test.fail(`Load of '${url}' succeeded. Expected failure.`);
-              browser.test.notifyFail("setIcon security tests");
-              return;
-            } catch (e) {
-              // We can't actually inspect the error here, since the
-              // error object belongs to the privileged scope of the API,
-              // rather than to the extension scope that calls into it.
-              // Just assume it's the expected security error, for now.
-              browser.test.succeed(`Load of '${url}' failed. Expected failure.`);
-            }
+            promises.push(
+              browser[api].setIcon({tabId, path: url}).then(
+                () => {
+                  browser.test.fail(`Load of '${url}' succeeded. Expected failure.`);
+                  browser.test.notifyFail("setIcon security tests");
+                },
+                error => {
+                  browser.test.succeed(`Load of '${url}' failed. Expected failure. ${error}`);
+                }));
           }
         }
 
-        browser.test.notifyPass("setIcon security tests");
+        Promise.all(promises).then(() => {
+          browser.test.notifyPass("setIcon security tests");
+        });
       });
     },
   });
 
   yield extension.startup();
 
   yield extension.awaitFinish("setIcon security tests");
   yield extension.unload();
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -49,22 +49,23 @@ add_task(function* () {
 
       let parentToDel = browser.contextMenus.create({ title: "parentToDel" });
       browser.contextMenus.create(
         { title: "child1", parentId: parentToDel, onclick: genericOnClick });
       browser.contextMenus.create(
         { title: "child2", parentId: parentToDel, onclick: genericOnClick });
       browser.contextMenus.remove(parentToDel);
 
-      try {
-        browser.contextMenus.update(parent, { parentId: child2 });
-        browser.test.notifyFail();
-      } catch (e) {
-        browser.test.notifyPass();
-      }
+      browser.contextMenus.update(parent, { parentId: child2 }).then(
+        () => {
+          browser.test.notifyFail();
+        },
+        () => {
+          browser.test.notifyPass();
+        });
     },
   });
 
   let expectedClickInfo;
   function checkClickInfo(info) {
     info = JSON.parse(info);
     for (let i in expectedClickInfo) {
       is(info[i], expectedClickInfo[i],
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
@@ -0,0 +1,114 @@
+"use strict";
+
+let { AddonManager } = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
+let { Extension } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function install(url) {
+  return new Promise((resolve, reject) => {
+    AddonManager.getInstallForURL(url, (install) => {
+      install.addListener({
+        onInstallEnded: (i, addon) => resolve(addon),
+        onInstallFailed: () => reject(),
+      });
+      install.install();
+    }, "application/x-xpinstall");
+  });
+}
+
+function* makeAndInstallXPI(id, backgroundScript, loadedURL) {
+  let xpi = Extension.generateXPI(id, {
+    background: "(" + backgroundScript.toString() + ")()",
+  });
+  SimpleTest.registerCleanupFunction(function cleanupXPI() {
+    Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+    xpi.remove(false);
+  });
+
+  let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL);
+
+  let fileURI = Services.io.newFileURI(xpi);
+  info(`installing ${fileURI.spec}`);
+  let addon = yield install(fileURI.spec);
+  info("installed");
+
+  // A WebExtension is started asynchronously, we have our test extension
+  // open a new tab to signal that the background script has executed.
+  let loadTab = yield loadPromise;
+  yield BrowserTestUtils.removeTab(loadTab);
+
+  return addon;
+}
+
+
+add_task(function* test_setuninstallurl_badargs() {
+  function backgroundScript() {
+    let promises = [
+      browser.runtime.setUninstallURL("this is not a url")
+        .then(() => {
+          browser.test.notifyFail("setUninstallURL should have failed with bad url");
+        })
+        .catch(error => {
+          browser.test.assertTrue(/Invalid URL/.test(error.message), "error message indicates malformed url");
+        }),
+
+      browser.runtime.setUninstallURL("file:///etc/passwd")
+        .then(() => {
+          browser.test.notifyFail("setUninstallURL should have failed with non-http[s] url");
+        })
+        .catch(error => {
+          browser.test.assertTrue(/must have the scheme http or https/.test(error.message), "error message indicates bad scheme");
+        }),
+    ];
+
+    Promise.all(promises)
+      .then(() => browser.test.notifyPass("setUninstallURL bad params"));
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: "(" + backgroundScript.toString() + ")()",
+  });
+  yield extension.startup();
+  yield extension.awaitFinish();
+  yield extension.unload();
+});
+
+// Test the documented behavior of setUninstallURL() that passing an
+// empty string is equivalent to not setting an uninstall URL
+// (i.e., no new tab is opened upon uninstall)
+add_task(function* test_setuninstall_empty_url() {
+  function backgroundScript() {
+    browser.runtime.setUninstallURL("")
+      .then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" }));
+  }
+
+  let addon = yield makeAndInstallXPI("test_uinstallurl2@tests.mozilla.org",
+                                      backgroundScript,
+                                      "http://example.com/addon_loaded");
+
+  addon.uninstall(true);
+  info("uninstalled");
+
+  // no need to explicitly check for the absence of a new tab,
+  // BrowserTestUtils will eventually complain if one is opened.
+});
+
+add_task(function* test_setuninstallurl() {
+  function backgroundScript() {
+    browser.runtime.setUninstallURL("http://example.com/addon_uninstalled")
+      .then(() => browser.tabs.create({ url: "http://example.com/addon_loaded" }));
+  }
+
+  let addon = yield makeAndInstallXPI("test_uinstallurl@tests.mozilla.org",
+                                      backgroundScript,
+                                      "http://example.com/addon_loaded");
+
+  // look for a new tab with the uninstall url.
+  let uninstallPromise = BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/addon_uninstalled");
+
+  addon.uninstall(true);
+  info("uninstalled");
+
+  let uninstalledTab = yield uninstallPromise;
+  isnot(uninstalledTab, null, "opened tab with uninstall url");
+  yield BrowserTestUtils.removeTab(uninstalledTab);
+});
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -546,16 +546,17 @@ BrowserGlue.prototype = {
       AddonWatcher.init();
       os.addObserver(this, AddonWatcher.TOPIC_SLOW_ADDON_DETECTED, false);
     }
 
     ExtensionManagement.registerScript("chrome://browser/content/ext-utils.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-browserAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-contextMenus.js");
+    ExtensionManagement.registerScript("chrome://browser/content/ext-desktop-runtime.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-tabs.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
     ExtensionManagement.registerScript("chrome://browser/content/ext-bookmarks.js");
 
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/bookmarks.json");
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/browser_action.json");
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/context_menus.json");
     ExtensionManagement.registerSchema("chrome://browser/content/schemas/context_menus_internal.json");
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -25,19 +25,19 @@ var gMainPane = {
     }
 
 #ifdef HAVE_SHELL_SERVICE
     this.updateSetDefaultBrowser();
 #ifdef XP_WIN
     // In Windows 8 we launch the control panel since it's the only
     // way to get all file type association prefs. So we don't know
     // when the user will select the default.  We refresh here periodically
-    // in case the default changes.  On other Windows OS's defaults can also
+    // in case the default changes. On other Windows OS's defaults can also
     // be set while the prefs are open.
-    window.setInterval(this.updateSetDefaultBrowser, 1000);
+    window.setInterval(this.updateSetDefaultBrowser.bind(this), 1000);
 #endif
 #endif
 
     // set up the "use current page" label-changing listener
     this._updateUseCurrentButton();
     window.addEventListener("focus", this._updateUseCurrentButton.bind(this), false);
 
     this.updateBrowserStartupLastSession();
@@ -690,32 +690,38 @@ var gMainPane = {
   {
     let shellSvc = getShellService();
     let defaultBrowserBox = document.getElementById("defaultBrowserBox");
     if (!shellSvc) {
       defaultBrowserBox.hidden = true;
       return;
     }
     let setDefaultPane = document.getElementById("setDefaultPane");
-    let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
-    setDefaultPane.selectedIndex = selectedIndex;
+    let isDefault = shellSvc.isDefaultBrowser(false, true);
+    setDefaultPane.selectedIndex = isDefault ? 1 : 0;
+    let alwaysCheck = document.getElementById("alwaysCheckDefault");
+    alwaysCheck.disabled = alwaysCheck.disabled ||
+                           isDefault && alwaysCheck.checked;
   },
 
   /**
    * Set browser as the operating system default browser.
    */
   setDefaultBrowser: function()
   {
+    let alwaysCheckPref = document.getElementById("browser.shell.checkDefaultBrowser");
+    alwaysCheckPref.value = true;
+
     let shellSvc = getShellService();
     if (!shellSvc)
       return;
     try {
       shellSvc.setDefaultBrowser(true, false);
     } catch (ex) {
       Cu.reportError(ex);
       return;
     }
-    let selectedIndex =
-      shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
+
+    let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
     document.getElementById("setDefaultPane").selectedIndex = selectedIndex;
   }
 #endif
 };
--- a/browser/components/preferences/in-content/tests/browser.ini
+++ b/browser/components/preferences/in-content/tests/browser.ini
@@ -12,16 +12,17 @@ support-files =
 [browser_bug795764_cachedisabled.js]
 [browser_bug1018066_resetScrollPosition.js]
 [browser_bug1020245_openPreferences_to_paneContent.js]
 [browser_change_app_handler.js]
 skip-if = os != "win" # This test tests the windows-specific app selection dialog, so can't run on non-Windows
 [browser_connection.js]
 [browser_connection_bug388287.js]
 [browser_cookies_exceptions.js]
+[browser_defaultbrowser_alwayscheck.js]
 [browser_healthreport.js]
 skip-if = true || !healthreport # Bug 1185403 for the "true"
 [browser_homepages_filter_aboutpreferences.js]
 [browser_notifications_do_not_disturb.js]
 [browser_permissions_urlFieldHidden.js]
 [browser_proxy_backup.js]
 [browser_privacypane_1.js]
 [browser_privacypane_3.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/preferences/in-content/tests/browser_defaultbrowser_alwayscheck.js
@@ -0,0 +1,103 @@
+"use strict";
+
+const CHECK_DEFAULT_INITIAL = Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser");
+
+add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() {
+  yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+
+  yield test_with_mock_shellservice({isDefault: false}, function*() {
+    let setDefaultPane = content.document.getElementById("setDefaultPane");
+    is(setDefaultPane.selectedIndex, "0",
+       "The 'make default' pane should be visible when not default");
+    let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+    is(alwaysCheck.checked, false, "Always Check is unchecked by default");
+    is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), false,
+       "alwaysCheck pref should be false by default in test runs");
+
+    let setDefaultButton = content.document.getElementById("setDefaultButton");
+    setDefaultButton.click();
+    content.window.gMainPane.updateSetDefaultBrowser();
+
+    yield ContentTaskUtils.waitForCondition(() => alwaysCheck.checked,
+      "'Always Check' checkbox should get checked after clicking the 'Set Default' button");
+
+    is(alwaysCheck.checked, true,
+       "Clicking 'Make Default' checks the 'Always Check' checkbox");
+    is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true,
+       "Checking the checkbox should set the pref to true");
+    is(alwaysCheck.disabled, true,
+       "'Always Check' checkbox is locked with default browser and alwaysCheck=true");
+    is(setDefaultPane.selectedIndex, "1",
+       "The 'make default' pane should not be visible when default");
+    is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true,
+       "checkDefaultBrowser pref is now enabled");
+  });
+
+  gBrowser.removeCurrentTab();
+  Services.prefs.clearUserPref("browser.shell.checkDefaultBrowser");
+});
+
+add_task(function* clicking_make_default_checks_alwaysCheck_checkbox() {
+  Services.prefs.lockPref("browser.shell.checkDefaultBrowser");
+  yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:preferences");
+
+  yield test_with_mock_shellservice({isDefault: false}, function*() {
+    let setDefaultPane = content.document.getElementById("setDefaultPane");
+    is(setDefaultPane.selectedIndex, "0",
+       "The 'make default' pane should be visible when not default");
+    let alwaysCheck = content.document.getElementById("alwaysCheckDefault");
+    is(alwaysCheck.disabled, true, "Always Check is disabled when locked");
+    is(alwaysCheck.checked, true,
+       "Always Check is checked because defaultPref is true and pref is locked");
+    is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true,
+       "alwaysCheck pref should ship with 'true' by default");
+
+    let setDefaultButton = content.document.getElementById("setDefaultButton");
+    setDefaultButton.click();
+    content.window.gMainPane.updateSetDefaultBrowser();
+
+    yield ContentTaskUtils.waitForCondition(() => setDefaultPane.selectedIndex == "1",
+      "Browser is now default");
+
+    is(alwaysCheck.checked, true,
+       "'Always Check' is still checked because it's locked");
+    is(alwaysCheck.disabled, true,
+       "'Always Check is disabled because it's locked");
+    is(Services.prefs.getBoolPref("browser.shell.checkDefaultBrowser"), true,
+       "The pref is locked and so doesn't get changed");
+  });
+
+  Services.prefs.unlockPref("browser.shell.checkDefaultBrowser");
+  gBrowser.removeCurrentTab();
+});
+
+registerCleanupFunction(function() {
+  Services.prefs.unlockPref("browser.shell.checkDefaultBrowser");
+  Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL);
+});
+
+function* test_with_mock_shellservice(options, testFn) {
+  yield ContentTask.spawn(gBrowser.selectedBrowser, options, function*(options) {
+    let doc = content.document;
+    let win = doc.defaultView;
+    win.oldShellService = win.getShellService();
+    let mockShellService = {
+      _isDefault: false,
+      isDefaultBrowser() {
+        return this._isDefault;
+      },
+      setDefaultBrowser() {
+        this._isDefault = true;
+      },
+    };
+    win.getShellService = function() {
+      return mockShellService;
+    }
+    mockShellService._isDefault = options.isDefault;
+    win.gMainPane.updateSetDefaultBrowser();
+  });
+
+  yield ContentTask.spawn(gBrowser.selectedBrowser, null, testFn);
+
+  Services.prefs.setBoolPref("browser.shell.checkDefaultBrowser", CHECK_DEFAULT_INITIAL);
+}
--- a/browser/extensions/pocket/bootstrap.js
+++ b/browser/extensions/pocket/bootstrap.js
@@ -463,28 +463,16 @@ var PocketOverlay = {
       this.updatePocketItemVisibility(win.document);
     }
   },
   onWidgetRemoved: function(aWidgetId, aArea, aPosition) {
     for (let win of allBrowserWindows()) {
       this.updatePocketItemVisibility(win.document);
     }
   },
-  onWidgetReset: function(aNode, aContainer) {
-    // CUI was reset and doesn't respect default area for API widgets, place our
-    // widget back to the default area
-    // initially place the button after the bookmarks button if it is in the UI
-    let widgets = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR);
-    let bmbtn = widgets.indexOf("bookmarks-menu-button");
-    if (bmbtn > -1) {
-      CustomizableUI.addWidgetToArea("pocket-button", CustomizableUI.AREA_NAVBAR, bmbtn + 1);
-    } else {
-      CustomizableUI.addWidgetToArea("pocket-button", CustomizableUI.AREA_NAVBAR);
-    }
-  },
   updatePocketItemVisibility: function(doc) {
     let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button");
     for (let prefix of ["panelMenu_", "menu_", "BMB_"]) {
       let element = doc.getElementById(prefix + "pocket");
       if (element) {
         element.hidden = hidden;
         doc.getElementById(prefix + "pocketSeparator").hidden = hidden;
       }
copy from devtools/client/framework/gDevTools.jsm
copy to devtools/client/framework/devtools-browser.js
--- a/devtools/client/framework/gDevTools.jsm
+++ b/devtools/client/framework/devtools-browser.js
@@ -1,563 +1,37 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
+const {Cc, Ci, Cu} = require("chrome");
+const Services = require("Services");
+const promise = require("promise");
+const {gDevTools} = require("./devtools");
 
-const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+// Load target and toolbox lazily as they need gDevTools to be fully initialized
+loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
+loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
 
-// Make most dependencies be reloadable so that the reload addon
-// can update all of them while keeping gDevTools.jsm as-is
-// Bug 1188405 is going to refactor this JSM into a commonjs module
-// so that it can be reloaded as other modules.
-let require, loader, promise, DefaultTools, DefaultThemes;
-let loadDependencies = () => {
-  let l = Cu.import("resource://devtools/shared/Loader.jsm", {});
-  require = l.require;
-  loader = l.loader;
-  promise = require("promise");
-  // Load target and toolbox lazily as they need gDevTools to be fully initialized
-  loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
-  loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
-
-  XPCOMUtils.defineLazyModuleGetter(this, "console",
-                                    "resource://gre/modules/Console.jsm");
-  XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
-                                    "resource:///modules/CustomizableUI.jsm");
-  loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
-  loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
-
-  let d = require("devtools/client/definitions");
-  DefaultTools = d.defaultTools;
-  DefaultThemes = d.defaultThemes;
-};
-loadDependencies();
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const Telemetry = require("devtools/client/shared/telemetry");
-const {JsonView} = require("devtools/client/jsonview/main");
-
-const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
-const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
-const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
-const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
-
-const FORBIDDEN_IDS = new Set(["toolbox", ""]);
-const MAX_ORDINAL = 99;
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+                                  "resource:///modules/CustomizableUI.jsm");
 
 const bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
 
 /**
- * DevTools is a class that represents a set of developer tools, it holds a
- * set of tools and keeps track of open toolboxes in the browser.
- */
-this.DevTools = function DevTools() {
-  this._tools = new Map();     // Map<toolId, tool>
-  this._themes = new Map();    // Map<themeId, theme>
-  this._toolboxes = new Map(); // Map<target, toolbox>
-  this._telemetry = new Telemetry();
-
-  // destroy() is an observer's handler so we need to preserve context.
-  this.destroy = this.destroy.bind(this);
-  this._teardown = this._teardown.bind(this);
-
-  // JSON Viewer for 'application/json' documents.
-  JsonView.initialize();
-
-  EventEmitter.decorate(this);
-
-  Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
-  Services.obs.addObserver(this.destroy, "quit-application", false);
-};
-
-DevTools.prototype = {
-  /**
-   * Register a new developer tool.
-   *
-   * A definition is a light object that holds different information about a
-   * developer tool. This object is not supposed to have any operational code.
-   * See it as a "manifest".
-   * The only actual code lives in the build() function, which will be used to
-   * start an instance of this tool.
-   *
-   * Each toolDefinition has the following properties:
-   * - id: Unique identifier for this tool (string|required)
-   * - visibilityswitch: Property name to allow us to hide this tool from the
-   *                     DevTools Toolbox.
-   *                     A falsy value indicates that it cannot be hidden.
-   * - icon: URL pointing to a graphic which will be used as the src for an
-   *         16x16 img tag (string|required)
-   * - invertIconForLightTheme: The icon can automatically have an inversion
-   *         filter applied (default is false).  All builtin tools are true, but
-   *         addons may omit this to prevent unwanted changes to the `icon`
-   *         image. filter: invert(1) is applied to the image (boolean|optional)
-   * - url: URL pointing to a XUL/XHTML document containing the user interface
-   *        (string|required)
-   * - label: Localized name for the tool to be displayed to the user
-   *          (string|required)
-   * - hideInOptions: Boolean indicating whether or not this tool should be
-                      shown in toolbox options or not. Defaults to false.
-   *                  (boolean)
-   * - build: Function that takes an iframe, which has been populated with the
-   *          markup from |url|, and also the toolbox containing the panel.
-   *          And returns an instance of ToolPanel (function|required)
-   */
-  registerTool: function DT_registerTool(toolDefinition) {
-    let toolId = toolDefinition.id;
-
-    if (!toolId || FORBIDDEN_IDS.has(toolId)) {
-      throw new Error("Invalid definition.id");
-    }
-
-    // Make sure that additional tools will always be able to be hidden.
-    // When being called from main.js, defaultTools has not yet been exported.
-    // But, we can assume that in this case, it is a default tool.
-    if (DefaultTools && DefaultTools.indexOf(toolDefinition) == -1) {
-      toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
-    }
-
-    this._tools.set(toolId, toolDefinition);
-
-    this.emit("tool-registered", toolId);
-  },
-
-  /**
-   * Removes all tools that match the given |toolId|
-   * Needed so that add-ons can remove themselves when they are deactivated
-   *
-   * @param {string|object} tool
-   *        Definition or the id of the tool to unregister. Passing the
-   *        tool id should be avoided as it is a temporary measure.
-   * @param {boolean} isQuitApplication
-   *        true to indicate that the call is due to app quit, so we should not
-   *        cause a cascade of costly events
-   */
-  unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
-    let toolId = null;
-    if (typeof tool == "string") {
-      toolId = tool;
-      tool = this._tools.get(tool);
-    }
-    else {
-      toolId = tool.id;
-    }
-    this._tools.delete(toolId);
-
-    if (!isQuitApplication) {
-      this.emit("tool-unregistered", tool);
-    }
-  },
-
-  /**
-   * Sorting function used for sorting tools based on their ordinals.
-   */
-  ordinalSort: function DT_ordinalSort(d1, d2) {
-    let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
-    let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
-    return o1 - o2;
-  },
-
-  getDefaultTools: function DT_getDefaultTools() {
-    return DefaultTools.sort(this.ordinalSort);
-  },
-
-  getAdditionalTools: function DT_getAdditionalTools() {
-    let tools = [];
-    for (let [key, value] of this._tools) {
-      if (DefaultTools.indexOf(value) == -1) {
-        tools.push(value);
-      }
-    }
-    return tools.sort(this.ordinalSort);
-  },
-
-  /**
-   * Get a tool definition if it exists and is enabled.
-   *
-   * @param {string} toolId
-   *        The id of the tool to show
-   *
-   * @return {ToolDefinition|null} tool
-   *         The ToolDefinition for the id or null.
-   */
-  getToolDefinition: function DT_getToolDefinition(toolId) {
-    let tool = this._tools.get(toolId);
-    if (!tool) {
-      return null;
-    } else if (!tool.visibilityswitch) {
-      return tool;
-    }
-
-    let enabled;
-    try {
-      enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
-    } catch (e) {
-      enabled = true;
-    }
-
-    return enabled ? tool : null;
-  },
-
-  /**
-   * Allow ToolBoxes to get at the list of tools that they should populate
-   * themselves with.
-   *
-   * @return {Map} tools
-   *         A map of the the tool definitions registered in this instance
-   */
-  getToolDefinitionMap: function DT_getToolDefinitionMap() {
-    let tools = new Map();
-
-    for (let [id, definition] of this._tools) {
-      if (this.getToolDefinition(id)) {
-        tools.set(id, definition);
-      }
-    }
-
-    return tools;
-  },
-
-  /**
-   * Tools have an inherent ordering that can't be represented in a Map so
-   * getToolDefinitionArray provides an alternative representation of the
-   * definitions sorted by ordinal value.
-   *
-   * @return {Array} tools
-   *         A sorted array of the tool definitions registered in this instance
-   */
-  getToolDefinitionArray: function DT_getToolDefinitionArray() {
-    let definitions = [];
-
-    for (let [id, definition] of this._tools) {
-      if (this.getToolDefinition(id)) {
-        definitions.push(definition);
-      }
-    }
-
-    return definitions.sort(this.ordinalSort);
-  },
-
-  /**
-   * Register a new theme for developer tools toolbox.
-   *
-   * A definition is a light object that holds various information about a
-   * theme.
-   *
-   * Each themeDefinition has the following properties:
-   * - id: Unique identifier for this theme (string|required)
-   * - label: Localized name for the theme to be displayed to the user
-   *          (string|required)
-   * - stylesheets: Array of URLs pointing to a CSS document(s) containing
-   *                the theme style rules (array|required)
-   * - classList: Array of class names identifying the theme within a document.
-   *              These names are set to document element when applying
-   *              the theme (array|required)
-   * - onApply: Function that is executed by the framework when the theme
-   *            is applied. The function takes the current iframe window
-   *            and the previous theme id as arguments (function)
-   * - onUnapply: Function that is executed by the framework when the theme
-   *            is unapplied. The function takes the current iframe window
-   *            and the new theme id as arguments (function)
-   */
-  registerTheme: function DT_registerTheme(themeDefinition) {
-    let themeId = themeDefinition.id;
-
-    if (!themeId) {
-      throw new Error("Invalid theme id");
-    }
-
-    if (this._themes.get(themeId)) {
-      throw new Error("Theme with the same id is already registered");
-    }
-
-    this._themes.set(themeId, themeDefinition);
-
-    this.emit("theme-registered", themeId);
-  },
-
-  /**
-   * Removes an existing theme from the list of registered themes.
-   * Needed so that add-ons can remove themselves when they are deactivated
-   *
-   * @param {string|object} theme
-   *        Definition or the id of the theme to unregister.
-   */
-  unregisterTheme: function DT_unregisterTheme(theme) {
-    let themeId = null;
-    if (typeof theme == "string") {
-      themeId = theme;
-      theme = this._themes.get(theme);
-    }
-    else {
-      themeId = theme.id;
-    }
-
-    let currTheme = Services.prefs.getCharPref("devtools.theme");
-
-    // Note that we can't check if `theme` is an item
-    // of `DefaultThemes` as we end up reloading definitions
-    // module and end up with different theme objects
-    let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
-
-    // Reset the theme if an extension theme that's currently applied
-    // is being removed.
-    // Ignore shutdown since addons get disabled during that time.
-    if (!Services.startup.shuttingDown &&
-        !isCoreTheme &&
-        theme.id == currTheme) {
-      Services.prefs.setCharPref("devtools.theme", "light");
-
-      let data = {
-        pref: "devtools.theme",
-        newValue: "light",
-        oldValue: currTheme
-      };
-
-      gDevTools.emit("pref-changed", data);
-
-      this.emit("theme-unregistered", theme);
-    }
-
-    this._themes.delete(themeId);
-  },
-
-  /**
-   * Get a theme definition if it exists.
-   *
-   * @param {string} themeId
-   *        The id of the theme
-   *
-   * @return {ThemeDefinition|null} theme
-   *         The ThemeDefinition for the id or null.
-   */
-  getThemeDefinition: function DT_getThemeDefinition(themeId) {
-    let theme = this._themes.get(themeId);
-    if (!theme) {
-      return null;
-    }
-    return theme;
-  },
-
-  /**
-   * Get map of registered themes.
-   *
-   * @return {Map} themes
-   *         A map of the the theme definitions registered in this instance
-   */
-  getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
-    let themes = new Map();
-
-    for (let [id, definition] of this._themes) {
-      if (this.getThemeDefinition(id)) {
-        themes.set(id, definition);
-      }
-    }
-
-    return themes;
-  },
-
-  /**
-   * Get registered themes definitions sorted by ordinal value.
-   *
-   * @return {Array} themes
-   *         A sorted array of the theme definitions registered in this instance
-   */
-  getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
-    let definitions = [];
-
-    for (let [id, definition] of this._themes) {
-      if (this.getThemeDefinition(id)) {
-        definitions.push(definition);
-      }
-    }
-
-    return definitions.sort(this.ordinalSort);
-  },
-
-  /**
-   * Show a Toolbox for a target (either by creating a new one, or if a toolbox
-   * already exists for the target, by bring to the front the existing one)
-   * If |toolId| is specified then the displayed toolbox will have the
-   * specified tool selected.
-   * If |hostType| is specified then the toolbox will be displayed using the
-   * specified HostType.
-   *
-   * @param {Target} target
-   *         The target the toolbox will debug
-   * @param {string} toolId
-   *        The id of the tool to show
-   * @param {Toolbox.HostType} hostType
-   *        The type of host (bottom, window, side)
-   * @param {object} hostOptions
-   *        Options for host specifically
-   *
-   * @return {Toolbox} toolbox
-   *        The toolbox that was opened
-   */
-  showToolbox: function(target, toolId, hostType, hostOptions) {
-    let deferred = promise.defer();
-
-    let toolbox = this._toolboxes.get(target);
-    if (toolbox) {
-
-      let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
-          toolbox.switchHost(hostType) :
-          promise.resolve(null);
-
-      if (toolId != null && toolbox.currentToolId != toolId) {
-        hostPromise = hostPromise.then(function() {
-          return toolbox.selectTool(toolId);
-        });
-      }
-
-      return hostPromise.then(function() {
-        toolbox.raise();
-        return toolbox;
-      });
-    }
-    else {
-      // No toolbox for target, create one
-      toolbox = new Toolbox(target, toolId, hostType, hostOptions);
-
-      this.emit("toolbox-created", toolbox);
-
-      this._toolboxes.set(target, toolbox);
-
-      toolbox.once("destroy", () => {
-        this.emit("toolbox-destroy", target);
-      });
-
-      toolbox.once("destroyed", () => {
-        this._toolboxes.delete(target);
-        this.emit("toolbox-destroyed", target);
-      });
-
-      // If toolId was passed in, it will already be selected before the
-      // open promise resolves.
-      toolbox.open().then(() => {
-        deferred.resolve(toolbox);
-        this.emit("toolbox-ready", toolbox);
-      });
-    }
-
-    return deferred.promise;
-  },
-
-  /**
-   * Return the toolbox for a given target.
-   *
-   * @param  {object} target
-   *         Target value e.g. the target that owns this toolbox
-   *
-   * @return {Toolbox} toolbox
-   *         The toolbox that is debugging the given target
-   */
-  getToolbox: function DT_getToolbox(target) {
-    return this._toolboxes.get(target);
-  },
-
-  /**
-   * Close the toolbox for a given target
-   *
-   * @return promise
-   *         This promise will resolve to false if no toolbox was found
-   *         associated to the target. true, if the toolbox was successfully
-   *         closed.
-   */
-  closeToolbox: function DT_closeToolbox(target) {
-    let toolbox = this._toolboxes.get(target);
-    if (toolbox == null) {
-      return promise.resolve(false);
-    }
-    return toolbox.destroy().then(() => true);
-  },
-
-  _pingTelemetry: function() {
-    let mean = function(arr) {
-      if (arr.length === 0) {
-        return 0;
-      }
-
-      let total = arr.reduce((a, b) => a + b);
-      return Math.ceil(total / arr.length);
-    };
-
-    let tabStats = gDevToolsBrowser._tabStats;
-    this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
-    this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
-    this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
-    this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
-  },
-
-  /**
-   * Called to tear down a tools provider.
-   */
-  _teardown: function DT_teardown() {
-    for (let [target, toolbox] of this._toolboxes) {
-      toolbox.destroy();
-    }
-  },
-
-  /**
-   * All browser windows have been closed, tidy up remaining objects.
-   */
-  destroy: function() {
-    Services.obs.removeObserver(this.destroy, "quit-application");
-    Services.obs.removeObserver(this._teardown, "devtools-unloaded");
-
-    for (let [key, tool] of this.getToolDefinitionMap()) {
-      this.unregisterTool(key, true);
-    }
-
-    JsonView.destroy();
-
-    this._pingTelemetry();
-    this._telemetry = null;
-
-    // Cleaning down the toolboxes: i.e.
-    //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
-    // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
-  },
-
-  // Force reloading dependencies if the loader happens to have reloaded
-  reload() {
-    loadDependencies();
-  },
-
-  /**
-   * Iterator that yields each of the toolboxes.
-   */
-  *[Symbol.iterator]() {
-    for (let toolbox of this._toolboxes) {
-      yield toolbox;
-    }
-  }
-};
-
-/**
- * gDevTools is a singleton that controls the Firefox Developer Tools.
- *
- * It is an instance of a DevTools class that holds a set of tools. It has the
- * same lifetime as the browser.
- */
-var gDevTools = new DevTools();
-this.gDevTools = gDevTools;
-
-/**
  * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
  * Firefox instance.
  */
-var gDevToolsBrowser = {
+var gDevToolsBrowser = exports.gDevToolsBrowser = {
   /**
    * A record of the windows whose menus we altered, so we can undo the changes
    * as the window is closed
    */
   _trackedBrowserWindows: new Set(),
 
   _tabStats: {
     peakOpen: 0,
@@ -566,16 +40,17 @@ var gDevToolsBrowser = {
     histPinned: []
   },
 
   /**
    * This function is for the benefit of Tools:DevToolbox in
    * browser/base/content/browser-sets.inc and should not be used outside
    * of there
    */
+  // used by browser-sets.inc, command
   toggleToolboxCommand: function(gBrowser) {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
 
     // If a toolbox exists, using toggle from the Main window :
     // - should close a docked toolbox
     // - should focus a windowed toolbox
     let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
@@ -667,16 +142,18 @@ var gDevToolsBrowser = {
    *   we open the toolbox and select the tool
    * - if the toolbox is open, and the targeted tool is not selected,
    *   we select it
    * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is NOT a window, we close the toolbox
    * - if the toolbox is open, and the targeted tool is selected,
    *   and the host is a window, we raise the toolbox window
    */
+  // Used when: - registering a new tool
+  //            - new xul window, to add menu items
   selectToolCommand: function(gBrowser, toolId) {
     let target = TargetFactory.forTab(gBrowser.selectedTab);
     let toolbox = gDevTools.getToolbox(target);
     let toolDefinition = gDevTools.getToolDefinition(toolId);
 
     if (toolbox &&
         (toolbox.currentToolId == toolId ||
           (toolId == "webconsole" && toolbox.splitConsole)))
@@ -698,23 +175,26 @@ var gDevToolsBrowser = {
         gDevTools.emit("select-tool-command", toolId);
       });
     }
   },
 
   /**
    * Open a tab to allow connects to a remote browser
    */
+   // Used by browser-sets.inc, command
   openConnectScreen: function(gBrowser) {
     gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
   },
 
   /**
    * Open WebIDE
    */
+   // Used by browser-sets.inc, command
+   //         itself, webide widget
   openWebIDE: function() {
     let win = Services.wm.getMostRecentWindow("devtools:webide");
     if (win) {
       win.focus();
     } else {
       Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
     }
   },
@@ -762,28 +242,30 @@ var gDevToolsBrowser = {
                 deferred.resolve(target);
               });
       });
     });
 
     return deferred.promise;
   },
 
+   // Used by browser-sets.inc, command
   openContentProcessToolbox: function () {
     this._getContentProcessTarget()
         .then(target => {
           // Display a new toolbox, in a new window, with debugger by default
           return gDevTools.showToolbox(target, "jsdebugger",
                                        Toolbox.HostType.WINDOW);
         });
   },
 
   /**
    * Install WebIDE widget
    */
+  // Used by itself
   installWebIDEWidget: function() {
     if (this.isWebIDEWidgetInstalled()) {
       return;
     }
 
     let defaultArea;
     if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
       defaultArea = CustomizableUI.AREA_NAVBAR;
@@ -821,26 +303,28 @@ var gDevToolsBrowser = {
       CustomizableUI.removeWidgetFromArea("webide-button");
     }
     CustomizableUI.destroyWidget("webide-button");
   },
 
   /**
    * Move WebIDE widget to the navbar
    */
+   // Used by webide.js
   moveWebIDEWidgetInNavbar: function() {
     CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
   },
 
   /**
    * 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
    */
+  // Used by browser.js
   registerBrowserWindow: function DT_registerBrowserWindow(win) {
     this.updateCommandAvailability(win);
     this.ensurePrefObserver();
     gDevToolsBrowser._trackedBrowserWindows.add(win);
     gDevToolsBrowser._addAllToolsToMenu(win.document);
 
     if (this._isFirebugInstalled()) {
       let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
@@ -1315,18 +799,16 @@ var gDevToolsBrowser = {
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
     Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
     Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
   },
 }
 
-this.gDevToolsBrowser = gDevToolsBrowser;
-
 gDevTools.on("tool-registered", function(ev, toolId) {
   let toolDefinition = gDevTools._tools.get(toolId);
   gDevToolsBrowser._addToolToWindows(toolDefinition);
 });
 
 gDevTools.on("tool-unregistered", function(ev, toolId) {
   if (typeof toolId != "string") {
     toolId = toolId.id;
@@ -1335,9 +817,12 @@ gDevTools.on("tool-unregistered", functi
 });
 
 gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
 gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
 
 Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
 
 // Load the browser devtools main module as the loader's main module.
+// This is done precisely here as main.js ends up dispatching the
+// tool-registered events we are listening in this module.
 loader.main("devtools/client/main");
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/devtools.js
@@ -0,0 +1,511 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const Services = require("Services");
+const promise = require("promise");
+
+// Load gDevToolsBrowser toolbox lazily as they need gDevTools to be fully initialized
+loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
+loader.lazyRequireGetter(this, "gDevToolsBrowser", "devtools/client/framework/devtools-browser", true);
+
+const {defaultTools: DefaultTools, defaultThemes: DefaultThemes} =
+  require("devtools/client/definitions");
+const EventEmitter = require("devtools/shared/event-emitter");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {JsonView} = require("devtools/client/jsonview/main");
+
+const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
+const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
+const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
+const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
+
+const FORBIDDEN_IDS = new Set(["toolbox", ""]);
+const MAX_ORDINAL = 99;
+
+/**
+ * DevTools is a class that represents a set of developer tools, it holds a
+ * set of tools and keeps track of open toolboxes in the browser.
+ */
+this.DevTools = function DevTools() {
+  this._tools = new Map();     // Map<toolId, tool>
+  this._themes = new Map();    // Map<themeId, theme>
+  this._toolboxes = new Map(); // Map<target, toolbox>
+  this._telemetry = new Telemetry();
+
+  // destroy() is an observer's handler so we need to preserve context.
+  this.destroy = this.destroy.bind(this);
+  this._teardown = this._teardown.bind(this);
+
+  // JSON Viewer for 'application/json' documents.
+  JsonView.initialize();
+
+  EventEmitter.decorate(this);
+
+  Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
+  Services.obs.addObserver(this.destroy, "quit-application", false);
+};
+
+DevTools.prototype = {
+  /**
+   * Register a new developer tool.
+   *
+   * A definition is a light object that holds different information about a
+   * developer tool. This object is not supposed to have any operational code.
+   * See it as a "manifest".
+   * The only actual code lives in the build() function, which will be used to
+   * start an instance of this tool.
+   *
+   * Each toolDefinition has the following properties:
+   * - id: Unique identifier for this tool (string|required)
+   * - visibilityswitch: Property name to allow us to hide this tool from the
+   *                     DevTools Toolbox.
+   *                     A falsy value indicates that it cannot be hidden.
+   * - icon: URL pointing to a graphic which will be used as the src for an
+   *         16x16 img tag (string|required)
+   * - invertIconForLightTheme: The icon can automatically have an inversion
+   *         filter applied (default is false).  All builtin tools are true, but
+   *         addons may omit this to prevent unwanted changes to the `icon`
+   *         image. filter: invert(1) is applied to the image (boolean|optional)
+   * - url: URL pointing to a XUL/XHTML document containing the user interface
+   *        (string|required)
+   * - label: Localized name for the tool to be displayed to the user
+   *          (string|required)
+   * - hideInOptions: Boolean indicating whether or not this tool should be
+                      shown in toolbox options or not. Defaults to false.
+   *                  (boolean)
+   * - build: Function that takes an iframe, which has been populated with the
+   *          markup from |url|, and also the toolbox containing the panel.
+   *          And returns an instance of ToolPanel (function|required)
+   */
+  registerTool: function DT_registerTool(toolDefinition) {
+    let toolId = toolDefinition.id;
+
+    if (!toolId || FORBIDDEN_IDS.has(toolId)) {
+      throw new Error("Invalid definition.id");
+    }
+
+    // Make sure that additional tools will always be able to be hidden.
+    // When being called from main.js, defaultTools has not yet been exported.
+    // But, we can assume that in this case, it is a default tool.
+    if (DefaultTools && DefaultTools.indexOf(toolDefinition) == -1) {
+      toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
+    }
+
+    this._tools.set(toolId, toolDefinition);
+
+    this.emit("tool-registered", toolId);
+  },
+
+  /**
+   * Removes all tools that match the given |toolId|
+   * Needed so that add-ons can remove themselves when they are deactivated
+   *
+   * @param {string|object} tool
+   *        Definition or the id of the tool to unregister. Passing the
+   *        tool id should be avoided as it is a temporary measure.
+   * @param {boolean} isQuitApplication
+   *        true to indicate that the call is due to app quit, so we should not
+   *        cause a cascade of costly events
+   */
+  unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
+    let toolId = null;
+    if (typeof tool == "string") {
+      toolId = tool;
+      tool = this._tools.get(tool);
+    }
+    else {
+      toolId = tool.id;
+    }
+    this._tools.delete(toolId);
+
+    if (!isQuitApplication) {
+      this.emit("tool-unregistered", tool);
+    }
+  },
+
+  /**
+   * Sorting function used for sorting tools based on their ordinals.
+   */
+  ordinalSort: function DT_ordinalSort(d1, d2) {
+    let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
+    let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
+    return o1 - o2;
+  },
+
+  getDefaultTools: function DT_getDefaultTools() {
+    return DefaultTools.sort(this.ordinalSort);
+  },
+
+  getAdditionalTools: function DT_getAdditionalTools() {
+    let tools = [];
+    for (let [key, value] of this._tools) {
+      if (DefaultTools.indexOf(value) == -1) {
+        tools.push(value);
+      }
+    }
+    return tools.sort(this.ordinalSort);
+  },
+
+  /**
+   * Get a tool definition if it exists and is enabled.
+   *
+   * @param {string} toolId
+   *        The id of the tool to show
+   *
+   * @return {ToolDefinition|null} tool
+   *         The ToolDefinition for the id or null.
+   */
+  getToolDefinition: function DT_getToolDefinition(toolId) {
+    let tool = this._tools.get(toolId);
+    if (!tool) {
+      return null;
+    } else if (!tool.visibilityswitch) {
+      return tool;
+    }
+
+    let enabled;
+    try {
+      enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
+    } catch (e) {
+      enabled = true;
+    }
+
+    return enabled ? tool : null;
+  },
+
+  /**
+   * Allow ToolBoxes to get at the list of tools that they should populate
+   * themselves with.
+   *
+   * @return {Map} tools
+   *         A map of the the tool definitions registered in this instance
+   */
+  getToolDefinitionMap: function DT_getToolDefinitionMap() {
+    let tools = new Map();
+
+    for (let [id, definition] of this._tools) {
+      if (this.getToolDefinition(id)) {
+        tools.set(id, definition);
+      }
+    }
+
+    return tools;
+  },
+
+  /**
+   * Tools have an inherent ordering that can't be represented in a Map so
+   * getToolDefinitionArray provides an alternative representation of the
+   * definitions sorted by ordinal value.
+   *
+   * @return {Array} tools
+   *         A sorted array of the tool definitions registered in this instance
+   */
+  getToolDefinitionArray: function DT_getToolDefinitionArray() {
+    let definitions = [];
+
+    for (let [id, definition] of this._tools) {
+      if (this.getToolDefinition(id)) {
+        definitions.push(definition);
+      }
+    }
+
+    return definitions.sort(this.ordinalSort);
+  },
+
+  /**
+   * Register a new theme for developer tools toolbox.
+   *
+   * A definition is a light object that holds various information about a
+   * theme.
+   *
+   * Each themeDefinition has the following properties:
+   * - id: Unique identifier for this theme (string|required)
+   * - label: Localized name for the theme to be displayed to the user
+   *          (string|required)
+   * - stylesheets: Array of URLs pointing to a CSS document(s) containing
+   *                the theme style rules (array|required)
+   * - classList: Array of class names identifying the theme within a document.
+   *              These names are set to document element when applying
+   *              the theme (array|required)
+   * - onApply: Function that is executed by the framework when the theme
+   *            is applied. The function takes the current iframe window
+   *            and the previous theme id as arguments (function)
+   * - onUnapply: Function that is executed by the framework when the theme
+   *            is unapplied. The function takes the current iframe window
+   *            and the new theme id as arguments (function)
+   */
+  registerTheme: function DT_registerTheme(themeDefinition) {
+    let themeId = themeDefinition.id;
+
+    if (!themeId) {
+      throw new Error("Invalid theme id");
+    }
+
+    if (this._themes.get(themeId)) {
+      throw new Error("Theme with the same id is already registered");
+    }
+
+    this._themes.set(themeId, themeDefinition);
+
+    this.emit("theme-registered", themeId);
+  },
+
+  /**
+   * Removes an existing theme from the list of registered themes.
+   * Needed so that add-ons can remove themselves when they are deactivated
+   *
+   * @param {string|object} theme
+   *        Definition or the id of the theme to unregister.
+   */
+  unregisterTheme: function DT_unregisterTheme(theme) {
+    let themeId = null;
+    if (typeof theme == "string") {
+      themeId = theme;
+      theme = this._themes.get(theme);
+    }
+    else {
+      themeId = theme.id;
+    }
+
+    let currTheme = Services.prefs.getCharPref("devtools.theme");
+
+    // Note that we can't check if `theme` is an item
+    // of `DefaultThemes` as we end up reloading definitions
+    // module and end up with different theme objects
+    let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
+
+    // Reset the theme if an extension theme that's currently applied
+    // is being removed.
+    // Ignore shutdown since addons get disabled during that time.
+    if (!Services.startup.shuttingDown &&
+        !isCoreTheme &&
+        theme.id == currTheme) {
+      Services.prefs.setCharPref("devtools.theme", "light");
+
+      let data = {
+        pref: "devtools.theme",
+        newValue: "light",
+        oldValue: currTheme
+      };
+
+      this.emit("pref-changed", data);
+
+      this.emit("theme-unregistered", theme);
+    }
+
+    this._themes.delete(themeId);
+  },
+
+  /**
+   * Get a theme definition if it exists.
+   *
+   * @param {string} themeId
+   *        The id of the theme
+   *
+   * @return {ThemeDefinition|null} theme
+   *         The ThemeDefinition for the id or null.
+   */
+  getThemeDefinition: function DT_getThemeDefinition(themeId) {
+    let theme = this._themes.get(themeId);
+    if (!theme) {
+      return null;
+    }
+    return theme;
+  },
+
+  /**
+   * Get map of registered themes.
+   *
+   * @return {Map} themes
+   *         A map of the the theme definitions registered in this instance
+   */
+  getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
+    let themes = new Map();
+
+    for (let [id, definition] of this._themes) {
+      if (this.getThemeDefinition(id)) {
+        themes.set(id, definition);
+      }
+    }
+
+    return themes;
+  },
+
+  /**
+   * Get registered themes definitions sorted by ordinal value.
+   *
+   * @return {Array} themes
+   *         A sorted array of the theme definitions registered in this instance
+   */
+  getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
+    let definitions = [];
+
+    for (let [id, definition] of this._themes) {
+      if (this.getThemeDefinition(id)) {
+        definitions.push(definition);
+      }
+    }
+
+    return definitions.sort(this.ordinalSort);
+  },
+
+  /**
+   * Show a Toolbox for a target (either by creating a new one, or if a toolbox
+   * already exists for the target, by bring to the front the existing one)
+   * If |toolId| is specified then the displayed toolbox will have the
+   * specified tool selected.
+   * If |hostType| is specified then the toolbox will be displayed using the
+   * specified HostType.
+   *
+   * @param {Target} target
+   *         The target the toolbox will debug
+   * @param {string} toolId
+   *        The id of the tool to show
+   * @param {Toolbox.HostType} hostType
+   *        The type of host (bottom, window, side)
+   * @param {object} hostOptions
+   *        Options for host specifically
+   *
+   * @return {Toolbox} toolbox
+   *        The toolbox that was opened
+   */
+  showToolbox: function(target, toolId, hostType, hostOptions) {
+    let deferred = promise.defer();
+
+    let toolbox = this._toolboxes.get(target);
+    if (toolbox) {
+
+      let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
+          toolbox.switchHost(hostType) :
+          promise.resolve(null);
+
+      if (toolId != null && toolbox.currentToolId != toolId) {
+        hostPromise = hostPromise.then(function() {
+          return toolbox.selectTool(toolId);
+        });
+      }
+
+      return hostPromise.then(function() {
+        toolbox.raise();
+        return toolbox;
+      });
+    }
+    else {
+      // No toolbox for target, create one
+      toolbox = new Toolbox(target, toolId, hostType, hostOptions);
+
+      this.emit("toolbox-created", toolbox);
+
+      this._toolboxes.set(target, toolbox);
+
+      toolbox.once("destroy", () => {
+        this.emit("toolbox-destroy", target);
+      });
+
+      toolbox.once("destroyed", () => {
+        this._toolboxes.delete(target);
+        this.emit("toolbox-destroyed", target);
+      });
+
+      // If toolId was passed in, it will already be selected before the
+      // open promise resolves.
+      toolbox.open().then(() => {
+        deferred.resolve(toolbox);
+        this.emit("toolbox-ready", toolbox);
+      });
+    }
+
+    return deferred.promise;
+  },
+
+  /**
+   * Return the toolbox for a given target.
+   *
+   * @param  {object} target
+   *         Target value e.g. the target that owns this toolbox
+   *
+   * @return {Toolbox} toolbox
+   *         The toolbox that is debugging the given target
+   */
+  getToolbox: function DT_getToolbox(target) {
+    return this._toolboxes.get(target);
+  },
+
+  /**
+   * Close the toolbox for a given target
+   *
+   * @return promise
+   *         This promise will resolve to false if no toolbox was found
+   *         associated to the target. true, if the toolbox was successfully
+   *         closed.
+   */
+  closeToolbox: function DT_closeToolbox(target) {
+    let toolbox = this._toolboxes.get(target);
+    if (toolbox == null) {
+      return promise.resolve(false);
+    }
+    return toolbox.destroy().then(() => true);
+  },
+
+  _pingTelemetry: function() {
+    let mean = function(arr) {
+      if (arr.length === 0) {
+        return 0;
+      }
+
+      let total = arr.reduce((a, b) => a + b);
+      return Math.ceil(total / arr.length);
+    };
+
+    let tabStats = gDevToolsBrowser._tabStats;
+    this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
+    this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
+    this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
+    this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
+  },
+
+  /**
+   * Called to tear down a tools provider.
+   */
+  _teardown: function DT_teardown() {
+    for (let [target, toolbox] of this._toolboxes) {
+      toolbox.destroy();
+    }
+  },
+
+  /**
+   * All browser windows have been closed, tidy up remaining objects.
+   */
+  destroy: function() {
+    Services.obs.removeObserver(this.destroy, "quit-application");
+    Services.obs.removeObserver(this._teardown, "devtools-unloaded");
+
+    for (let [key, tool] of this.getToolDefinitionMap()) {
+      this.unregisterTool(key, true);
+    }
+
+    JsonView.destroy();
+
+    this._pingTelemetry();
+    this._telemetry = null;
+
+    // Cleaning down the toolboxes: i.e.
+    //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
+    // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
+  },
+
+  /**
+   * Iterator that yields each of the toolboxes.
+   */
+  *[Symbol.iterator]() {
+    for (let toolbox of this._toolboxes) {
+      yield toolbox;
+    }
+  }
+};
+
+exports.gDevTools = new DevTools();
+
--- a/devtools/client/framework/gDevTools.jsm
+++ b/devtools/client/framework/gDevTools.jsm
@@ -1,1343 +1,163 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.EXPORTED_SYMBOLS = [ "gDevTools", "DevTools", "gDevToolsBrowser" ];
+/**
+ * This JSM is here to keep some compatibility with existing add-ons.
+ * Please now use the modules:
+ * - devtools/client/framework/devtools for gDevTools
+ * - devtools/client/framework/devtools-browser for gDevToolsBrowser
+ *
+ * We still do use gDevTools.jsm in our codebase,
+ * bug 1245462 is going to ensure we no longer do that.
+ */
+
+this.EXPORTED_SYMBOLS = [ "gDevTools", "gDevToolsBrowser" ];
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-// Make most dependencies be reloadable so that the reload addon
-// can update all of them while keeping gDevTools.jsm as-is
-// Bug 1188405 is going to refactor this JSM into a commonjs module
-// so that it can be reloaded as other modules.
-let require, loader, promise, DefaultTools, DefaultThemes;
-let loadDependencies = () => {
-  let l = Cu.import("resource://devtools/shared/Loader.jsm", {});
-  require = l.require;
-  loader = l.loader;
-  promise = require("promise");
-  // Load target and toolbox lazily as they need gDevTools to be fully initialized
-  loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
-  loader.lazyRequireGetter(this, "Toolbox", "devtools/client/framework/toolbox", true);
-
-  XPCOMUtils.defineLazyModuleGetter(this, "console",
-                                    "resource://gre/modules/Console.jsm");
-  XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
-                                    "resource:///modules/CustomizableUI.jsm");
-  loader.lazyRequireGetter(this, "DebuggerServer", "devtools/server/main", true);
-  loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true);
-
-  let d = require("devtools/client/definitions");
-  DefaultTools = d.defaultTools;
-  DefaultThemes = d.defaultThemes;
-};
-loadDependencies();
-
-const EventEmitter = require("devtools/shared/event-emitter");
-const Telemetry = require("devtools/client/shared/telemetry");
-const {JsonView} = require("devtools/client/jsonview/main");
-
-const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
-const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
-const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
-const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
-
-const FORBIDDEN_IDS = new Set(["toolbox", ""]);
-const MAX_ORDINAL = 99;
-
-const bundle = Services.strings.createBundle("chrome://devtools/locale/toolbox.properties");
+const { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 
 /**
- * DevTools is a class that represents a set of developer tools, it holds a
- * set of tools and keeps track of open toolboxes in the browser.
+ * Do not directly map to the commonjs modules so that callsites of
+ * gDevTools.jsm do not have to do anything to access to the very last version
+ * of the module. The `devtools` and `browser` getter are always going to
+ * retrieve the very last version of the modules.
  */
-this.DevTools = function DevTools() {
-  this._tools = new Map();     // Map<toolId, tool>
-  this._themes = new Map();    // Map<themeId, theme>
-  this._toolboxes = new Map(); // Map<target, toolbox>
-  this._telemetry = new Telemetry();
-
-  // destroy() is an observer's handler so we need to preserve context.
-  this.destroy = this.destroy.bind(this);
-  this._teardown = this._teardown.bind(this);
-
-  // JSON Viewer for 'application/json' documents.
-  JsonView.initialize();
-
-  EventEmitter.decorate(this);
-
-  Services.obs.addObserver(this._teardown, "devtools-unloaded", false);
-  Services.obs.addObserver(this.destroy, "quit-application", false);
-};
-
-DevTools.prototype = {
-  /**
-   * Register a new developer tool.
-   *
-   * A definition is a light object that holds different information about a
-   * developer tool. This object is not supposed to have any operational code.
-   * See it as a "manifest".
-   * The only actual code lives in the build() function, which will be used to
-   * start an instance of this tool.
-   *
-   * Each toolDefinition has the following properties:
-   * - id: Unique identifier for this tool (string|required)
-   * - visibilityswitch: Property name to allow us to hide this tool from the
-   *                     DevTools Toolbox.
-   *                     A falsy value indicates that it cannot be hidden.
-   * - icon: URL pointing to a graphic which will be used as the src for an
-   *         16x16 img tag (string|required)
-   * - invertIconForLightTheme: The icon can automatically have an inversion
-   *         filter applied (default is false).  All builtin tools are true, but
-   *         addons may omit this to prevent unwanted changes to the `icon`
-   *         image. filter: invert(1) is applied to the image (boolean|optional)
-   * - url: URL pointing to a XUL/XHTML document containing the user interface
-   *        (string|required)
-   * - label: Localized name for the tool to be displayed to the user
-   *          (string|required)
-   * - hideInOptions: Boolean indicating whether or not this tool should be
-                      shown in toolbox options or not. Defaults to false.
-   *                  (boolean)
-   * - build: Function that takes an iframe, which has been populated with the
-   *          markup from |url|, and also the toolbox containing the panel.
-   *          And returns an instance of ToolPanel (function|required)
-   */
-  registerTool: function DT_registerTool(toolDefinition) {
-    let toolId = toolDefinition.id;
-
-    if (!toolId || FORBIDDEN_IDS.has(toolId)) {
-      throw new Error("Invalid definition.id");
-    }
-
-    // Make sure that additional tools will always be able to be hidden.
-    // When being called from main.js, defaultTools has not yet been exported.
-    // But, we can assume that in this case, it is a default tool.
-    if (DefaultTools && DefaultTools.indexOf(toolDefinition) == -1) {
-      toolDefinition.visibilityswitch = "devtools." + toolId + ".enabled";
-    }
-
-    this._tools.set(toolId, toolDefinition);
-
-    this.emit("tool-registered", toolId);
-  },
-
-  /**
-   * Removes all tools that match the given |toolId|
-   * Needed so that add-ons can remove themselves when they are deactivated
-   *
-   * @param {string|object} tool
-   *        Definition or the id of the tool to unregister. Passing the
-   *        tool id should be avoided as it is a temporary measure.
-   * @param {boolean} isQuitApplication
-   *        true to indicate that the call is due to app quit, so we should not
-   *        cause a cascade of costly events
-   */
-  unregisterTool: function DT_unregisterTool(tool, isQuitApplication) {
-    let toolId = null;
-    if (typeof tool == "string") {
-      toolId = tool;
-      tool = this._tools.get(tool);
-    }
-    else {
-      toolId = tool.id;
-    }
-    this._tools.delete(toolId);
-
-    if (!isQuitApplication) {
-      this.emit("tool-unregistered", tool);
-    }
-  },
-
-  /**
-   * Sorting function used for sorting tools based on their ordinals.
-   */
-  ordinalSort: function DT_ordinalSort(d1, d2) {
-    let o1 = (typeof d1.ordinal == "number") ? d1.ordinal : MAX_ORDINAL;
-    let o2 = (typeof d2.ordinal == "number") ? d2.ordinal : MAX_ORDINAL;
-    return o1 - o2;
-  },
-
-  getDefaultTools: function DT_getDefaultTools() {
-    return DefaultTools.sort(this.ordinalSort);
-  },
-
-  getAdditionalTools: function DT_getAdditionalTools() {
-    let tools = [];
-    for (let [key, value] of this._tools) {
-      if (DefaultTools.indexOf(value) == -1) {
-        tools.push(value);
-      }
-    }
-    return tools.sort(this.ordinalSort);
-  },
-
-  /**
-   * Get a tool definition if it exists and is enabled.
-   *
-   * @param {string} toolId
-   *        The id of the tool to show
-   *
-   * @return {ToolDefinition|null} tool
-   *         The ToolDefinition for the id or null.
-   */
-  getToolDefinition: function DT_getToolDefinition(toolId) {
-    let tool = this._tools.get(toolId);
-    if (!tool) {
-      return null;
-    } else if (!tool.visibilityswitch) {
-      return tool;
-    }
-
-    let enabled;
-    try {
-      enabled = Services.prefs.getBoolPref(tool.visibilityswitch);
-    } catch (e) {
-      enabled = true;
-    }
-
-    return enabled ? tool : null;
-  },
-
-  /**
-   * Allow ToolBoxes to get at the list of tools that they should populate
-   * themselves with.
-   *
-   * @return {Map} tools
-   *         A map of the the tool definitions registered in this instance
-   */
-  getToolDefinitionMap: function DT_getToolDefinitionMap() {
-    let tools = new Map();
-
-    for (let [id, definition] of this._tools) {
-      if (this.getToolDefinition(id)) {
-        tools.set(id, definition);
-      }
-    }
-
-    return tools;
-  },
-
-  /**
-   * Tools have an inherent ordering that can't be represented in a Map so
-   * getToolDefinitionArray provides an alternative representation of the
-   * definitions sorted by ordinal value.
-   *
-   * @return {Array} tools
-   *         A sorted array of the tool definitions registered in this instance
-   */
-  getToolDefinitionArray: function DT_getToolDefinitionArray() {
-    let definitions = [];
-
-    for (let [id, definition] of this._tools) {
-      if (this.getToolDefinition(id)) {
-        definitions.push(definition);
-      }
-    }
-
-    return definitions.sort(this.ordinalSort);
-  },
-
-  /**
-   * Register a new theme for developer tools toolbox.
-   *
-   * A definition is a light object that holds various information about a
-   * theme.
-   *
-   * Each themeDefinition has the following properties:
-   * - id: Unique identifier for this theme (string|required)
-   * - label: Localized name for the theme to be displayed to the user
-   *          (string|required)
-   * - stylesheets: Array of URLs pointing to a CSS document(s) containing
-   *                the theme style rules (array|required)
-   * - classList: Array of class names identifying the theme within a document.
-   *              These names are set to document element when applying
-   *              the theme (array|required)
-   * - onApply: Function that is executed by the framework when the theme
-   *            is applied. The function takes the current iframe window
-   *            and the previous theme id as arguments (function)
-   * - onUnapply: Function that is executed by the framework when the theme
-   *            is unapplied. The function takes the current iframe window
-   *            and the new theme id as arguments (function)
-   */
-  registerTheme: function DT_registerTheme(themeDefinition) {
-    let themeId = themeDefinition.id;
-
-    if (!themeId) {
-      throw new Error("Invalid theme id");
-    }
-
-    if (this._themes.get(themeId)) {
-      throw new Error("Theme with the same id is already registered");
-    }
-
-    this._themes.set(themeId, themeDefinition);
-
-    this.emit("theme-registered", themeId);
-  },
-
-  /**
-   * Removes an existing theme from the list of registered themes.
-   * Needed so that add-ons can remove themselves when they are deactivated
-   *
-   * @param {string|object} theme
-   *        Definition or the id of the theme to unregister.
-   */
-  unregisterTheme: function DT_unregisterTheme(theme) {
-    let themeId = null;
-    if (typeof theme == "string") {
-      themeId = theme;
-      theme = this._themes.get(theme);
-    }
-    else {
-      themeId = theme.id;
-    }
-
-    let currTheme = Services.prefs.getCharPref("devtools.theme");
-
-    // Note that we can't check if `theme` is an item
-    // of `DefaultThemes` as we end up reloading definitions
-    // module and end up with different theme objects
-    let isCoreTheme = DefaultThemes.some(t => t.id === themeId);
-
-    // Reset the theme if an extension theme that's currently applied
-    // is being removed.
-    // Ignore shutdown since addons get disabled during that time.
-    if (!Services.startup.shuttingDown &&
-        !isCoreTheme &&
-        theme.id == currTheme) {
-      Services.prefs.setCharPref("devtools.theme", "light");
-
-      let data = {
-        pref: "devtools.theme",
-        newValue: "light",
-        oldValue: currTheme
-      };
-
-      gDevTools.emit("pref-changed", data);
-
-      this.emit("theme-unregistered", theme);
-    }
-
-    this._themes.delete(themeId);
-  },
-
-  /**
-   * Get a theme definition if it exists.
-   *
-   * @param {string} themeId
-   *        The id of the theme
-   *
-   * @return {ThemeDefinition|null} theme
-   *         The ThemeDefinition for the id or null.
-   */
-  getThemeDefinition: function DT_getThemeDefinition(themeId) {
-    let theme = this._themes.get(themeId);
-    if (!theme) {
-      return null;
-    }
-    return theme;
-  },
-
-  /**
-   * Get map of registered themes.
-   *
-   * @return {Map} themes
-   *         A map of the the theme definitions registered in this instance
-   */
-  getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
-    let themes = new Map();
-
-    for (let [id, definition] of this._themes) {
-      if (this.getThemeDefinition(id)) {
-        themes.set(id, definition);
-      }
-    }
-
-    return themes;
-  },
-
-  /**
-   * Get registered themes definitions sorted by ordinal value.
-   *
-   * @return {Array} themes
-   *         A sorted array of the theme definitions registered in this instance
-   */
-  getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
-    let definitions = [];
-
-    for (let [id, definition] of this._themes) {
-      if (this.getThemeDefinition(id)) {
-        definitions.push(definition);
-      }
-    }
-
-    return definitions.sort(this.ordinalSort);
-  },
-
-  /**
-   * Show a Toolbox for a target (either by creating a new one, or if a toolbox
-   * already exists for the target, by bring to the front the existing one)
-   * If |toolId| is specified then the displayed toolbox will have the
-   * specified tool selected.
-   * If |hostType| is specified then the toolbox will be displayed using the
-   * specified HostType.
-   *
-   * @param {Target} target
-   *         The target the toolbox will debug
-   * @param {string} toolId
-   *        The id of the tool to show
-   * @param {Toolbox.HostType} hostType
-   *        The type of host (bottom, window, side)
-   * @param {object} hostOptions
-   *        Options for host specifically
-   *
-   * @return {Toolbox} toolbox
-   *        The toolbox that was opened
-   */
-  showToolbox: function(target, toolId, hostType, hostOptions) {
-    let deferred = promise.defer();
-
-    let toolbox = this._toolboxes.get(target);
-    if (toolbox) {
-
-      let hostPromise = (hostType != null && toolbox.hostType != hostType) ?
-          toolbox.switchHost(hostType) :
-          promise.resolve(null);
-
-      if (toolId != null && toolbox.currentToolId != toolId) {
-        hostPromise = hostPromise.then(function() {
-          return toolbox.selectTool(toolId);
-        });
-      }
-
-      return hostPromise.then(function() {
-        toolbox.raise();
-        return toolbox;
-      });
-    }
-    else {
-      // No toolbox for target, create one
-      toolbox = new Toolbox(target, toolId, hostType, hostOptions);
-
-      this.emit("toolbox-created", toolbox);
-
-      this._toolboxes.set(target, toolbox);
-
-      toolbox.once("destroy", () => {
-        this.emit("toolbox-destroy", target);
-      });
-
-      toolbox.once("destroyed", () => {
-        this._toolboxes.delete(target);
-        this.emit("toolbox-destroyed", target);
-      });
-
-      // If toolId was passed in, it will already be selected before the
-      // open promise resolves.
-      toolbox.open().then(() => {
-        deferred.resolve(toolbox);
-        this.emit("toolbox-ready", toolbox);
-      });
-    }
-
-    return deferred.promise;
-  },
-
-  /**
-   * Return the toolbox for a given target.
-   *
-   * @param  {object} target
-   *         Target value e.g. the target that owns this toolbox
-   *
-   * @return {Toolbox} toolbox
-   *         The toolbox that is debugging the given target
-   */
-  getToolbox: function DT_getToolbox(target) {
-    return this._toolboxes.get(target);
-  },
-
-  /**
-   * Close the toolbox for a given target
-   *
-   * @return promise
-   *         This promise will resolve to false if no toolbox was found
-   *         associated to the target. true, if the toolbox was successfully
-   *         closed.
-   */
-  closeToolbox: function DT_closeToolbox(target) {
-    let toolbox = this._toolboxes.get(target);
-    if (toolbox == null) {
-      return promise.resolve(false);
-    }
-    return toolbox.destroy().then(() => true);
-  },
-
-  _pingTelemetry: function() {
-    let mean = function(arr) {
-      if (arr.length === 0) {
-        return 0;
-      }
-
-      let total = arr.reduce((a, b) => a + b);
-      return Math.ceil(total / arr.length);
-    };
-
-    let tabStats = gDevToolsBrowser._tabStats;
-    this._telemetry.log(TABS_OPEN_PEAK_HISTOGRAM, tabStats.peakOpen);
-    this._telemetry.log(TABS_OPEN_AVG_HISTOGRAM, mean(tabStats.histOpen));
-    this._telemetry.log(TABS_PINNED_PEAK_HISTOGRAM, tabStats.peakPinned);
-    this._telemetry.log(TABS_PINNED_AVG_HISTOGRAM, mean(tabStats.histPinned));
-  },
-
-  /**
-   * Called to tear down a tools provider.
-   */
-  _teardown: function DT_teardown() {
-    for (let [target, toolbox] of this._toolboxes) {
-      toolbox.destroy();
-    }
-  },
-
-  /**
-   * All browser windows have been closed, tidy up remaining objects.
-   */
-  destroy: function() {
-    Services.obs.removeObserver(this.destroy, "quit-application");
-    Services.obs.removeObserver(this._teardown, "devtools-unloaded");
-
-    for (let [key, tool] of this.getToolDefinitionMap()) {
-      this.unregisterTool(key, true);
-    }
-
-    JsonView.destroy();
-
-    this._pingTelemetry();
-    this._telemetry = null;
-
-    // Cleaning down the toolboxes: i.e.
-    //   for (let [target, toolbox] of this._toolboxes) toolbox.destroy();
-    // Is taken care of by the gDevToolsBrowser.forgetBrowserWindow
-  },
-
-  // Force reloading dependencies if the loader happens to have reloaded
-  reload() {
-    loadDependencies();
-  },
-
-  /**
-   * Iterator that yields each of the toolboxes.
-   */
-  *[Symbol.iterator]() {
-    for (let toolbox of this._toolboxes) {
-      yield toolbox;
-    }
+Object.defineProperty(this, "require", {
+  get() {
+    let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+    return require;
   }
-};
+});
+Object.defineProperty(this, "devtools", {
+  get() {
+    return require("devtools/client/framework/devtools").gDevTools;
+  }
+});
+Object.defineProperty(this, "browser", {
+  get() {
+    return require("devtools/client/framework/devtools-browser").gDevToolsBrowser;
+  }
+});
 
 /**
  * gDevTools is a singleton that controls the Firefox Developer Tools.
  *
  * It is an instance of a DevTools class that holds a set of tools. It has the
  * same lifetime as the browser.
  */
-var gDevTools = new DevTools();
-this.gDevTools = gDevTools;
+let gDevToolsMethods = [
+  // Used by the reload addon.
+  // Force reloading dependencies if the loader happens to have reloaded.
+  "reload",
+
+  // Used by: - b2g desktop.js
+  //          - nsContextMenu
+  //          - /devtools code
+  "showToolbox",
+
+  // Used by Addon SDK and /devtools
+  "closeToolbox",
+  "getToolbox",
+
+  // Used by Addon SDK, main.js and tests:
+  "registerTool",
+  "registerTheme",
+  "unregisterTool",
+  "unregisterTheme",
+
+  // Used by main.js and test
+  "getToolDefinitionArray",
+  "getThemeDefinitionArray",
+
+  // Used by theme-switching.js
+  "getThemeDefinition",
+  "emit",
+
+  // Used by /devtools
+  "on",
+  "off",
+  "once",
+
+  // Used by tests
+  "getToolDefinitionMap",
+  "getThemeDefinitionMap",
+  "getDefaultTools",
+  "getAdditionalTools",
+  "getToolDefinition",
+];
+this.gDevTools = {
+  // Used by tests
+  get _toolboxes() {
+    return devtools._toolboxes;
+  },
+  get _tools() {
+    return devtools._tools;
+  },
+  *[Symbol.iterator]() {
+    for (let toolbox of this._toolboxes) {
+      yield toolbox;
+    }
+  }
+};
+gDevToolsMethods.forEach(name => {
+  this.gDevTools[name] = (...args) => {
+    return devtools[name].apply(devtools, args);
+  };
+});
+
 
 /**
  * gDevToolsBrowser exposes functions to connect the gDevTools instance with a
  * Firefox instance.
  */
-var gDevToolsBrowser = {
-  /**
-   * A record of the windows whose menus we altered, so we can undo the changes
-   * as the window is closed
-   */
-  _trackedBrowserWindows: new Set(),
-
-  _tabStats: {
-    peakOpen: 0,
-    peakPinned: 0,
-    histOpen: [],
-    histPinned: []
-  },
-
-  /**
-   * This function is for the benefit of Tools:DevToolbox in
-   * browser/base/content/browser-sets.inc and should not be used outside
-   * of there
-   */
-  toggleToolboxCommand: function(gBrowser) {
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    let toolbox = gDevTools.getToolbox(target);
-
-    // If a toolbox exists, using toggle from the Main window :
-    // - should close a docked toolbox
-    // - should focus a windowed toolbox
-    let isDocked = toolbox && toolbox.hostType != Toolbox.HostType.WINDOW;
-    isDocked ? toolbox.destroy() : gDevTools.showToolbox(target);
-  },
-
-  /**
-   * This function ensures the right commands are enabled in a window,
-   * depending on their relevant prefs. It gets run when a window is registered,
-   * or when any of the devtools prefs change.
-   */
-  updateCommandAvailability: function(win) {
-    let doc = win.document;
-
-    function toggleCmd(id, isEnabled) {
-      let cmd = doc.getElementById(id);
-      if (isEnabled) {
-        cmd.removeAttribute("disabled");
-        cmd.removeAttribute("hidden");
-      } else {
-        cmd.setAttribute("disabled", "true");
-        cmd.setAttribute("hidden", "true");
-      }
-    };
-
-    // Enable developer toolbar?
-    let devToolbarEnabled = Services.prefs.getBoolPref("devtools.toolbar.enabled");
-    toggleCmd("Tools:DevToolbar", devToolbarEnabled);
-    let focusEl = doc.getElementById("Tools:DevToolbarFocus");
-    if (devToolbarEnabled) {
-      focusEl.removeAttribute("disabled");
-    } else {
-      focusEl.setAttribute("disabled", "true");
-    }
-    if (devToolbarEnabled && Services.prefs.getBoolPref("devtools.toolbar.visible")) {
-      win.DeveloperToolbar.show(false).catch(console.error);
-    }
-
-    // Enable WebIDE?
-    let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
-    toggleCmd("Tools:WebIDE", webIDEEnabled);
-
-    let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
-    if (webIDEEnabled && showWebIDEWidget) {
-      gDevToolsBrowser.installWebIDEWidget();
-    } else {
-      gDevToolsBrowser.uninstallWebIDEWidget();
-    }
-
-    // Enable Browser Toolbox?
-    let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
-    let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
-    let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled;
-    toggleCmd("Tools:BrowserToolbox", remoteEnabled);
-    toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
-
-    // Enable Error Console?
-    let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
-    toggleCmd("Tools:ErrorConsole", consoleEnabled);
-
-    // Enable DevTools connection screen, if the preference allows this.
-    toggleCmd("Tools:DevToolsConnect", devtoolsRemoteEnabled);
-  },
+let gDevToolsBrowserMethods = [
+  // used by browser-sets.inc, command
+  "toggleToolboxCommand",
 
-  observe: function(subject, topic, prefName) {
-    if (prefName.endsWith("enabled")) {
-      for (let win of this._trackedBrowserWindows) {
-        this.updateCommandAvailability(win);
-      }
-    }
-  },
-
-  _prefObserverRegistered: false,
-
-  ensurePrefObserver: function() {
-    if (!this._prefObserverRegistered) {
-      this._prefObserverRegistered = true;
-      Services.prefs.addObserver("devtools.", this, false);
-    }
-  },
-
-
-  /**
-   * This function is for the benefit of Tools:{toolId} commands,
-   * triggered from the WebDeveloper menu and keyboard shortcuts.
-   *
-   * selectToolCommand's behavior:
-   * - if the toolbox is closed,
-   *   we open the toolbox and select the tool
-   * - if the toolbox is open, and the targeted tool is not selected,
-   *   we select it
-   * - if the toolbox is open, and the targeted tool is selected,
-   *   and the host is NOT a window, we close the toolbox
-   * - if the toolbox is open, and the targeted tool is selected,
-   *   and the host is a window, we raise the toolbox window
-   */
-  selectToolCommand: function(gBrowser, toolId) {
-    let target = TargetFactory.forTab(gBrowser.selectedTab);
-    let toolbox = gDevTools.getToolbox(target);
-    let toolDefinition = gDevTools.getToolDefinition(toolId);
-
-    if (toolbox &&
-        (toolbox.currentToolId == toolId ||
-          (toolId == "webconsole" && toolbox.splitConsole)))
-    {
-      toolbox.fireCustomKey(toolId);
+  // Used by browser.js itself, by setting a oncommand string...
+  "selectToolCommand",
 
-      if (toolDefinition.preventClosingOnKey || toolbox.hostType == Toolbox.HostType.WINDOW) {
-        toolbox.raise();
-      } else {
-        toolbox.destroy();
-      }
-      gDevTools.emit("select-tool-command", toolId);
-    } else {
-      gDevTools.showToolbox(target, toolId).then(() => {
-        let target = TargetFactory.forTab(gBrowser.selectedTab);
-        let toolbox = gDevTools.getToolbox(target);
-
-        toolbox.fireCustomKey(toolId);
-        gDevTools.emit("select-tool-command", toolId);
-      });
-    }
-  },
-
-  /**
-   * Open a tab to allow connects to a remote browser
-   */
-  openConnectScreen: function(gBrowser) {
-    gBrowser.selectedTab = gBrowser.addTab("chrome://devtools/content/framework/connect/connect.xhtml");
-  },
-
-  /**
-   * Open WebIDE
-   */
-  openWebIDE: function() {
-    let win = Services.wm.getMostRecentWindow("devtools:webide");
-    if (win) {
-      win.focus();
-    } else {
-      Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
-    }
-  },
-
-  _getContentProcessTarget: function () {
-    // Create a DebuggerServer in order to connect locally to it
-    if (!DebuggerServer.initialized) {
-      DebuggerServer.init();
-      DebuggerServer.addBrowserActors();
-    }
-    DebuggerServer.allowChromeProcess = true;
-
-    let transport = DebuggerServer.connectPipe();
-    let client = new DebuggerClient(transport);
+  // Used by browser-sets.inc, command
+  "openConnectScreen",
 
-    let deferred = promise.defer();
-    client.connect().then(() => {
-      client.mainRoot.listProcesses(response => {
-        // Do nothing if there is only one process, the parent process.
-        let contentProcesses = response.processes.filter(p => (!p.parent));
-        if (contentProcesses.length < 1) {
-          let msg = bundle.GetStringFromName("toolbox.noContentProcess.message");
-          Services.prompt.alert(null, "", msg);
-          deferred.reject("No content processes available.");
-          return;
-        }
-        // Otherwise, arbitrary connect to the unique content process.
-        client.getProcess(contentProcesses[0].id)
-              .then(response => {
-                let options = {
-                  form: response.form,
-                  client: client,
-                  chrome: true,
-                  isTabActor: false
-                };
-                return TargetFactory.forRemoteTab(options);
-              })
-              .then(target => {
-                // Ensure closing the connection in order to cleanup
-                // the debugger client and also the server created in the
-                // content process
-                target.on("close", () => {
-                  client.close();
-                });
-                deferred.resolve(target);
-              });
-      });
-    });
-
-    return deferred.promise;
-  },
-
-  openContentProcessToolbox: function () {
-    this._getContentProcessTarget()
-        .then(target => {
-          // Display a new toolbox, in a new window, with debugger by default
-          return gDevTools.showToolbox(target, "jsdebugger",
-                                       Toolbox.HostType.WINDOW);
-        });
-  },
-
-  /**
-   * Install WebIDE widget
-   */
-  installWebIDEWidget: function() {
-    if (this.isWebIDEWidgetInstalled()) {
-      return;
-    }
-
-    let defaultArea;
-    if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
-      defaultArea = CustomizableUI.AREA_NAVBAR;
-    } else {
-      defaultArea = CustomizableUI.AREA_PANEL;
-    }
-
-    CustomizableUI.createWidget({
-      id: "webide-button",
-      shortcutId: "key_webide",
-      label: "devtools-webide-button2.label",
-      tooltiptext: "devtools-webide-button2.tooltiptext",
-      defaultArea: defaultArea,
-      onCommand: function(aEvent) {
-        gDevToolsBrowser.openWebIDE();
-      }
-    });
-  },
-
-  isWebIDEWidgetInstalled: function() {
-    let widgetWrapper = CustomizableUI.getWidget("webide-button");
-    return !!(widgetWrapper && widgetWrapper.provider == CustomizableUI.PROVIDER_API);
-  },
-
-  /**
-   * The deferred promise will be resolved by WebIDE's UI.init()
-   */
-  isWebIDEInitialized: promise.defer(),
-
-  /**
-   * Uninstall WebIDE widget
-   */
-  uninstallWebIDEWidget: function() {
-    if (this.isWebIDEWidgetInstalled()) {
-      CustomizableUI.removeWidgetFromArea("webide-button");
-    }
-    CustomizableUI.destroyWidget("webide-button");
-  },
-
-  /**
-   * Move WebIDE widget to the navbar
-   */
-  moveWebIDEWidgetInNavbar: function() {
-    CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
-  },
+  // Used by browser-sets.inc, command
+  //         itself, webide widget
+  "openWebIDE",
 
-  /**
-   * 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
-   */
-  registerBrowserWindow: function DT_registerBrowserWindow(win) {
-    this.updateCommandAvailability(win);
-    this.ensurePrefObserver();
-    gDevToolsBrowser._trackedBrowserWindows.add(win);
-    gDevToolsBrowser._addAllToolsToMenu(win.document);
-
-    if (this._isFirebugInstalled()) {
-      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
-      broadcaster.removeAttribute("key");
-    }
-
-    let tabContainer = win.gBrowser.tabContainer;
-    tabContainer.addEventListener("TabSelect", this, false);
-    tabContainer.addEventListener("TabOpen", this, false);
-    tabContainer.addEventListener("TabClose", this, false);
-    tabContainer.addEventListener("TabPinned", this, false);
-    tabContainer.addEventListener("TabUnpinned", this, false);
-  },
-
-  /**
-   * Add a <key> to <keyset id="devtoolsKeyset">.
-   * Appending a <key> element is not always enough. The <keyset> needs
-   * to be detached and reattached to make sure the <key> is taken into
-   * account (see bug 832984).
-   *
-   * @param {XULDocument} doc
-   *        The document to which keys are to be added
-   * @param {XULElement} or {DocumentFragment} keys
-   *        Keys to add
-   */
-  attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
-    let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
-
-    if (!devtoolsKeyset) {
-      devtoolsKeyset = doc.createElement("keyset");
-      devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
-    }
-    devtoolsKeyset.appendChild(keys);
-    let mainKeyset = doc.getElementById("mainKeyset");
-    mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
-  },
+  // Used by browser-sets.inc, command
+  "openContentProcessToolbox",
 
-  /**
-   * Hook the JS debugger tool to the "Debug Script" button of the slow script
-   * dialog.
-   */
-  setSlowScriptDebugHandler: function DT_setSlowScriptDebugHandler() {
-    let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
-                         .getService(Ci.nsISlowScriptDebug);
-    let tm = Cc["@mozilla.org/thread-manager;1"].getService(Ci.nsIThreadManager);
-
-    function slowScriptDebugHandler(aTab, aCallback) {
-      let target = TargetFactory.forTab(aTab);
-
-      gDevTools.showToolbox(target, "jsdebugger").then(toolbox => {
-        let threadClient = toolbox.getCurrentPanel().panelWin.gThreadClient;
-
-        // Break in place, which means resuming the debuggee thread and pausing
-        // right before the next step happens.
-        switch (threadClient.state) {
-          case "paused":
-            // When the debugger is already paused.
-            threadClient.resumeThenPause();
-            aCallback();
-            break;
-          case "attached":
-            // When the debugger is already open.
-            threadClient.interrupt(() => {
-              threadClient.resumeThenPause();
-              aCallback();
-            });
-            break;
-          case "resuming":
-            // The debugger is newly opened.
-            threadClient.addOneTimeListener("resumed", () => {
-              threadClient.interrupt(() => {
-                threadClient.resumeThenPause();
-                aCallback();
-              });
-            });
-            break;
-          default:
-            throw Error("invalid thread client state in slow script debug handler: " +
-                        threadClient.state);
-          }
-      });
-    }
-
-    debugService.activationHandler = function(aWindow) {
-      let chromeWindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIWebNavigation)
-                                .QueryInterface(Ci.nsIDocShellTreeItem)
-                                .rootTreeItem
-                                .QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDOMWindow)
-                                .QueryInterface(Ci.nsIDOMChromeWindow);
-
-      let setupFinished = false;
-      slowScriptDebugHandler(chromeWindow.gBrowser.selectedTab,
-                             () => { setupFinished = true; });
+  // Used by webide.js
+  "moveWebIDEWidgetInNavbar",
 
-      // Don't return from the interrupt handler until the debugger is brought
-      // up; no reason to continue executing the slow script.
-      let utils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-      utils.enterModalState();
-      while (!setupFinished) {
-        tm.currentThread.processNextEvent(true);
-      }
-      utils.leaveModalState();
-    };
-
-    debugService.remoteActivationHandler = function(aBrowser, aCallback) {
-      let chromeWindow = aBrowser.ownerDocument.defaultView;
-      let tab = chromeWindow.gBrowser.getTabForBrowser(aBrowser);
-      chromeWindow.gBrowser.selected = tab;
-
-      function callback() {
-        aCallback.finishDebuggerStartup();
-      }
-
-      slowScriptDebugHandler(tab, callback);
-    };
-  },
-
-  /**
-   * Unset the slow script debug handler.
-   */
-  unsetSlowScriptDebugHandler: function DT_unsetSlowScriptDebugHandler() {
-    let debugService = Cc["@mozilla.org/dom/slow-script-debug;1"]
-                         .getService(Ci.nsISlowScriptDebug);
-    debugService.activationHandler = undefined;
-  },
-
-  /**
-   * Detect the presence of a Firebug.
-   *
-   * @return promise
-   */
-  _isFirebugInstalled: function DT_isFirebugInstalled() {
-    let bootstrappedAddons = Services.prefs.getCharPref("extensions.bootstrappedAddons");
-    return bootstrappedAddons.indexOf("firebug@software.joehewitt.com") != -1;
-  },
-
-  /**
-   * Add the menuitem for a tool to all open browser windows.
-   *
-   * @param {object} toolDefinition
-   *        properties of the tool to add
-   */
-  _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
-    // No menu item or global shortcut is required for options panel.
-    if (!toolDefinition.inMenu) {
-      return;
-    }
-
-    // Skip if the tool is disabled.
-    try {
-      if (toolDefinition.visibilityswitch &&
-         !Services.prefs.getBoolPref(toolDefinition.visibilityswitch)) {
-        return;
-      }
-    } catch(e) {}
-
-    // We need to insert the new tool in the right place, which means knowing
-    // the tool that comes before the tool that we're trying to add
-    let allDefs = gDevTools.getToolDefinitionArray();
-    let prevDef;
-    for (let def of allDefs) {
-      if (!def.inMenu) {
-        continue;
-      }
-      if (def === toolDefinition) {
-        break;
-      }
-      prevDef = def;
-    }
-
-    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
-      let doc = win.document;
-      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
-
-      doc.getElementById("mainCommandSet").appendChild(elements.cmd);
-
-      if (elements.key) {
-        this.attachKeybindingsToBrowser(doc, elements.key);
-      }
-
-      doc.getElementById("mainBroadcasterSet").appendChild(elements.bc);
-
-      let amp = doc.getElementById("appmenu_webDeveloper_popup");
-      if (amp) {
-        let ref;
-
-        if (prevDef != null) {
-          let menuitem = doc.getElementById("appmenuitem_" + prevDef.id);
-          ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
-        } else {
-          ref = doc.getElementById("appmenu_devtools_separator");
-        }
+  // Used by browser.js
+  "registerBrowserWindow",
 
-        if (ref) {
-          amp.insertBefore(elements.appmenuitem, ref);
-        }
-      }
-
-      let ref;
-
-      if (prevDef) {
-        let menuitem = doc.getElementById("menuitem_" + prevDef.id);
-        ref = menuitem && menuitem.nextSibling ? menuitem.nextSibling : null;
-      } else {
-        ref = doc.getElementById("menu_devtools_separator");
-      }
-
-      if (ref) {
-        ref.parentNode.insertBefore(elements.menuitem, ref);
-      }
-    }
-
-    if (toolDefinition.id === "jsdebugger") {
-      gDevToolsBrowser.setSlowScriptDebugHandler();
-    }
-  },
-
-  /**
-   * Add all tools to the developer tools menu of a window.
-   *
-   * @param {XULDocument} doc
-   *        The document to which the tool items are to be added.
-   */
-  _addAllToolsToMenu: function DT_addAllToolsToMenu(doc) {
-    let fragCommands = doc.createDocumentFragment();
-    let fragKeys = doc.createDocumentFragment();
-    let fragBroadcasters = doc.createDocumentFragment();
-    let fragAppMenuItems = doc.createDocumentFragment();
-    let fragMenuItems = doc.createDocumentFragment();
-
-    for (let toolDefinition of gDevTools.getToolDefinitionArray()) {
-      if (!toolDefinition.inMenu) {
-        continue;
-      }
-
-      let elements = gDevToolsBrowser._createToolMenuElements(toolDefinition, doc);
-
-      if (!elements) {
-        return;
-      }
-
-      fragCommands.appendChild(elements.cmd);
-      if (elements.key) {
-        fragKeys.appendChild(elements.key);
-      }
-      fragBroadcasters.appendChild(elements.bc);
-      fragAppMenuItems.appendChild(elements.appmenuitem);
-      fragMenuItems.appendChild(elements.menuitem);
-    }
-
-    let mcs = doc.getElementById("mainCommandSet");
-    mcs.appendChild(fragCommands);
-
-    this.attachKeybindingsToBrowser(doc, fragKeys);
-
-    let mbs = doc.getElementById("mainBroadcasterSet");
-    mbs.appendChild(fragBroadcasters);
-
-    let amps = doc.getElementById("appmenu_devtools_separator");
-    if (amps) {
-      amps.parentNode.insertBefore(fragAppMenuItems, amps);
-    }
-
-    let mps = doc.getElementById("menu_devtools_separator");
-    if (mps) {
-      mps.parentNode.insertBefore(fragMenuItems, mps);
-    }
-  },
-
-  /**
-   * Add a menu entry for a tool definition
-   *
-   * @param {string} toolDefinition
-   *        Tool definition of the tool to add a menu entry.
-   * @param {XULDocument} doc
-   *        The document to which the tool menu item is to be added.
-   */
-  _createToolMenuElements: function DT_createToolMenuElements(toolDefinition, doc) {
-    let id = toolDefinition.id;
-
-    // Prevent multiple entries for the same tool.
-    if (doc.getElementById("Tools:" + id)) {
-      return;
-    }
-
-    let cmd = doc.createElement("command");
-    cmd.id = "Tools:" + id;
-    cmd.setAttribute("oncommand",
-        'gDevToolsBrowser.selectToolCommand(gBrowser, "' + id + '");');
-
-    let key = null;
-    if (toolDefinition.key) {
-      key = doc.createElement("key");
-      key.id = "key_" + id;
+  // Used by reload addon
+  "hasToolboxOpened",
 
-      if (toolDefinition.key.startsWith("VK_")) {
-        key.setAttribute("keycode", toolDefinition.key);
-      } else {
-        key.setAttribute("key", toolDefinition.key);
-      }
-
-      key.setAttribute("command", cmd.id);
-      key.setAttribute("modifiers", toolDefinition.modifiers);
-    }
-
-    let bc = doc.createElement("broadcaster");
-    bc.id = "devtoolsMenuBroadcaster_" + id;
-    bc.setAttribute("label", toolDefinition.menuLabel || toolDefinition.label);
-    bc.setAttribute("command", cmd.id);
-
-    if (key) {
-      bc.setAttribute("key", "key_" + id);
-    }
-
-    let appmenuitem = doc.createElement("menuitem");
-    appmenuitem.id = "appmenuitem_" + id;
-    appmenuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
-
-    let menuitem = doc.createElement("menuitem");
-    menuitem.id = "menuitem_" + id;
-    menuitem.setAttribute("observes", "devtoolsMenuBroadcaster_" + id);
-
-    if (toolDefinition.accesskey) {
-      menuitem.setAttribute("accesskey", toolDefinition.accesskey);
-    }
-
-    return {
-      cmd: cmd,
-      key: key,
-      bc: bc,
-      appmenuitem: appmenuitem,
-      menuitem: menuitem
-    };
-  },
-
-  hasToolboxOpened: function(win) {
-    let tab = win.gBrowser.selectedTab;
-    for (let [target, toolbox] of gDevTools._toolboxes) {
-      if (target.tab == tab) {
-        return true;
-      }
-    }
-    return false;
-  },
-
-  /**
-   * Update the "Toggle Tools" checkbox in the developer tools menu. This is
-   * called when a toolbox is created or destroyed.
-   */
-  _updateMenuCheckbox: function DT_updateMenuCheckbox() {
-    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
-
-      let hasToolbox = gDevToolsBrowser.hasToolboxOpened(win);
-
-      let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
-      if (hasToolbox) {
-        broadcaster.setAttribute("checked", "true");
-      } else {
-        broadcaster.removeAttribute("checked");
-      }
-    }
-  },
-
-  /**
-   * Remove the menuitem for a tool to all open browser windows.
-   *
-   * @param {string} toolId
-   *        id of the tool to remove
-   */
-  _removeToolFromWindows: function DT_removeToolFromWindows(toolId) {
-    for (let win of gDevToolsBrowser._trackedBrowserWindows) {
-      gDevToolsBrowser._removeToolFromMenu(toolId, win.document);
-    }
-
-    if (toolId === "jsdebugger") {
-      gDevToolsBrowser.unsetSlowScriptDebugHandler();
-    }
+  // Used by browser.js
+  "forgetBrowserWindow"
+];
+this.gDevToolsBrowser = {
+  // Used by webide.js
+  get isWebIDEInitialized() {
+    return browser.isWebIDEInitialized;
   },
-
-  /**
-   * Remove a tool's menuitem from a window
-   *
-   * @param {string} toolId
-   *        Id of the tool to add a menu entry for
-   * @param {XULDocument} doc
-   *        The document to which the tool menu item is to be removed from
-   */
-  _removeToolFromMenu: function DT_removeToolFromMenu(toolId, doc) {
-    let command = doc.getElementById("Tools:" + toolId);
-    if (command) {
-      command.parentNode.removeChild(command);
-    }
-
-    let key = doc.getElementById("key_" + toolId);
-    if (key) {
-      key.parentNode.removeChild(key);
-    }
-
-    let bc = doc.getElementById("devtoolsMenuBroadcaster_" + toolId);
-    if (bc) {
-      bc.parentNode.removeChild(bc);
-    }
-
-    let appmenuitem = doc.getElementById("appmenuitem_" + toolId);
-    if (appmenuitem) {
-      appmenuitem.parentNode.removeChild(appmenuitem);
-    }
-
-    let menuitem = doc.getElementById("menuitem_" + toolId);
-    if (menuitem) {
-      menuitem.parentNode.removeChild(menuitem);
-    }
-  },
-
-  /**
-   * Called on browser unload to remove menu entries, toolboxes and event
-   * listeners from the closed browser window.
-   *
-   * @param  {XULWindow} win
-   *         The window containing the menu entry
-   */
-  forgetBrowserWindow: function DT_forgetBrowserWindow(win) {
-    gDevToolsBrowser._trackedBrowserWindows.delete(win);
-
-    // Destroy toolboxes for closed window
-    for (let [target, toolbox] of gDevTools._toolboxes) {
-      if (toolbox.frame && toolbox.frame.ownerDocument.defaultView == win) {
-        toolbox.destroy();
-      }
-    }
-
-    let tabContainer = win.gBrowser.tabContainer;
-    tabContainer.removeEventListener("TabSelect", this, false);
-    tabContainer.removeEventListener("TabOpen", this, false);
-    tabContainer.removeEventListener("TabClose", this, false);
-    tabContainer.removeEventListener("TabPinned", this, false);
-    tabContainer.removeEventListener("TabUnpinned", this, false);
-  },
-
-  handleEvent: function(event) {
-    switch (event.type) {
-      case "TabOpen":
-      case "TabClose":
-      case "TabPinned":
-      case "TabUnpinned":
-        let open = 0;
-        let pinned = 0;
-
-        for (let win of this._trackedBrowserWindows) {
-          let tabContainer = win.gBrowser.tabContainer;
-          let numPinnedTabs = win.gBrowser._numPinnedTabs || 0;
-          let numTabs = tabContainer.itemCount - numPinnedTabs;
-
-          open += numTabs;
-          pinned += numPinnedTabs;
-        }
-
-        this._tabStats.histOpen.push(open);
-        this._tabStats.histPinned.push(pinned);
-        this._tabStats.peakOpen = Math.max(open, this._tabStats.peakOpen);
-        this._tabStats.peakPinned = Math.max(pinned, this._tabStats.peakPinned);
-      break;
-      case "TabSelect":
-        gDevToolsBrowser._updateMenuCheckbox();
-    }
-  },
-
-  /**
-   * All browser windows have been closed, tidy up remaining objects.
-   */
-  destroy: function() {
-    Services.prefs.removeObserver("devtools.", gDevToolsBrowser);
-    Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
-  },
-}
-
-this.gDevToolsBrowser = gDevToolsBrowser;
-
-gDevTools.on("tool-registered", function(ev, toolId) {
-  let toolDefinition = gDevTools._tools.get(toolId);
-  gDevToolsBrowser._addToolToWindows(toolDefinition);
+  // Used by a test (should be removed)
+  get _trackedBrowserWindows() {
+    return browser._trackedBrowserWindows;
+  }
+};
+gDevToolsBrowserMethods.forEach(name => {
+  this.gDevToolsBrowser[name] = (...args) => {
+    return browser[name].apply(browser, args);
+  };
 });
-
-gDevTools.on("tool-unregistered", function(ev, toolId) {
-  if (typeof toolId != "string") {
-    toolId = toolId.id;
-  }
-  gDevToolsBrowser._removeToolFromWindows(toolId);
-});
-
-gDevTools.on("toolbox-ready", gDevToolsBrowser._updateMenuCheckbox);
-gDevTools.on("toolbox-destroyed", gDevToolsBrowser._updateMenuCheckbox);
-
-Services.obs.addObserver(gDevToolsBrowser.destroy, "quit-application", false);
-
-// Load the browser devtools main module as the loader's main module.
-loader.main("devtools/client/main");
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -6,16 +6,18 @@
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
 TEST_HARNESS_FILES.xpcshell.devtools.client.framework.test += [
     'test/shared-redux-head.js',
 ]
 
 DevToolsModules(
     'attach-thread.js',
+    'devtools-browser.js',
+    'devtools.js',
     'gDevTools.jsm',
     'selection.js',
     'sidebar.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
     'toolbox.js',
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -1,17 +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/. */
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
-var { gDevTools } = Cu.import("resource://devtools/client/framework/gDevTools.jsm", {});
-var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+// Require this module just to setup things like themes and tools
+// devtools-browser is special as it loads main module
+// To be cleaned up in bug 1247203.
+require("devtools/client/framework/devtools-browser");
+var { gDevTools } = require("devtools/client/framework/devtools");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
 var { DebuggerClient } = require("devtools/shared/client/main");
 var { ViewHelpers } =
   Cu.import("resource://devtools/client/shared/widgets/ViewHelpers.jsm", {});
 var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 
--- a/devtools/client/responsive.html/actions/index.js
+++ b/devtools/client/responsive.html/actions/index.js
@@ -12,16 +12,19 @@ createEnum([
 
   // The location of the page has changed.  This may be triggered by the user
   // directly entering a new URL, navigating with links, etc.
   "CHANGE_LOCATION",
 
   // Add an additional viewport to display the document.
   "ADD_VIEWPORT",
 
+  // Rotate the viewport.
+  "ROTATE_VIEWPORT",
+
 ], module.exports);
 
 /**
  * Create a simple enum-like object with keys mirrored to values from an array.
  * This makes comparison to a specfic value simpler without having to repeat and
  * mis-type the value.
  */
 function createEnum(array, target) {
--- a/devtools/client/responsive.html/actions/viewports.js
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -1,20 +1,30 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { ADD_VIEWPORT } = require("./index");
+const { ADD_VIEWPORT, ROTATE_VIEWPORT } = require("./index");
 
 module.exports = {
 
   /**
    * Add an additional viewport to display the document.
    */
   addViewport() {
     return {
       type: ADD_VIEWPORT,
     };
   },
 
+  /**
+   * Rotate the viewport.
+   */
+  rotateViewport(id) {
+    return {
+      type: ROTATE_VIEWPORT,
+      id,
+    };
+  },
+
 };
--- a/devtools/client/responsive.html/app.js
+++ b/devtools/client/responsive.html/app.js
@@ -3,37 +3,40 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 const { connect } = require("devtools/client/shared/vendor/react-redux");
 
+const { rotateViewport } = require("./actions/viewports");
 const Types = require("./types");
 const Viewports = createFactory(require("./components/viewports"));
 
 let App = createClass({
 
   displayName: "App",
 
   propTypes: {
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
   },
 
   render() {
     let {
+      dispatch,
       location,
       viewports,
     } = this.props;
 
     // For the moment, the app is just the viewports.  This seems likely to
     // change assuming we add a global toolbar or something similar.
     return Viewports({
       location,
       viewports,
+      onRotateViewport: id => dispatch(rotateViewport(id)),
     });
   },
 
 });
 
 module.exports = connect(state => state)(App);
--- a/devtools/client/responsive.html/components/moz.build
+++ b/devtools/client/responsive.html/components/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # 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/.
 
 DevToolsModules(
     'browser.js',
+    'viewport-toolbar.js',
     'viewport.js',
     'viewports.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/components/viewport-toolbar.js
@@ -0,0 +1,34 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { DOM: dom, createClass, PropTypes } =
+  require("devtools/client/shared/vendor/react");
+
+module.exports = createClass({
+
+  displayName: "ViewportToolbar",
+
+  propTypes: {
+    onRotateViewport: PropTypes.func.isRequired,
+  },
+
+  render() {
+    let {
+      onRotateViewport,
+    } = this.props;
+
+    return dom.div(
+      {
+        className: "viewport-toolbar",
+      },
+      dom.button({
+        className: "viewport-rotate-button viewport-toolbar-button",
+        onClick: onRotateViewport,
+      })
+    );
+  },
+
+});
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -4,40 +4,41 @@
 
 "use strict";
 
 const { DOM: dom, createClass, createFactory, PropTypes } =
   require("devtools/client/shared/vendor/react");
 
 const Types = require("../types");
 const Browser = createFactory(require("./browser"));
+const ViewportToolbar = createFactory(require("./viewport-toolbar"));
 
 module.exports = createClass({
 
   displayName: "Viewport",
 
   propTypes: {
     location: Types.location.isRequired,
     viewport: PropTypes.shape(Types.viewport).isRequired,
+    onRotateViewport: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       location,
       viewport,
+      onRotateViewport,
     } = this.props;
 
-    // Additional elements will soon appear here around the Browser, like drag
-    // handles, etc.
     return dom.div(
       {
         className: "viewport"
       },
-      dom.div({
-        className: "viewport-header",
+      ViewportToolbar({
+        onRotateViewport,
       }),
       Browser({
         location,
         width: viewport.width,
         height: viewport.height,
       })
     );
   },
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -12,31 +12,34 @@ const Viewport = createFactory(require("
 
 module.exports = createClass({
 
   displayName: "Viewports",
 
   propTypes: {
     location: Types.location.isRequired,
     viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
+    onRotateViewport: PropTypes.func.isRequired,
   },
 
   render() {
     let {
       location,
       viewports,
+      onRotateViewport,
     } = this.props;
 
     return dom.div(
       {
         id: "viewports",
       },
-      viewports.map((viewport, index) => {
+      viewports.map(viewport => {
         return Viewport({
-          key: index,
+          key: viewport.id,
           location,
           viewport,
+          onRotateViewport: () => onRotateViewport(viewport.id),
         });
       })
     );
   },
 
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/images/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+DevToolsModules(
+    'rotate-viewport.svg',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/images/rotate-viewport.svg
@@ -0,0 +1,6 @@
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <path d="M12.3 11.5l-2.2-8.2-.3-.3c-.1-.1-.2-.1-.4-.1L4 4.4c-.2.1-.4.4-.3.6l2.2 8.2c0 .1.1.2.2.3.1 0 .2.1.3.1h.1l5.4-1.5c.3 0 .5-.3.4-.6zM9.2 4.1l1.5 5.5-4.4 1.2-1.5-5.5 4.4-1.2zm-2.4 8.4l-.3-1.1 4.4-1.2.3 1.1-4.4 1.2zM3.7 13.7c-1.2 0-3.4-.6-3.7-2.8-.3-2.2 1.3-3.3 2.1-3.5.2-.1.4.1.5.3.1.2-.1.4-.3.5-.1 0-1.8.6-1.6 2.7.2 1.5 1.6 1.9 2.4 2l-.7-2.4c0-.2.2-.5.4-.5.2-.1.4 0 .5.2l.9 3c0 .1 0 .3-.1.4-.1.1-.2.1-.4.1zM12.3 3.1c1.2 0 3.4.6 3.7 2.8.3 2.2-1.3 3.3-2.1 3.5-.2.1-.4-.1-.5-.3-.1-.2.1-.4.3-.5.1 0 1.8-.6 1.6-2.7-.2-1.5-1.6-1.9-2.4-2l.7 2.4c.1.2-.1.4-.3.5-.2.1-.4-.1-.5-.3l-.9-3c0-.1 0-.3.1-.4h.3z"/>
+</svg>
--- a/devtools/client/responsive.html/index.css
+++ b/devtools/client/responsive.html/index.css
@@ -1,11 +1,23 @@
 /* TODO: May break up into component local CSS.  Pending future discussions by
  * React component group on how to best handle CSS. */
 
+/**
+ * CSS Variables specific to the responsive design mode
+ */
+
+.theme-light {
+  --viewport-box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+}
+
+.theme-dark {
+  --viewport-box-shadow: 0 4px 4px 0 rgba(105, 105, 105, 0.26);
+}
+
 html, body {
   margin: 0;
   height: 100%;
 }
 
 body {
   /* Only allow horizontal scrolling when more viewports are added */
   overflow-y: hidden;
@@ -37,22 +49,54 @@ body {
  * Viewport Container
  */
 
 .viewport {
   display: inline-block;
   /* Align all viewports to the top */
   vertical-align: top;
   border: 1px solid var(--theme-splitter-color);
-  box-shadow: 0 4px 4px 0 rgba(155, 155, 155, 0.26);
+  box-shadow: var(--viewport-box-shadow);
+}
+
+/**
+ * Viewport Toolbar
+ */
+
+.viewport-toolbar {
+  background-color: var(--theme-toolbar-background);
+  border-bottom: 1px solid var(--theme-splitter-color);
+  color: var(--theme-body-color);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  height: 18px;
 }
 
-.viewport-header {
-  background-color: var(--theme-toolbar-background);
-  border-bottom: 1px solid var(--theme-splitter-color);
-  color: var(--theme-body-color-alt);
+.viewport-toolbar-button {
+  border: none;
+  display: block;
+  margin: 1px 3px;
+  padding: 0;
+  width: 16px;
   height: 16px;
+  opacity: 0.8;
+  background-color: var(--theme-body-color);
+  transition: background 0.25s ease;
+}
+
+.viewport-toolbar-button:hover {
+  opacity: 1;
+}
+
+.viewport-toolbar-button:active {
+  background-color: var(--theme-selection-background);
+  opacity: 1;
+}
+
+.viewport-rotate-button {
+  mask-image: url("./images/rotate-viewport.svg");
 }
 
 .browser {
   display: block;
   border: 0;
 }
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -2,16 +2,17 @@
 # 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/.
 
 DIRS += [
     'actions',
     'components',
+    'images',
     'reducers',
 ]
 
 DevToolsModules(
     'app.js',
     'index.css',
     'manager.js',
     'reducers.js',
--- a/devtools/client/responsive.html/reducers/viewports.js
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -1,30 +1,46 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { ADD_VIEWPORT } = require("../actions/index");
+const { ADD_VIEWPORT, ROTATE_VIEWPORT } = require("../actions/index");
+
+let nextViewportId = 0;
 
 const INITIAL_VIEWPORTS = [];
 const INITIAL_VIEWPORT = {
+  id: nextViewportId++,
   width: 320,
   height: 480,
 };
 
 let reducers = {
 
   [ADD_VIEWPORT](viewports) {
     // For the moment, there can be at most one viewport.
     if (viewports.length === 1) {
       return viewports;
     }
-    return [...viewports, INITIAL_VIEWPORT];
+    return [...viewports, Object.assign({}, INITIAL_VIEWPORT)];
+  },
+
+  [ROTATE_VIEWPORT](viewports, { id }) {
+    return viewports.map(viewport => {
+      if (viewport.id !== id) {
+        return viewport;
+      }
+
+      return Object.assign({}, viewport, {
+        width: viewport.height,
+        height: viewport.width,
+      });
+    });
   },
 
 };
 
 module.exports = function(viewports = INITIAL_VIEWPORTS, action) {
   let reducer = reducers[action.type];
   if (!reducer) {
     return viewports;
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/unit/test_rotate_viewport.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test rotating the viewport.
+
+const { addViewport, rotateViewport } =
+  require("devtools/client/responsive.html/actions/viewports");
+
+add_task(function*() {
+  let store = Store();
+  const { getState, dispatch } = store;
+
+  dispatch(addViewport());
+
+  let viewport = getState().viewports[0];
+  equal(viewport.width, 320, "Default width of 320");
+  equal(viewport.height, 480, "Default height of 480");
+
+  dispatch(rotateViewport(0));
+  viewport = getState().viewports[0];
+  equal(viewport.width, 480, "Rotated width of 480");
+  equal(viewport.height, 320, "Rotated height of 320");
+});
--- a/devtools/client/responsive.html/test/unit/xpcshell.ini
+++ b/devtools/client/responsive.html/test/unit/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 tags = devtools
 head = head.js ../../../framework/test/shared-redux-head.js
 tail =
 firefox-appdir = browser
 
 [test_add_viewport.js]
 [test_change_location.js]
+[test_rotate_viewport.js]
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -9,16 +9,19 @@ const { PropTypes } = require("devtools/
 // React PropTypes are used to describe the expected "shape" of various common
 // objects that get passed down as props to components.
 
 /**
  * A single viewport displaying a document.
  */
 exports.viewport = {
 
+  // The id of the viewport
+  id: PropTypes.number.isRequired,
+
   // The width of the viewport
   width: PropTypes.number,
 
   // The height of the viewport
   height: PropTypes.number,
 
 };
 
--- a/devtools/client/webide/modules/simulators.js
+++ b/devtools/client/webide/modules/simulators.js
@@ -39,23 +39,23 @@ var Simulators = {
       let value = yield asyncStorage.getItem("simulators");
       if (Array.isArray(value)) {
         value.forEach(options => {
           let simulator = new Simulator(options);
           Simulators.add(simulator, true);
 
           // If the simulator had a reference to an addon, fix it.
           if (options.addonID) {
-            let job = promise.defer();
+            let deferred = promise.defer();
             AddonManager.getAddonByID(options.addonID, addon => {
               simulator.addon = addon;
               delete simulator.options.addonID;
-              job.resolve();
+              deferred.resolve();
             });
-            jobs.push(job);
+            jobs.push(deferred.promise);
           }
         });
       }
 
       yield promise.all(jobs);
       yield Simulators._addUnusedAddons();
       Simulators.emitUpdated();
       return Simulators._simulators;
@@ -227,17 +227,17 @@ var Simulators = {
   /**
    * Detect simulator addons, including "unofficial" ones.
    */
   isSimulatorAddon(addon) {
     return !!this.simulatorAddonVersion(addon);
   },
 
   emitUpdated() {
-    this.emit("updated");
+    this.emit("updated", { length: this._simulators.length });
     this._simulators.sort(LocaleCompare);
     this._save();
   },
 
   onConfigure(e, simulator) {
     this._lastConfiguredSimulator = simulator;
   },
 
--- a/devtools/client/webide/test/test_simulators.html
+++ b/devtools/client/webide/test/test_simulators.html
@@ -36,16 +36,31 @@
             if (addon.status == status) {
               addon.off("update", onUpdate);
               nextTick().then(() => deferred.resolve());
             }
           });
           return deferred.promise;
         }
 
+        function waitForUpdate(length) {
+          info(`Wait for update with length ${length}`);
+          let deferred = promise.defer();
+          let handler = (_, data) => {
+            if (data.length != length) {
+              return;
+            }
+            info(`Got update with length ${length}`);
+            Simulators.off("updated", handler);
+            deferred.resolve();
+          };
+          Simulators.on("updated", handler);
+          return deferred.promise;
+        }
+
         Task.spawn(function* () {
           let win = yield openWebIDE(false);
 
           yield Simulators._load();
 
           let docRuntime = getRuntimeDocument(win);
           let find = win.document.querySelector.bind(docRuntime);
           let findAll = win.document.querySelectorAll.bind(docRuntime);
@@ -78,27 +93,35 @@
           // Install fake "Firefox OS 1.0" simulator addon.
 
           let addons = yield GetAvailableAddons();
 
           let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
 
           sim10.install();
 
+          let updated = waitForUpdate(1);
           yield addonStatus(sim10, "installed");
+          yield updated;
+          // Wait for next tick to ensure UI elements are updated
+          yield nextTick();
 
           is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator in runtime panel");
 
           // Install fake "Firefox OS 2.0" simulator addon.
 
           let sim20 = addons.simulators.filter(a => a.version == "2.0")[0];
 
           sim20.install();
 
+          updated = waitForUpdate(2);
           yield addonStatus(sim20, "installed");
+          yield updated;
+          // Wait for next tick to ensure UI elements are updated
+          yield nextTick();
 
           is(findAll(".runtime-panel-item-simulator").length, 2, "Two simulators in runtime panel");
 
           // Dry run a simulator to verify that its parameters look right.
 
           let params = yield runSimulator(0);
 
           ok(params.path.includes(sim10.addonID) && params.path.includes("b2g-bin"), "Simulator binary path looks right");
@@ -108,16 +131,17 @@
 
           let profilePath = params.args[pid + 1];
           ok(profilePath.includes(sim10.addonID) && profilePath.includes("profile"), "Simulator profile path looks right");
 
           ok(params.args.indexOf("-dbgport") > -1 || params.args.indexOf("-start-debugger-server") > -1, "Simulator process arguments have a debugger port");
 
           ok(params.args.indexOf("-no-remote") > -1, "Simulator process arguments have --no-remote");
 
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           // Configure the fake 1.0 simulator.
 
           simulatorList.querySelectorAll(".configure-button")[0].click();
           is(win.document.querySelector("#deck").selectedPanel, simulatorPanel, "Simulator deck panel is selected");
 
           yield lazyIframeIsLoaded(simulatorPanel);
@@ -250,16 +274,17 @@
           ok(sid > -1, "Simulator process arguments have --screen");
           ok(params.args[sid + 1].includes(width + "x" + height), "Simulator screen resolution looks right");
 
           yield set(form.version, sim10.addonID);
 
           // Configure the fake 2.0 simulator.
 
           simulatorList.querySelectorAll(".configure-button")[1].click();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           // Test `name`.
 
           is(form.name.value, findAll(".runtime-panel-item-simulator")[1].textContent, "Original simulator name");
 
           yield set(form.name, customName + "2.0");
 
@@ -292,52 +317,59 @@
           is(form.height.value, String(device.height), "New device height is correct");
 
           params = yield runSimulator(1);
 
           sid = params.args.indexOf("-screen");
           ok(params.args[sid + 1].includes(device.width + "x" + device.height), "Simulator screen resolution looks right");
 
           // Test Simulator Menu.
-          is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden\n");
+          is(doc.querySelector("#tv_simulator_menu").style.visibility, "hidden", "OpenTVDummyDirectory Button is not hidden");
 
           // Restore default simulator options.
 
           doc.querySelector("#reset").click();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           for (let param in defaults.phone) {
             is(form[param].value, String(defaults.phone[param]), "Default phone value for device " + param);
           }
 
           // Install and configure the fake "Firefox OS 3.0 TV" simulator addon.
 
           let sim30tv = addons.simulators.filter(a => a.version == "3.0_tv")[0];
 
           sim30tv.install();
 
+          updated = waitForUpdate(3);
           yield addonStatus(sim30tv, "installed");
+          yield updated;
+          // Wait for next tick to ensure UI elements are updated
+          yield nextTick();
 
           is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators in runtime panel");
 
           simulatorList.querySelectorAll(".configure-button")[2].click();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           for (let param in defaults.television) {
             is(form[param].value, String(defaults.television[param]), "Default TV value for device " + param);
           }
 
           // Test Simulator Menu
           is(doc.querySelector("#tv_simulator_menu").style.visibility, "visible", "OpenTVDummyDirectory Button is not visible");
 
           // Force reload the list of simulators.
 
           Simulators._loadingPromise = null;
           Simulators._simulators = [];
           yield Simulators._load();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           is(findAll(".runtime-panel-item-simulator").length, 3, "Three simulators saved and reloaded " + Simulators._simulators.map(s => s.name).join(','));
 
           // Uninstall the 3.0 TV and 2.0 addons, and watch their Simulator objects disappear.
 
           sim30tv.uninstall();
 
@@ -349,19 +381,21 @@
 
           yield addonStatus(sim20, "uninstalled");
 
           is(findAll(".runtime-panel-item-simulator").length, 1, "One simulator left in runtime panel");
 
           // Remove 1.0 simulator.
 
           simulatorList.querySelectorAll(".configure-button")[0].click();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           doc.querySelector("#remove").click();
+          // Wait for next tick to ensure UI elements are updated
           yield nextTick();
 
           is(findAll(".runtime-panel-item-simulator").length, 0, "Last simulator was removed");
 
           yield asyncStorage.removeItem("simulators");
 
           sim10.uninstall();
 
--- a/devtools/server/docs/actor-hierarchy.md
+++ b/devtools/server/docs/actor-hierarchy.md
@@ -2,32 +2,34 @@
 
 To start with, actors are living within /devtools/server/actors/ folder.
 They are organized in a hierarchy for easier lifecycle/memory management:
 once a parent is removed from the pool, its children are removed as well.
 (See actor-registration.md for more information about how to implement one)
 
 The overall hierarchy of actors looks like this:
 
-  RootActor: First one, automatically instanciated when we start connecting.
-   |         Mostly meant to instanciate new actors.
+  RootActor: First one, automatically instantiated when we start connecting.
+   |         Mostly meant to instantiate new actors.
    |
    |--> Global-scoped actors:
    |    Actors exposing features related to the main process,
-   |    that are not specific to any document/app/addon.
+   |    that are not specific to any particular context (document, tab, app,
+   |    add-on, or worker).
    |    A good example is the preference actor.
    |
    \--> "TabActor" (or alike):
-          |    Actors meant to designate one document, tab, app, addon
-          |    and track its lifetime.
+          |    Actors meant to designate one context (document, tab, app,
+          |    add-on, or worker) and track its lifetime.  Generally, there is
+          |    one of these for each thing you can point a toolbox at.
           |
           \--> Tab-scoped actors:
                Actors exposing one particular feature set, this time,
-               specific to a given document/app/addon.
-               Like console, inspector actors.
+               specific to a given context (document, tab, app, add-on, or
+               worker).  Examples include the console and inspector actors.
                These actors may extend this hierarchy by having their
                own children, like LongStringActor, WalkerActor, etc.
 
 ## RootActor
 
 The root actor is special. It is automatically created when a client connects.
 It has a special `actorID` which is unique and is "root".
 All other actors have an `actorID` which is computed dynamically,
@@ -47,44 +49,53 @@ and returns its `actorID`. That's the ma
    |   that lives in the child process.
    |   Returned by "listTabs" or "getTab" requests.
    |   |
    |   \-> ContentActor (childtab.js)
    |       Targets tabs living out-of-process (e10s) or apps (on firefox OS).
    |       Returned by "connect" on RemoteBrowserActor (for tabs) or
    |       "getAppActor" on the Webapps actor (for apps).
    |
+   |-- WorkerActor (worker.js)
+   |   Targets a worker (applies to various kinds like web worker, service
+   |   worker, etc.).
+   |   Returned by "listWorkers" request to the root actor to get all workers.
+   |   Returned by "listWorkers" request to a BrowserTabActor to get workers for
+   |   a specific tab.
+   |   Returned by "listWorkers" request to a ChildProcessActor to get workers
+   |   for the chrome of the child process.
+   |
    |-- ChromeActor (chrome.js)
    |   Targets all resources in the parent process of firefox
    |   (chrome documents, JSM, JS XPCOM, etc.).
    |   Returned by "getProcess" request without any argument.
    |
    |-- ChildProcessActor (child-process.js)
    |   Targets the chrome of the child process (e10s).
    |   Returned by "getProcess" request with a id argument,
    |   matching the targeted process.
    |
    \-- BrowserAddonActor (addon.js)
-       Targets the javascript of addons.
+       Targets the javascript of add-ons.
        Returned by "listAddons" request.
 
 ## "TabActor"
 
 Those are the actors exposed by the root actors which are meant to track the
-lifetime of a given context: tab, app, process or addon. It also allows
-to fetch the tab-scoped actors connected to this context. Actors like console,
-inspector, thread (for debugger), styleinspector, etc. Most of them inherit
-from TabActor (defined in webbrowser.js) which is document centric.
-It automatically tracks the lifetime of the targeted document, but it also
-tracks its iframes and allows switching the context to one of its iframes.
-For historical reasons, these actors also handle creating the ThreadActor,
-used to manage breakpoints in the debugger. All the other tab-scoped actors are
-created when we access the TabActor's grip. We return the tab-scoped actors
-`actorID` in it. Actors inheriting from TabActor expose `attach`/`detach`
-requests, that allows to start/stop the ThreadActor.
+lifetime of a given context: tab, app, process, add-on, or worker. It also
+allows to fetch the tab-scoped actors connected to this context. Actors like
+console, inspector, thread (for debugger), styleinspector, etc. Most of them
+inherit from TabActor (defined in webbrowser.js) which is document centric. It
+automatically tracks the lifetime of the targeted document, but it also tracks
+its iframes and allows switching the context to one of its iframes. For
+historical reasons, these actors also handle creating the ThreadActor, used to
+manage breakpoints in the debugger. All the other tab-scoped actors are created
+when we access the TabActor's grip. We return the tab-scoped actors `actorID` in
+it. Actors inheriting from TabActor expose `attach`/`detach` requests, that
+allows to start/stop the ThreadActor.
 
 The tab-scoped actors expect to find the following properties on the "TabActor":
  - threadActor:
    ThreadActor instance for the given context,
    only defined once `attach` request is called, or on construction.
  - isRootActor: (historical name)
    Always false, except on ChromeActor.
    Despite the attribute name, it is being used to accept all resources
@@ -103,16 +114,16 @@ attributes and events:
    List of all document globals including the main window object and all iframes.
  - docShell:
    DocShell reference for the targeted context.
  - docShells:
    List of all docshells for the targeted document and all its iframes.
  - chromeEventHandler:
    The chrome event handler for the current context. Allows to listen to events
    that can be missing/cancelled on this document itself.
+
 See TabActor documentation for events definition.
 
-
 ## Tab-scoped actors
 
-Each of these actors focuses on providing one particular feature set, specific to one context,
-that can be a web page, an app, a top level firefox window, a process or an addon resource.
-
+Each of these actors focuses on providing one particular feature set, specific
+to one context, that can be a web page, an app, a top level firefox window, a
+process, an add-on, or a worker.
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -249,16 +249,17 @@ var gNextLoaderID = 0;
  * then a new one can also be created.
  */
 this.DevToolsLoader = function DevToolsLoader() {
   this.require = this.require.bind(this);
   this.lazyGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils);
   this.lazyImporter = XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils);
   this.lazyServiceGetter = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils);
   this.lazyRequireGetter = this.lazyRequireGetter.bind(this);
+  this.main = this.main.bind(this);
 };
 
 DevToolsLoader.prototype = {
   get provider() {
     if (!this._provider) {
       this._chooseProvider();
     }
     return this._provider;
@@ -385,17 +386,18 @@ DevToolsLoader.prototype = {
       atob: atob,
       btoa: btoa,
       _Iterator: Iterator,
       loader: {
         lazyGetter: this.lazyGetter,
         lazyImporter: this.lazyImporter,
         lazyServiceGetter: this.lazyServiceGetter,
         lazyRequireGetter: this.lazyRequireGetter,
-        id: this.id
+        id: this.id,
+        main: this.main
       },
     };
     // Lazy define console in order to load Console.jsm only when it is used
     XPCOMUtils.defineLazyGetter(this._provider.globals, "console", () => {
       return Cu.import("resource://gre/modules/Console.jsm", {}).console;
     });
 
     this._provider.load();
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -925,16 +925,19 @@ pref("toolkit.telemetry.unified", false)
 
 // Unified AccessibleCarets (touch-caret and selection-carets).
 #ifdef NIGHTLY_BUILD
 pref("layout.accessiblecaret.enabled", true);
 #else
 pref("layout.accessiblecaret.enabled", false);
 #endif
 
+// Android hides the selection bars at the two ends of the selection highlight.
+pref("layout.accessiblecaret.bar.enabled", false);
+
 // Android needs to show the caret when long tapping on an empty content.
 pref("layout.accessiblecaret.caret_shown_when_long_tapping_on_empty_content", true);
 
 // Android needs persistent carets and actionbar. Turn off the caret timeout.
 pref("layout.accessiblecaret.timeout_ms", 0);
 
 // Android generates long tap (mouse) events.
 pref("layout.accessiblecaret.use_long_tap_injector", false);
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -552,17 +552,17 @@ public class BrowserApp extends GeckoApp
         if (AndroidGamepadManager.handleKeyEvent(event)) {
             return true;
         }
         return super.onKeyUp(keyCode, event);
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
-        if (!isSupportedSystem()) {
+        if (!HardwareUtils.isSupportedSystem()) {
             // This build does not support the Android version of the device; Exit early.
             super.onCreate(savedInstanceState);
             return;
         }
 
         final Intent intent = getIntent();
 
         // Note that we're calling GeckoProfile.get *before GeckoApp.onCreate*.
@@ -1340,17 +1340,17 @@ public class BrowserApp extends GeckoApp
 
     @Override
     public void setAccessibilityEnabled(boolean enabled) {
         mDynamicToolbar.setAccessibilityEnabled(enabled);
     }
 
     @Override
     public void onDestroy() {
-        if (!isSupportedSystem()) {
+        if (!HardwareUtils.isSupportedSystem()) {
             // This build does not support the Android version of the device; Exit early.
             super.onDestroy();
             return;
         }
 
         mDynamicToolbar.destroy();
 
         if (mBrowserToolbar != null)
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1157,17 +1157,17 @@ public abstract class GeckoApp
     public void onCreate(Bundle savedInstanceState) {
         GeckoAppShell.ensureCrashHandling();
 
         // Enable Android Strict Mode for developers' local builds (the "default" channel).
         if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
             enableStrictMode();
         }
 
-        if (!isSupportedSystem()) {
+        if (!HardwareUtils.isSupportedSystem()) {
             // This build does not support the Android version of the device: Show an error and finish the app.
             super.onCreate(savedInstanceState);
             showSDKVersionError();
             finish();
             return;
         }
 
         // The clock starts...now. Better hurry!
@@ -2080,17 +2080,17 @@ public abstract class GeckoApp
             StrictMode.setThreadPolicy(savedPolicy);
         }
 
         super.onRestart();
     }
 
     @Override
     public void onDestroy() {
-        if (!isSupportedSystem()) {
+        if (!HardwareUtils.isSupportedSystem()) {
             // This build does not support the Android version of the device:
             // We did not initialize anything, so skip cleaning up.
             super.onDestroy();
             return;
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener)this,
             "Gecko:Ready",
@@ -2188,42 +2188,16 @@ public abstract class GeckoApp
                   .putExtra(Intent.EXTRA_INTENT, mRestartIntent);
             startService(intent);
         } else {
             // Exiting, so kill our own process.
             Process.killProcess(Process.myPid());
         }
     }
 
-    protected boolean isSupportedSystem() {
-        if (Build.VERSION.SDK_INT < Versions.MIN_SDK_VERSION ||
-            Build.VERSION.SDK_INT > Versions.MAX_SDK_VERSION) {
-            return false;
-        }
-
-        // See http://developer.android.com/ndk/guides/abis.html
-        boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
-        boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
-
-        boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
-        boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
-
-        // Only reject known incompatible ABIs. Better safe than sorry.
-        if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
-            return false;
-        }
-
-        if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
-            return true;
-        }
-
-        Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
-        return true;
-    }
-
     public void showSDKVersionError() {
         final String message = getString(R.string.unsupported_sdk_version, Build.CPU_ABI, Build.VERSION.SDK_INT);
         Toast.makeText(this, message, Toast.LENGTH_LONG).show();
     }
 
     // Get a temporary directory, may return null
     public static File getTempDirectory() {
         File dir = GeckoApplication.get().getExternalFilesDir("temp");
--- a/mobile/android/base/java/org/mozilla/gecko/Restrictions.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Restrictions.java
@@ -1,30 +1,30 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko;
 
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.restrictions.DefaultConfiguration;
 import org.mozilla.gecko.restrictions.GuestProfileConfiguration;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
+import org.mozilla.gecko.restrictions.RestrictionCache;
 import org.mozilla.gecko.restrictions.RestrictionConfiguration;
 
-import android.annotation.TargetApi;
-import android.content.Context;
-import android.os.Build;
-import android.os.UserManager;
-import android.util.Log;
-
 @RobocopTarget
 public class Restrictions {
     private static final String LOGTAG = "GeckoRestrictedProfiles";
 
     private static RestrictionConfiguration configuration;
 
     private static RestrictionConfiguration getConfiguration(Context context) {
         if (configuration == null) {
@@ -69,18 +69,17 @@ public class Restrictions {
         }
 
         if (Versions.preJBMR2) {
             // Early versions don't support restrictions at all
             return false;
         }
 
         // The user is on a restricted profile if, and only if, we injected application restrictions during account setup.
-        final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
-        return !mgr.getApplicationRestrictions(context.getPackageName()).isEmpty();
+        return RestrictionCache.hasApplicationRestrictions(context);
     }
 
     public static void update(Context context) {
         getConfiguration(context).update();
     }
 
     private static Restrictable geckoActionToRestriction(int action) {
         for (Restrictable rest : Restrictable.values()) {
--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadContentService.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko.dlc;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.dlc.catalog.DownloadContent;
 import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
+import org.mozilla.gecko.util.HardwareUtils;
 
 import android.app.IntentService;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.util.Log;
 
 /**
@@ -59,16 +60,22 @@ public class DownloadContentService exte
     }
 
     protected void onHandleIntent(Intent intent) {
         if (!AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
             Log.w(LOGTAG, "Download content is not enabled. Stop.");
             return;
         }
 
+        if (!HardwareUtils.isSupportedSystem()) {
+            // This service is running very early before checks in BrowserApp can prevent us from running.
+            Log.w(LOGTAG, "System is not supported. Stop.");
+            return;
+        }
+
         if (intent == null) {
             return;
         }
 
         final BaseAction action;
 
         switch (intent.getAction()) {
             case ACTION_STUDY_CATALOG:
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictedProfileConfiguration.java
@@ -61,58 +61,34 @@ public class RestrictedProfileConfigurat
         return hiddenRestrictions.contains(restrictable);
     }
 
     /* package-private */ static Map<Restrictable, Boolean> getConfiguration() {
         return configuration;
     }
 
     private Context context;
-    private Bundle cachedAppRestrictions;
-    private Bundle cachedUserRestrictions;
-    private boolean isCacheInvalid = true;
 
     public RestrictedProfileConfiguration(Context context) {
         this.context = context.getApplicationContext();
     }
 
     @Override
     public synchronized boolean isAllowed(Restrictable restrictable) {
-        if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
-            readRestrictions();
-            isCacheInvalid = false;
+        // Special casing system/user restrictions
+        if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
+            return RestrictionCache.getUserRestriction(context, restrictable.name);
         }
 
-        // Special casing system/user restrictions
-        if (restrictable == Restrictable.INSTALL_APPS || restrictable == Restrictable.MODIFY_ACCOUNTS) {
-            return !cachedUserRestrictions.getBoolean(restrictable.name);
-        }
-
-        if (!cachedAppRestrictions.containsKey(restrictable.name) && !configuration.containsKey(restrictable)) {
+        if (!RestrictionCache.hasApplicationRestriction(context, restrictable.name) && !configuration.containsKey(restrictable)) {
             // Always allow features that are not in the configuration
             return true;
         }
 
-        return cachedAppRestrictions.getBoolean(restrictable.name, configuration.get(restrictable));
-    }
-
-    private void readRestrictions() {
-        final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
-
-        StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
-
-        try {
-            Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
-            migrateRestrictionsIfNeeded(appRestrictions);
-
-            cachedAppRestrictions = appRestrictions;
-            cachedUserRestrictions = mgr.getUserRestrictions();
-        } finally {
-            StrictMode.setThreadPolicy(policy);
-        }
+        return RestrictionCache.getApplicationRestriction(context, restrictable.name, configuration.get(restrictable));
     }
 
     @Override
     public boolean canLoadUrl(String url) {
         if (!isAllowed(Restrictable.INSTALL_EXTENSION) && AboutPages.isAboutAddons(url)) {
             return false;
         }
 
@@ -130,45 +106,24 @@ public class RestrictedProfileConfigurat
 
     @Override
     public boolean isRestricted() {
         return true;
     }
 
     @Override
     public synchronized void update() {
-        isCacheInvalid = true;
+        RestrictionCache.invalidate();
     }
 
     public static List<Restrictable> getVisibleRestrictions() {
         final List<Restrictable> visibleList = new ArrayList<>();
 
         for (Restrictable restrictable : configuration.keySet()) {
             if (hiddenRestrictions.contains(restrictable)) {
                 continue;
             }
             visibleList.add(restrictable);
         }
 
         return visibleList;
     }
-
-    /**
-     * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
-     */
-    public static void migrateRestrictionsIfNeeded(Bundle bundle) {
-        if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
-            bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
-        }
-
-        if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
-            bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
-        }
-
-        if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
-            bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
-        }
-
-        if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
-            bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
-        }
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionCache.java
@@ -0,0 +1,99 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.gecko.restrictions;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.StrictMode;
+import android.os.UserManager;
+
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Cache for user and application restrictions.
+ */
+public class RestrictionCache {
+    private static Bundle cachedAppRestrictions;
+    private static Bundle cachedUserRestrictions;
+    private static boolean isCacheInvalid = true;
+
+    private RestrictionCache() {}
+
+    public static synchronized boolean getUserRestriction(Context context, String restriction) {
+        updateCacheIfNeeded(context);
+        return cachedUserRestrictions.getBoolean(restriction);
+    }
+
+    public static synchronized boolean hasApplicationRestriction(Context context, String restriction) {
+        updateCacheIfNeeded(context);
+        return cachedAppRestrictions.containsKey(restriction);
+    }
+
+    public static synchronized boolean getApplicationRestriction(Context context, String restriction, boolean defaultValue) {
+        updateCacheIfNeeded(context);
+        return cachedAppRestrictions.getBoolean(restriction, defaultValue);
+    }
+
+    public static synchronized boolean hasApplicationRestrictions(Context context) {
+        updateCacheIfNeeded(context);
+        return !cachedAppRestrictions.isEmpty();
+    }
+
+    public static synchronized void invalidate() {
+        isCacheInvalid = true;
+    }
+
+    private static void updateCacheIfNeeded(Context context) {
+        // If we are not on the UI thread then we can just go ahead and read the values (Bug 1189347).
+        // Otherwise we read from the cache to avoid blocking the UI thread. If the cache is invalid
+        // then we hazard the consequences and just do the read.
+        if (isCacheInvalid || !ThreadUtils.isOnUiThread()) {
+            readRestrictions(context);
+            isCacheInvalid = false;
+        }
+    }
+
+    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+    private static void readRestrictions(Context context) {
+        final UserManager mgr = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+        // If we do not have anything in the cache yet then this read might happen on the UI thread (Bug 1189347).
+        final StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+
+        try {
+            Bundle appRestrictions = mgr.getApplicationRestrictions(context.getPackageName());
+            migrateRestrictionsIfNeeded(appRestrictions);
+
+            cachedAppRestrictions = appRestrictions;
+            cachedUserRestrictions = mgr.getUserRestrictions(); // Always implies disk read
+        } finally {
+            StrictMode.setThreadPolicy(policy);
+        }
+    }
+
+    /**
+     * This method migrates the old set of DISALLOW_ restrictions to the new restrictable feature ones (Bug 1189336).
+     */
+    /* package-private */ static void migrateRestrictionsIfNeeded(Bundle bundle) {
+        if (!bundle.containsKey(Restrictable.INSTALL_EXTENSION.name) && bundle.containsKey("no_install_extensions")) {
+            bundle.putBoolean(Restrictable.INSTALL_EXTENSION.name, !bundle.getBoolean("no_install_extensions"));
+        }
+
+        if (!bundle.containsKey(Restrictable.PRIVATE_BROWSING.name) && bundle.containsKey("no_private_browsing")) {
+            bundle.putBoolean(Restrictable.PRIVATE_BROWSING.name, !bundle.getBoolean("no_private_browsing"));
+        }
+
+        if (!bundle.containsKey(Restrictable.CLEAR_HISTORY.name) && bundle.containsKey("no_clear_history")) {
+            bundle.putBoolean(Restrictable.CLEAR_HISTORY.name, !bundle.getBoolean("no_clear_history"));
+        }
+
+        if (!bundle.containsKey(Restrictable.ADVANCED_SETTINGS.name) && bundle.containsKey("no_advanced_settings")) {
+            bundle.putBoolean(Restrictable.ADVANCED_SETTINGS.name, !bundle.getBoolean("no_advanced_settings"));
+        }
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/restrictions/RestrictionProvider.java
@@ -33,17 +33,17 @@ public class RestrictionProvider extends
         }
 
         final PendingResult result = goAsync();
 
         new Thread() {
             @Override
             public void run() {
                 final Bundle oldRestrictions = intent.getBundleExtra(Intent.EXTRA_RESTRICTIONS_BUNDLE);
-                RestrictedProfileConfiguration.migrateRestrictionsIfNeeded(oldRestrictions);
+                RestrictionCache.migrateRestrictionsIfNeeded(oldRestrictions);
 
                 final Bundle extras = new Bundle();
 
                 ArrayList<RestrictionEntry> entries = initRestrictions(context, oldRestrictions);
                 extras.putParcelableArrayList(Intent.EXTRA_RESTRICTIONS_LIST, entries);
 
                 result.setResult(Activity.RESULT_OK, null, extras);
                 result.finish();
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
@@ -2,27 +2,28 @@
  * 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/. */
 
 package org.mozilla.gecko.telemetry;
 
 import android.content.Context;
 import android.os.Build;
-import java.io.IOException;
-import java.util.Locale;
 
 import com.keepsafe.switchboard.SwitchBoard;
-import org.json.JSONArray;
+
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
 import org.mozilla.gecko.util.StringUtils;
 
+import java.io.IOException;
+import java.util.Locale;
+
 /**
  * A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
  */
 public class TelemetryPingGenerator {
 
     // In the server url, the initial path directly after the "scheme://host:port/"
     private static final String SERVER_INITIAL_PATH = "submit/telemetry";
 
@@ -82,26 +83,19 @@ public class TelemetryPingGenerator {
 
         ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
         ping.put(CorePing.CLIENT_ID, clientId);
         ping.put(CorePing.DEVICE, deviceDescriptor);
         ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
         ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
         ping.put(CorePing.SEQ, seq);
         if (AppConstants.MOZ_SWITCHBOARD) {
-            ping.put(CorePing.EXPERIMENTS, getActiveExperiments(context));
+            ping.putArray(CorePing.EXPERIMENTS, SwitchBoard.getActiveExperiments(context));
         }
         // TODO (bug 1246816): Remove this "optional" parameter work-around when
         // GeckoProfile.getAndPersistProfileCreationDateFromFilesystem is implemented. That method returns -1
         // while it's not implemented so we don't include the parameter in the ping if that's the case.
         if (profileCreationDate >= 0) {
             ping.put(CorePing.PROFILE_CREATION_DATE, profileCreationDate);
         }
         return ping;
     }
-
-    private static JSONArray getActiveExperiments(final Context context) {
-        if (!AppConstants.MOZ_SWITCHBOARD) {
-            throw new IllegalStateException("This method should not be called with switchboard disabled");
-        }
-        return new JSONArray(SwitchBoard.getActiveExperiments(context));
-    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/HardwareUtils.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.util;
 
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.SysInfo;
 
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.os.Build;
 import android.util.Log;
 import android.view.ViewConfiguration;
@@ -92,9 +93,38 @@ public final class HardwareUtils {
         // for some reason.
         if (memSize == 0) {
             Log.w(LOGTAG, "Could not compute system memory. Falling back to isLowMemoryPlatform = false.");
             return false;
         }
 
         return memSize < LOW_MEMORY_THRESHOLD_MB;
     }
+
+    /**
+     * @return false if the current system is not supported (e.g. APK/system ABI mismatch).
+     */
+    public static boolean isSupportedSystem() {
+        if (Build.VERSION.SDK_INT < AppConstants.Versions.MIN_SDK_VERSION ||
+            Build.VERSION.SDK_INT > AppConstants.Versions.MAX_SDK_VERSION) {
+            return false;
+        }
+
+        // See http://developer.android.com/ndk/guides/abis.html
+        boolean isSystemARM = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("arm");
+        boolean isSystemX86 = Build.CPU_ABI != null && Build.CPU_ABI.startsWith("x86");
+
+        boolean isAppARM = AppConstants.ANDROID_CPU_ARCH.startsWith("arm");
+        boolean isAppX86 = AppConstants.ANDROID_CPU_ARCH.startsWith("x86");
+
+        // Only reject known incompatible ABIs. Better safe than sorry.
+        if ((isSystemX86 && isAppARM) || (isSystemARM && isAppX86)) {
+            return false;
+        }
+
+        if ((isSystemX86 && isAppX86) || (isSystemARM && isAppARM)) {
+            return true;
+        }
+
+        Log.w(LOGTAG, "Unknown app/system ABI combination: " + AppConstants.MOZ_APP_ABI + " / " + Build.CPU_ABI);
+        return true;
+    }
 }
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -500,16 +500,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'RemoteClientsDialogFragment.java',
     'RemoteTabsExpandableListAdapter.java',
     'Restarter.java',
     'Restrictions.java',
     'restrictions/DefaultConfiguration.java',
     'restrictions/GuestProfileConfiguration.java',
     'restrictions/Restrictable.java',
     'restrictions/RestrictedProfileConfiguration.java',
+    'restrictions/RestrictionCache.java',
     'restrictions/RestrictionConfiguration.java',
     'restrictions/RestrictionProvider.java',
     'ScreenshotObserver.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentity.java',
     'SmsManager.java',
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1311,18 +1311,21 @@ var BrowserApp = {
 
     if (aShowUndoSnackbar) {
       // Get a title for the undo close snackbar. Fall back to the URL if there is no title.
       let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
       let closedTabData = ss.getClosedTabs(window)[0];
 
       let message;
       let title = closedTabData.entries[closedTabData.index - 1].title;
-
-      if (title) {
+      let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser);
+
+      if (isPrivate) {
+        message = Strings.browser.GetStringFromName("privateClosedMessage.message");
+      } else if (title) {
         message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1);
       } else {
         message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault");
       }
 
       Snackbars.show(message, Snackbars.LENGTH_SHORT, {
         action: {
           label: Strings.browser.GetStringFromName("undoCloseToast.action2"),
--- a/mobile/android/locales/en-US/chrome/browser.properties
+++ b/mobile/android/locales/en-US/chrome/browser.properties
@@ -183,16 +183,21 @@ newprivatetabpopup.opened=New private ta
 # button label) and upper-case, to match Google and Android's convention.
 newtabpopup.switch=SWITCH
 
 # Undo close tab toast
 # LOCALIZATION NOTE (undoCloseToast.message): This message appears in a toast
 # when the user closes a tab. %S is the title of the tab that was closed.
 undoCloseToast.message=Closed %S
 
+# Private Tab closed message
+# LOCALIZATION NOTE (privateClosedMessage.message): This message appears
+# when the user closes a private tab.
+privateClosedMessage.message=Closed Private Browsing
+
 # LOCALIZATION NOTE (undoCloseToast.messageDefault): This message appears in a
 # toast when the user closes a tab if there is no title to display.
 undoCloseToast.messageDefault=Closed tab
 
 # LOCALIZATION NOTE (undoCloseToast.action2): Ideally, this string is short (it's a
 # button label) and upper-case, to match Google and Android's convention.
 undoCloseToast.action2=UNDO
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/browserid/JSONWebTokenUtils.java
@@ -1,53 +1,44 @@
 /* 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/. */
 
 package org.mozilla.gecko.browserid;
 
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.Map;
-import java.util.TreeMap;
-
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.apache.commons.codec.binary.StringUtils;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.TreeMap;
+
 /**
  * Encode and decode JSON Web Tokens.
  * <p>
  * Reverse-engineered from the Node.js jwcrypto library at
  * <a href="https://github.com/mozilla/jwcrypto">https://github.com/mozilla/jwcrypto</a>
  * and informed by the informal draft standard "JSON Web Token (JWT)" at
  * <a href="http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html">http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html</a>.
  */
 public class JSONWebTokenUtils {
   public static final long DEFAULT_CERTIFICATE_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
   public static final long DEFAULT_ASSERTION_DURATION_IN_MILLISECONDS = 60 * 60 * 1000;
   public static final long DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS = 9999999999999L;
   public static final String DEFAULT_CERTIFICATE_ISSUER = "127.0.0.1";
   public static final String DEFAULT_ASSERTION_ISSUER = "127.0.0.1";
 
   public static String encode(String payload, SigningPrivateKey privateKey) throws UnsupportedEncodingException, GeneralSecurityException  {
-    return encode(payload, privateKey, null);
-  }
-
-  protected static String encode(String payload, SigningPrivateKey privateKey, Map<String, Object> headerFields) throws UnsupportedEncodingException, GeneralSecurityException  {
-    ExtendedJSONObject header = new ExtendedJSONObject();
-    if (headerFields != null) {
-      header.putAll(headerFields);
-    }
+    final ExtendedJSONObject header = new ExtendedJSONObject();
     header.put("alg", privateKey.getAlgorithm());
     String encodedHeader  = Base64.encodeBase64URLSafeString(header.toJSONString().getBytes("UTF-8"));
     String encodedPayload = Base64.encodeBase64URLSafeString(payload.getBytes("UTF-8"));
     ArrayList<String> segments = new ArrayList<String>();
     segments.add(encodedHeader);
     segments.add(encodedPayload);
     byte[] message = Utils.toDelimitedString(".", segments).getBytes("UTF-8");
     byte[] signature = privateKey.signMessage(message);
@@ -73,18 +64,17 @@ public class JSONWebTokenUtils {
     return payload;
   }
 
   /**
    * Public for testing.
    */
   @SuppressWarnings("unchecked")
   public static String getPayloadString(String payloadString, String audience, String issuer,
-      Long issuedAt, long expiresAt) throws NonObjectJSONException,
-      IOException, ParseException {
+      Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException {
     ExtendedJSONObject payload;
     if (payloadString != null) {
       payload = new ExtendedJSONObject(payloadString);
     } else {
       payload = new ExtendedJSONObject();
     }
     if (audience != null) {
       payload.put("aud", audience);
@@ -93,27 +83,27 @@ public class JSONWebTokenUtils {
     if (issuedAt != null) {
       payload.put("iat", issuedAt);
     }
     payload.put("exp", expiresAt);
     // TreeMap so that keys are sorted. A small attempt to keep output stable over time.
     return JSONObject.toJSONString(new TreeMap<Object, Object>(payload.object));
   }
 
-  protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException, ParseException  {
+  protected static String getCertificatePayloadString(VerifyingPublicKey publicKeyToSign, String email) throws NonObjectJSONException, IOException  {
     ExtendedJSONObject payload = new ExtendedJSONObject();
     ExtendedJSONObject principal = new ExtendedJSONObject();
     principal.put("email", email);
     payload.put("principal", principal);
     payload.put("public-key", publicKeyToSign.toJSONObject());
     return payload.toJSONString();
   }
 
   public static String createCertificate(VerifyingPublicKey publicKeyToSign, String email,
-      String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+      String issuer, long issuedAt, long expiresAt, SigningPrivateKey privateKey) throws NonObjectJSONException, IOException, GeneralSecurityException  {
     String certificatePayloadString = getCertificatePayloadString(publicKeyToSign, email);
     String payloadString = getPayloadString(certificatePayloadString, null, issuer, issuedAt, expiresAt);
     return JSONWebTokenUtils.encode(payloadString, privateKey);
   }
 
   /**
    * Create a Browser ID assertion.
    *
@@ -130,21 +120,20 @@ public class JSONWebTokenUtils {
    * @param issuedAt
    *          timestamp for assertion, in milliseconds since the epoch; if null,
    *          no timestamp is included.
    * @param expiresAt
    *          expiration timestamp for assertion, in milliseconds since the epoch.
    * @return assertion.
    * @throws NonObjectJSONException
    * @throws IOException
-   * @throws ParseException
    * @throws GeneralSecurityException
    */
   public static String createAssertion(SigningPrivateKey privateKeyToSignWith, String certificate, String audience,
-      String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException  {
+      String issuer, Long issuedAt, long expiresAt) throws NonObjectJSONException, IOException, GeneralSecurityException  {
     String emptyAssertionPayloadString = "{}";
     String payloadString = getPayloadString(emptyAssertionPayloadString, audience, issuer, issuedAt, expiresAt);
     String signature = JSONWebTokenUtils.encode(payloadString, privateKeyToSignWith);
     return certificate + "~" + signature;
   }
 
   /**
    * For debugging only!
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/authenticator/AccountPickler.java
@@ -169,17 +169,17 @@ public class AccountPickler {
     final String jsonString = Utils.readFile(context, filename);
     if (jsonString == null) {
       Logger.info(LOG_TAG, "Pickle file '" + filename + "' not found; aborting.");
       return null;
     }
 
     ExtendedJSONObject json = null;
     try {
-      json = ExtendedJSONObject.parseJSONObject(jsonString);
+      json = new ExtendedJSONObject(jsonString);
     } catch (Exception e) {
       Logger.warn(LOG_TAG, "Got exception reading pickle file '" + filename + "'; aborting.", e);
       return null;
     }
 
     final UnpickleParams params;
     try {
       params = UnpickleParams.fromJSON(json);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/fxa/login/Married.java
@@ -8,17 +8,16 @@ import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.security.GeneralSecurityException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.fxa.FxAccountUtils;
 import org.mozilla.gecko.browserid.BrowserIDKeyPair;
 import org.mozilla.gecko.browserid.JSONWebTokenUtils;
 import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
 import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
@@ -50,17 +49,17 @@ public class Married extends TokensAndKe
     return o;
   }
 
   @Override
   public void execute(final ExecuteDelegate delegate) {
     delegate.handleTransition(new LogMessage("staying married"), this);
   }
 
-  public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, ParseException, GeneralSecurityException {
+  public String generateAssertion(String audience, String issuer) throws NonObjectJSONException, IOException, GeneralSecurityException {
     // We generate assertions with no iat and an exp after 2050 to avoid
     // invalid-timestamp errors from the token server.
     final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS;
     String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt);
     if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
       return assertion;
     }
 
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CollectionKeys.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko.sync;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map.Entry;
 import java.util.Set;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 
 public class CollectionKeys {
   private KeyBundle                  defaultKeyBundle     = null;
   private final HashMap<String, KeyBundle> collectionKeyBundles = new HashMap<String, KeyBundle>();
 
@@ -103,17 +102,17 @@ public class CollectionKeys {
    *
    * @param keys
    *          A "crypto/keys" <code>CryptoRecord</code>, encrypted with
    *          <code>syncKeyBundle</code> if <code>syncKeyBundle</code> is non-null.
    * @param syncKeyBundle
    *          If non-null, the sync key bundle to decrypt <code>keys</code> with.
    */
   public void setKeyPairsFromWBO(CryptoRecord keys, KeyBundle syncKeyBundle)
-      throws CryptoException, IOException, ParseException, NonObjectJSONException {
+      throws CryptoException, IOException, NonObjectJSONException {
     if (keys == null) {
       throw new IllegalArgumentException("cannot set key pairs from null record");
     }
     if (syncKeyBundle != null) {
       keys.keyBundle = syncKeyBundle;
       keys.decrypt();
     }
     ExtendedJSONObject cleartext = keys.payload;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/CryptoRecord.java
@@ -3,17 +3,16 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.CryptoInfo;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.crypto.MissingCryptoInputException;
 import org.mozilla.gecko.sync.crypto.NoKeyBundleException;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.repositories.domain.RecordParseException;
@@ -82,18 +81,19 @@ public class CryptoRecord extends Record
     super(null, null, 0, false);
     if (payload == null) {
       throw new IllegalArgumentException(
           "No payload provided to CryptoRecord constructor.");
     }
     this.payload = payload;
   }
 
-  public CryptoRecord(String jsonString) throws IOException, ParseException, NonObjectJSONException {
-    this(ExtendedJSONObject.parseJSONObject(jsonString));
+  public CryptoRecord(String jsonString) throws IOException, NonObjectJSONException {
+
+    this(new ExtendedJSONObject(jsonString));
   }
 
   /**
    * Create a new CryptoRecord with the same metadata as an existing record.
    *
    * @param source
    */
   public CryptoRecord(Record source) {
@@ -120,35 +120,34 @@ public class CryptoRecord extends Record
    *
    * and turn it into a CryptoRecord object.
    *
    * @param jsonRecord
    * @return
    *        A CryptoRecord that encapsulates the provided record.
    *
    * @throws NonObjectJSONException
-   * @throws ParseException
    * @throws IOException
    */
   public static CryptoRecord fromJSONRecord(String jsonRecord)
-      throws ParseException, NonObjectJSONException, IOException, RecordParseException {
+      throws NonObjectJSONException, IOException, RecordParseException {
     byte[] bytes = jsonRecord.getBytes("UTF-8");
     ExtendedJSONObject object = ExtendedJSONObject.parseUTF8AsJSONObject(bytes);
 
     return CryptoRecord.fromJSONRecord(object);
   }
 
   // TODO: defensive programming.
   public static CryptoRecord fromJSONRecord(ExtendedJSONObject jsonRecord)
-      throws IOException, ParseException, NonObjectJSONException, RecordParseException {
+      throws IOException, NonObjectJSONException, RecordParseException {
     String id                  = (String) jsonRecord.get(KEY_ID);
     String collection          = (String) jsonRecord.get(KEY_COLLECTION);
     String jsonEncodedPayload  = (String) jsonRecord.get(KEY_PAYLOAD);
 
-    ExtendedJSONObject payload = ExtendedJSONObject.parseJSONObject(jsonEncodedPayload);
+    ExtendedJSONObject payload = new ExtendedJSONObject(jsonEncodedPayload);
 
     CryptoRecord record = new CryptoRecord(payload);
     record.guid         = id;
     record.collection   = collection;
     if (jsonRecord.containsKey(KEY_MODIFIED)) {
       Long timestamp = jsonRecord.getTimestamp(KEY_MODIFIED);
       if (timestamp == null) {
         throw new RecordParseException("timestamp could not be parsed");
@@ -176,18 +175,17 @@ public class CryptoRecord extends Record
     // TODO: deleted?
     return record;
   }
 
   public void setKeyBundle(KeyBundle bundle) {
     this.keyBundle = bundle;
   }
 
-  public CryptoRecord decrypt() throws CryptoException, IOException, ParseException,
-                       NonObjectJSONException {
+  public CryptoRecord decrypt() throws CryptoException, IOException, NonObjectJSONException {
     if (keyBundle == null) {
       throw new NoKeyBundleException();
     }
 
     // Check that payload contains all pieces for crypto.
     if (!payload.containsKey(KEY_CIPHERTEXT) ||
         !payload.containsKey(KEY_IV) ||
         !payload.containsKey(KEY_HMAC)) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/ExtendedJSONObject.java
@@ -1,28 +1,29 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync;
 
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringReader;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
 import org.json.simple.parser.JSONParser;
 import org.json.simple.parser.ParseException;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
 
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
 /**
  * Extend JSONObject to do little things, like, y'know, accessing members.
  *
  * @author rnewman
  *
  */
 public class ExtendedJSONObject {
 
@@ -100,130 +101,80 @@ public class ExtendedJSONObject {
   }
 
   /**
    * Helper method to get a JSON array from a string.
    * <p>
    * You should prefer the stream interface {@link #parseJSONArray(Reader)}.
    *
    * @param jsonString input.
-   * @throws ParseException
    * @throws IOException
-   * @throws NonArrayJSONException if the object is valid JSON, but not an array.
+   * @throws NonArrayJSONException if the object is invalid JSON or not an array.
    */
   public static JSONArray parseJSONArray(String jsonString)
-      throws IOException, ParseException, NonArrayJSONException {
-    Object o = parseRaw(jsonString);
+      throws IOException, NonArrayJSONException {
+    Object o = null;
+    try {
+      o = parseRaw(jsonString);
+    } catch (ParseException e) {
+      throw new NonArrayJSONException(e);
+    }
 
     if (o == null) {
       return null;
     }
 
     if (o instanceof JSONArray) {
       return (JSONArray) o;
     }
 
     throw new NonArrayJSONException("value must be a JSON array");
   }
 
   /**
-   * Helper method to get a JSON object from a stream.
-   *
-   * @param in input {@link Reader}.
-   * @throws ParseException
-   * @throws IOException
-   * @throws NonArrayJSONException if the object is valid JSON, but not an object.
-   */
-  public static ExtendedJSONObject parseJSONObject(Reader in)
-      throws IOException, ParseException, NonObjectJSONException {
-    return new ExtendedJSONObject(in);
-  }
-
-  /**
-   * Helper method to get a JSON object from a string.
-   * <p>
-   * You should prefer the stream interface {@link #parseJSONObject(Reader)}.
-   *
-   * @param jsonString input.
-   * @throws ParseException
-   * @throws IOException
-   * @throws NonObjectJSONException if the object is valid JSON, but not an object.
-   */
-  public static ExtendedJSONObject parseJSONObject(String jsonString)
-      throws IOException, ParseException, NonObjectJSONException {
-    return new ExtendedJSONObject(jsonString);
-  }
-
-  /**
    * Helper method to get a JSON object from a UTF-8 byte array.
    *
    * @param in UTF-8 bytes.
-   * @throws ParseException
-   * @throws NonObjectJSONException if the object is valid JSON, but not an object.
+   * @throws NonObjectJSONException if the object is not valid JSON or not an object.
    * @throws IOException
    */
   public static ExtendedJSONObject parseUTF8AsJSONObject(byte[] in)
-      throws ParseException, NonObjectJSONException, IOException {
-    return parseJSONObject(new String(in, "UTF-8"));
+      throws NonObjectJSONException, IOException {
+    return new ExtendedJSONObject(new String(in, "UTF-8"));
   }
 
   public ExtendedJSONObject() {
     this.object = new JSONObject();
   }
 
   public ExtendedJSONObject(JSONObject o) {
     this.object = o;
   }
 
-  public ExtendedJSONObject deepCopy() {
-    final ExtendedJSONObject out = new ExtendedJSONObject();
-    @SuppressWarnings("unchecked")
-    final Set<Map.Entry<String, Object>> entries = this.object.entrySet();
-    for (Map.Entry<String, Object> entry : entries) {
-      final String key = entry.getKey();
-      final Object value = entry.getValue();
-      if (value instanceof JSONArray) {
-        // Oh god.
-        try {
-          out.put(key, new JSONParser().parse(((JSONArray) value).toJSONString()));
-        } catch (ParseException e) {
-          // This should never occur, because we're round-tripping.
-        }
-        continue;
-      }
-      if (value instanceof JSONObject) {
-        out.put(key, new ExtendedJSONObject((JSONObject) value).deepCopy().object);
-        continue;
-      }
-      if (value instanceof ExtendedJSONObject) {
-        out.put(key, ((ExtendedJSONObject) value).deepCopy());
-        continue;
-      }
-      // Oh well.
-      out.put(key, value);
-    }
-
-    return out;
-  }
-
-  public ExtendedJSONObject(Reader in) throws IOException, ParseException, NonObjectJSONException {
+  public ExtendedJSONObject(Reader in) throws IOException, NonObjectJSONException {
     if (in == null) {
       this.object = new JSONObject();
       return;
     }
 
-    Object obj = parseRaw(in);
+    Object obj = null;
+    try {
+      obj = parseRaw(in);
+    } catch (ParseException e) {
+      throw new NonObjectJSONException(e);
+    }
+
     if (obj instanceof JSONObject) {
       this.object = ((JSONObject) obj);
     } else {
       throw new NonObjectJSONException("value must be a JSON object");
     }
   }
 
-  public ExtendedJSONObject(String jsonString) throws IOException, ParseException, NonObjectJSONException {
+  public ExtendedJSONObject(String jsonString) throws IOException, NonObjectJSONException {
     this(jsonString == null ? null : new StringReader(jsonString));
   }
 
   @Override
   public ExtendedJSONObject clone() {
     return new ExtendedJSONObject((JSONObject) this.object.clone());
   }
 
@@ -314,25 +265,52 @@ public class ExtendedJSONObject {
     return this.object.toJSONString();
   }
 
   @Override
   public String toString() {
     return this.object.toString();
   }
 
-  public void put(String key, Object value) {
+  protected void putRaw(String key, Object value) {
     @SuppressWarnings("unchecked")
     Map<Object, Object> map = this.object;
     map.put(key, value);
   }
 
-  @SuppressWarnings({ "unchecked", "rawtypes" })
-  public void putAll(Map map) {
-    this.object.putAll(map);
+  public void put(String key, String value) {
+    this.putRaw(key, value);
+  }
+
+  public void put(String key, boolean value) {
+    this.putRaw(key, value);
+  }
+
+  public void put(String key, long value) {
+    this.putRaw(key, value);
+  }
+
+  public void put(String key, int value) {
+    this.putRaw(key, value);
+  }
+
+  public void put(String key, ExtendedJSONObject value) {
+    this.putRaw(key, value);
+  }
+
+  public void put(String key, JSONArray value) {
+    this.putRaw(key, value);
+  }
+
+  @SuppressWarnings("unchecked")
+  public void putArray(String key, List<String> value) {
+    // Frustratingly inefficient, but there you have it.
+    final JSONArray jsonArray = new JSONArray();
+    jsonArray.addAll(value);
+    this.putRaw(key, jsonArray);
   }
 
   /**
    * Remove key-value pair from JSONObject.
    *
    * @param key
    *          to be removed.
    * @return true if key exists and was removed, false otherwise.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/GlobalSession.java
@@ -2,23 +2,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.sync;
 
 import android.content.Context;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
-import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.FreshStartDelegate;
+import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.delegates.JSONRecordFetchDelegate;
 import org.mozilla.gecko.sync.delegates.KeyUploadDelegate;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.delegates.WipeServerDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.HttpResponseObserver;
 import org.mozilla.gecko.sync.net.SyncResponse;
@@ -97,17 +96,17 @@ public class GlobalSession implements Ht
   public URI wboURI(String collection, String id) throws URISyntaxException {
     return config.wboURI(collection, id);
   }
 
   public GlobalSession(SyncConfiguration config,
                        GlobalSessionCallback callback,
                        Context context,
                        ClientsDataDelegate clientsDelegate)
-    throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
+    throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
 
     if (callback == null) {
       throw new IllegalArgumentException("Must provide a callback to GlobalSession constructor.");
     }
 
     this.callback        = callback;
     this.context         = context;
     this.clientsDelegate = clientsDelegate;
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/MetaGlobal.java
@@ -8,17 +8,16 @@ import java.io.IOException;
 import java.net.URISyntaxException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedSyncIDException;
 import org.mozilla.gecko.sync.MetaGlobalException.MetaGlobalMalformedVersionException;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
@@ -51,17 +50,17 @@ public class MetaGlobal implements SyncS
   }
 
   public void fetch(MetaGlobalDelegate delegate) {
     this.callback = delegate;
     try {
       this.isUploading = false;
       SyncStorageRecordRequest r = new SyncStorageRecordRequest(this.metaURL);
       r.delegate = this;
-      r.deferGet();
+      r.get();
     } catch (URISyntaxException e) {
       this.callback.handleError(e);
     }
   }
 
   public void upload(MetaGlobalDelegate callback) {
     try {
       this.isUploading = true;
@@ -92,17 +91,17 @@ public class MetaGlobal implements SyncS
     ExtendedJSONObject payload = this.asRecordContents();
     CryptoRecord record = new CryptoRecord(payload);
     record.collection = "meta";
     record.guid       = "global";
     record.deleted    = false;
     return record;
   }
 
-  public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, ParseException, NonObjectJSONException, NonArrayJSONException {
+  public void setFromRecord(CryptoRecord record) throws IllegalStateException, IOException, NonObjectJSONException, NonArrayJSONException {
     if (record == null) {
       throw new IllegalArgumentException("Cannot set meta/global from null record");
     }
     Logger.debug(LOG_TAG, "meta/global is " + record.payload.toJSONString());
     this.storageVersion = (Long) record.payload.get("storageVersion");
     this.syncID = (String) record.payload.get("syncID");
 
     setEngines(record.payload.getObject("engines"));
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonArrayJSONException.java
@@ -5,9 +5,13 @@
 package org.mozilla.gecko.sync;
 
 public class NonArrayJSONException extends UnexpectedJSONException {
   private static final long serialVersionUID = 5582918057432365749L;
 
   public NonArrayJSONException(String detailMessage) {
     super(detailMessage);
   }
+
+  public NonArrayJSONException(Throwable throwable) {
+    super(throwable);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/NonObjectJSONException.java
@@ -5,9 +5,13 @@
 package org.mozilla.gecko.sync;
 
 public class NonObjectJSONException extends UnexpectedJSONException {
   private static final long serialVersionUID = 2214238763035650087L;
 
   public NonObjectJSONException(String detailMessage) {
     super(detailMessage);
   }
+
+  public NonObjectJSONException(Throwable throwable) {
+    super(throwable);
+  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SyncConfiguration.java
@@ -166,17 +166,17 @@ public class SyncConfiguration {
    *         or null otherwise.
    */
   protected static Set<String> getEngineNamesFromPref(SharedPreferences prefs, String pref) {
     final String json = prefs.getString(pref, null);
     if (json == null) {
       return null;
     }
     try {
-      final ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
+      final ExtendedJSONObject o = new ExtendedJSONObject(json);
       return new HashSet<String>(o.keySet());
     } catch (Exception e) {
       return null;
     }
   }
 
   /**
    * Returns the set of engine names that the user has enabled. If none
@@ -207,17 +207,17 @@ public class SyncConfiguration {
    *         engine name, Value is the new sync state.
    */
   public static Map<String, Boolean> getUserSelectedEngines(SharedPreferences prefs) {
     String json = prefs.getString(PREF_USER_SELECTED_ENGINES_TO_SYNC, null);
     if (json == null) {
       return null;
     }
     try {
-      ExtendedJSONObject o = ExtendedJSONObject.parseJSONObject(json);
+      ExtendedJSONObject o = new ExtendedJSONObject(json);
       Map<String, Boolean> map = new HashMap<String, Boolean>();
       for (Entry<String, Object> e : o.entrySet()) {
         String key = e.getKey();
         Boolean value = (Boolean) e.getValue();
         map.put(key, value);
         // Forms depends on history. Add forms if history is selected.
         if ("history".equals(key)) {
           map.put("forms", value);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/SynchronizerConfiguration.java
@@ -1,42 +1,41 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync;
 
-import java.io.IOException;
+import android.content.SharedPreferences.Editor;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.PrefsBranch;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.repositories.RepositorySessionBundle;
 
-import android.content.SharedPreferences.Editor;
+import java.io.IOException;
 
 public class SynchronizerConfiguration {
   private static final String LOG_TAG = "SynczrConfiguration";
 
   public String syncID;
   public RepositorySessionBundle remoteBundle;
   public RepositorySessionBundle localBundle;
 
-  public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException {
+  public SynchronizerConfiguration(PrefsBranch config) throws NonObjectJSONException, IOException {
     this.load(config);
   }
 
   public SynchronizerConfiguration(String syncID, RepositorySessionBundle remoteBundle, RepositorySessionBundle localBundle) {
     this.syncID       = syncID;
     this.remoteBundle = remoteBundle;
     this.localBundle  = localBundle;
   }
 
   // This should get partly shuffled back into SyncConfiguration, I think.
-  public void load(PrefsBranch config) throws NonObjectJSONException, IOException, ParseException {
+  public void load(PrefsBranch config) throws NonObjectJSONException, IOException {
     if (config == null) {
       throw new IllegalArgumentException("config cannot be null.");
     }
     String remoteJSON = config.getString("remote", null);
     String localJSON  = config.getString("local",  null);
     RepositorySessionBundle rB = new RepositorySessionBundle(remoteJSON);
     RepositorySessionBundle lB = new RepositorySessionBundle(localJSON);
     if (remoteJSON == null) {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/UnexpectedJSONException.java
@@ -6,16 +6,20 @@ package org.mozilla.gecko.sync;
 
 public class UnexpectedJSONException extends Exception {
   private static final long serialVersionUID = 4797570033096443169L;
 
   public UnexpectedJSONException(String detailMessage) {
     super(detailMessage);
   }
 
+  public UnexpectedJSONException(Throwable throwable) {
+    super(throwable);
+  }
+
   public static class BadRequiredFieldJSONException extends UnexpectedJSONException {
     private static final long serialVersionUID = -9207736984784497612L;
 
     public BadRequiredFieldJSONException(String string) {
       super(string);
     }
   }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/Utils.java
@@ -29,19 +29,16 @@ import org.mozilla.apache.commons.codec.
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.background.nativecode.NativeCrypto;
 import org.mozilla.gecko.sync.setup.Constants;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.Bundle;
-import android.text.Spannable;
-import android.text.Spanned;
-import android.text.style.ClickableSpan;
 
 public class Utils {
 
   private static final String LOG_TAG = "Utils";
 
   private static final SecureRandom sharedSecureRandom = new SecureRandom();
 
   // See <http://developer.android.com/reference/android/content/Context.html#getSharedPreferences%28java.lang.String,%20int%29>
@@ -420,24 +417,24 @@ public class Utils {
     if (toSyncString == null && toSkipString == null) {
       return knownStageNames;
     }
 
     ArrayList<String> toSync = null;
     ArrayList<String> toSkip = null;
     if (toSyncString != null) {
       try {
-        toSync = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSyncString).keySet());
+        toSync = new ArrayList<String>(new ExtendedJSONObject(toSyncString).keySet());
       } catch (Exception e) {
         Logger.warn(LOG_TAG, "Got exception parsing stages to sync: '" + toSyncString + "'.", e);
       }
     }
     if (toSkipString != null) {
       try {
-        toSkip = new ArrayList<String>(ExtendedJSONObject.parseJSONObject(toSkipString).keySet());
+        toSkip = new ArrayList<String>(new ExtendedJSONObject(toSkipString).keySet());
       } catch (Exception e) {
         Logger.warn(LOG_TAG, "Got exception parsing stages to skip: '" + toSkipString + "'.", e);
       }
     }
 
     Logger.info(LOG_TAG, "Asked to sync '" + Utils.toCommaSeparatedString(toSync) +
                          "' and to skip '" + Utils.toCommaSeparatedString(toSkip) + "'.");
     return getStagesToSync(knownStageNames, toSync, toSkip);
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/MozResponse.java
@@ -6,17 +6,16 @@ package org.mozilla.gecko.sync.net;
 
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.Scanner;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 
 import ch.boye.httpclientandroidlib.Header;
 import ch.boye.httpclientandroidlib.HttpEntity;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.HttpStatus;
@@ -84,35 +83,33 @@ public class MozResponse {
 
   /**
    * Return the body as a <b>non-null</b> <code>ExtendedJSONObject</code>.
    *
    * @return A non-null <code>ExtendedJSONObject</code>.
    *
    * @throws IllegalStateException
    * @throws IOException
-   * @throws ParseException
    * @throws NonObjectJSONException
    */
-  public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException,
-                                 ParseException, NonObjectJSONException {
+  public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, NonObjectJSONException {
     if (body != null) {
       // Do it from the cached String.
-      return ExtendedJSONObject.parseJSONObject(body);
+      return new ExtendedJSONObject(body);
     }
 
     HttpEntity entity = this.response.getEntity();
     if (entity == null) {
       throw new IOException("no entity");
     }
 
     InputStream content = entity.getContent();
     try {
       Reader in = new BufferedReader(new InputStreamReader(content, "UTF-8"));
-      return ExtendedJSONObject.parseJSONObject(in);
+      return new ExtendedJSONObject(in);
     } finally {
       content.close();
     }
   }
 
   protected boolean hasHeader(String h) {
     return this.response.containsHeader(h);
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/net/SyncStorageRecordRequest.java
@@ -1,23 +1,22 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync.net;
 
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.mozilla.gecko.sync.CryptoRecord;
+
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
 
-import org.json.simple.JSONArray;
-import org.json.simple.JSONObject;
-import org.mozilla.gecko.sync.CryptoRecord;
-import org.mozilla.gecko.sync.ThreadPool;
-
 /**
  * Resource class that implements expected headers and processing for Sync.
  * Accepts a simplified delegate.
  *
  * Includes:
  * * Basic Auth headers (via Resource)
  * * Error responses:
  *   * 401
@@ -88,27 +87,9 @@ public class SyncStorageRecordRequest ex
 
   public void post(CryptoRecord record) {
     this.post(record.toJSONObject());
   }
 
   public void put(CryptoRecord record) {
     this.put(record.toJSONObject());
   }
-
-  public void deferGet() {
-    final SyncStorageRecordRequest self = this;
-    ThreadPool.run(new Runnable() {
-      @Override
-      public void run() {
-        self.get();
-      }});
-  }
-
-  public void deferPut(final JSONObject body) {
-    final SyncStorageRecordRequest self = this;
-    ThreadPool.run(new Runnable() {
-      @Override
-      public void run() {
-        self.put(body);
-      }});
-  }
 }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/RepositorySessionBundle.java
@@ -1,30 +1,30 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync.repositories;
 
-import java.io.IOException;
-
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 
+import java.io.IOException;
+
 public class RepositorySessionBundle {
   public static final String LOG_TAG = RepositorySessionBundle.class.getSimpleName();
 
   protected static final String JSON_KEY_TIMESTAMP = "timestamp";
 
   protected final ExtendedJSONObject object;
 
-  public RepositorySessionBundle(String jsonString) throws IOException, ParseException, NonObjectJSONException {
-    object = ExtendedJSONObject.parseJSONObject(jsonString);
+  public RepositorySessionBundle(String jsonString) throws IOException, NonObjectJSONException {
+
+    object = new ExtendedJSONObject(jsonString);
   }
 
   public RepositorySessionBundle(long lastSyncTimestamp) {
     object = new ExtendedJSONObject();
     this.setTimestamp(lastSyncTimestamp);
   }
 
   public long getTimestamp() {
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/android/RepoUtils.java
@@ -1,32 +1,31 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync.repositories.android;
 
-import java.io.IOException;
+import android.content.ContentProviderClient;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.RemoteException;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonArrayJSONException;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
 
-import android.content.ContentProviderClient;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteDatabase;
-import android.net.Uri;
-import android.os.RemoteException;
+import java.io.IOException;
 
 public class RepoUtils {
 
   private static final String LOG_TAG = "RepoUtils";
 
   /**
    * A helper class for monotonous SQL querying. Does timing and logging,
    * offers a utility to throw on a null cursor.
@@ -134,19 +133,16 @@ public class RepoUtils {
     try {
       return ExtendedJSONObject.parseJSONArray(getStringFromCursor(cur, colId));
     } catch (NonArrayJSONException e) {
       Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
       return null;
     } catch (IOException e) {
       Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
       return null;
-    } catch (ParseException e) {
-      Logger.error(LOG_TAG, "JSON parsing error for " + colId, e);
-      return null;
     }
   }
 
   /**
    * Return true if the provided URI is non-empty and acceptable to Fennec
    * (i.e., not an undesirable scheme).
    *
    * This code is pilfered from Fennec, which pilfered from Places.
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/sync/stage/ServerSyncStage.java
@@ -1,20 +1,16 @@
 /* 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/. */
 
 package org.mozilla.gecko.sync.stage;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
+import android.content.Context;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.common.log.Logger;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.HTTPFailureException;
 import org.mozilla.gecko.sync.MetaGlobalException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
@@ -38,17 +34,20 @@ import org.mozilla.gecko.sync.repositori
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.synchronizer.ServerLocalSynchronizer;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerDelegate;
 import org.mozilla.gecko.sync.synchronizer.SynchronizerSession;
 
-import android.content.Context;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
 
 /**
  * Fetch from a server collection into a local repository, encrypting
  * and decrypting along the way.
  *
  * @author rnewman
  *
  */
@@ -116,17 +115,17 @@ public abstract class ServerSyncStage ex
       if (enabledInMetaGlobal != enabledInSelection) {
         // Engine enable state has been changed by the user.
         Logger.debug(LOG_TAG, "Engine state has been changed by user. Throwing exception.");
         throw new MetaGlobalException.MetaGlobalEngineStateChangedException(enabledInSelection);
       }
     }
   }
 
-  protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException, ParseException {
+  protected EngineSettings getEngineSettings() throws NonObjectJSONException, IOException {
     Integer version = getStorageVersion();
     if (version == null) {
       Logger.warn(LOG_TAG, "null storage version for " + this + "; using version 0.");
       version = 0;
     }
 
     SynchronizerConfiguration config = this.getConfig();
     if (config == null) {
@@ -162,25 +161,25 @@ public abstract class ServerSyncStage ex
     cryptoRepo.recordFactory = getRecordFactory();
     return cryptoRepo;
   }
 
   protected String bundlePrefix() {
     return this.getCollection() + ".";
   }
 
-  protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException, ParseException {
+  protected SynchronizerConfiguration getConfig() throws NonObjectJSONException, IOException {
     return new SynchronizerConfiguration(session.config.getBranch(bundlePrefix()));
   }
 
   protected void persistConfig(SynchronizerConfiguration synchronizerConfiguration) {
     synchronizerConfiguration.persist(session.config.getBranch(bundlePrefix()));
   }
 
-  public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException, ParseException {
+  public Synchronizer getConfiguredSynchronizer(GlobalSession session) throws NoCollectionKeysSetException, URISyntaxException, NonObjectJSONException, IOException {
     Repository remote = wrappedServerRepo();
 
     Synchronizer synchronizer = new ServerLocalSynchronizer();
     synchronizer.repositoryA = remote;
     synchronizer.repositoryB = this.getLocalRepository();
     synchronizer.load(getConfig());
 
     return synchronizer;
@@ -544,17 +543,17 @@ public abstract class ServerSyncStage ex
     try {
       synchronizer = this.getConfiguredSynchronizer(session);
     } catch (NoCollectionKeysSetException e) {
       session.abort(e, "No CollectionKeys.");
       return;
     } catch (URISyntaxException e) {
       session.abort(e, "Invalid URI syntax for server repository.");
       return;
-    } catch (NonObjectJSONException | ParseException | IOException e) {
+    } catch (NonObjectJSONException | IOException e) {
       session.abort(e, "Invalid persisted JSON for config.");
       return;
     }
 
     Logger.debug(LOG_TAG, "Invoking synchronizer.");
     synchronizer.synchronize(session.getContext(), this);
     Logger.debug(LOG_TAG, "Reached end of execute.");
   }
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/tokenserver/TokenServerClient.java
@@ -166,17 +166,17 @@ public class TokenServerClient {
           for (Object error : result.getArray(JSON_KEY_ERRORS)) {
             Logger.warn(LOG_TAG, "" + error);
 
             if (error instanceof JSONObject) {
               errorList.add(new ExtendedJSONObject((JSONObject) error));
             }
           }
         } catch (NonArrayJSONException e) {
-          Logger.warn(LOG_TAG, "Got non-JSON array '" + result.getString(JSON_KEY_ERRORS) + "'.", e);
+          Logger.warn(LOG_TAG, "Got non-JSON array '" + JSON_KEY_ERRORS + "'.", e);
         }
       }
 
       if (statusCode == 400) {
         throw new TokenServerMalformedRequestException(errorList, result.toJSONString());
       }
 
       if (statusCode == 401) {
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java
@@ -1,47 +1,43 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.db;
 
-import java.io.IOException;
-import java.util.ArrayList;
+import android.database.Cursor;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
 import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.sync.NonArrayJSONException;
-import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataExtender;
 import org.mozilla.gecko.sync.repositories.android.RepoUtils;
 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
 
-import android.database.Cursor;
+import java.util.ArrayList;
 
 public class TestAndroidBrowserHistoryDataExtender extends AndroidSyncTestCase {
 
   protected AndroidBrowserHistoryDataExtender extender;
   protected static final String LOG_TAG = "SyncHistoryVisitsTest";
 
   public void setUp() {
     extender = new AndroidBrowserHistoryDataExtender(getApplicationContext());
     extender.wipe();
   }
 
   public void tearDown() {
     extender.close();
   }
 
-  public void testStoreFetch() throws NullCursorException, NonObjectJSONException, IOException, ParseException {
+  public void testStoreFetch() throws Exception {
     String guid = Utils.generateGuid();
     extender.store(Utils.generateGuid(), null);
     extender.store(guid, null);
     extender.store(Utils.generateGuid(), null);
 
     Cursor cur = null;
     try {
       cur = extender.fetch(guid);
@@ -50,17 +46,17 @@ public class TestAndroidBrowserHistoryDa
       assertEquals(guid, cur.getString(0));
     } finally {
       if (cur != null) {
         cur.close();
       }
     }
   }
 
-  public void testVisitsForGUID() throws NonArrayJSONException, NonObjectJSONException, IOException, ParseException, NullCursorException {
+  public void testVisitsForGUID() throws Exception {
     String guid = Utils.generateGuid();
     JSONArray visits = new ExtendedJSONObject("{ \"visits\": [ { \"key\" : \"value\" } ] }").getArray("visits");
 
     extender.store(Utils.generateGuid(), null);
     extender.store(guid, visits);
     extender.store(Utils.generateGuid(), null);
 
     JSONArray fetchedVisits = extender.visitsForGUID(guid);
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/sync/TestResetting.java
@@ -1,41 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.sync;
 
-import java.io.IOException;
+import android.content.SharedPreferences;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.background.helpers.AndroidSyncTestCase;
 import org.mozilla.gecko.background.testhelpers.BaseMockServerSyncStage;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockRecord;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
 import org.mozilla.gecko.background.testhelpers.WBORepository;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.MetaGlobalException;
-import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
-import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
-import org.mozilla.gecko.sync.crypto.CryptoException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 import org.mozilla.gecko.sync.stage.NoSuchStageException;
 import org.mozilla.gecko.sync.synchronizer.Synchronizer;
 
-import android.content.SharedPreferences;
-
 /**
  * Test the on-device side effects of reset operations on a stage.
  *
  * See also "TestResetCommands" in the unit test suite.
  */
 public class TestResetting extends AndroidSyncTestCase {
   private static final String TEST_USERNAME    = "johndoe";
   private static final String TEST_PASSWORD    = "password";
@@ -151,18 +145,17 @@ public class TestResetting extends Andro
           } catch (NoSuchStageException e) {
             performNotify(e);
           }
         }
       });
     }
   }
 
-  private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
-
+  private GlobalSession createDefaultGlobalSession(final GlobalSessionCallback callback) throws Exception {
     final KeyBundle keyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
     final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(TEST_USERNAME, TEST_PASSWORD);
     final SharedPreferences prefs = new MockSharedPreferences();
     final SyncConfiguration config = new SyncConfiguration(TEST_USERNAME, authHeaderProvider, prefs);
     config.syncKeyBundle = keyBundle;
     return new GlobalSession(config, callback, getApplicationContext(), null) {
       @Override
       public boolean isEngineRemotelyEnabled(String engineName,
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
@@ -1,24 +1,21 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
-import java.net.URISyntaxException;
-
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
-import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.repositories.RecordFactory;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.stage.ServerSyncStage;
 
+import java.net.URISyntaxException;
+
 /**
  * A stage that joins two Repositories with no wrapping.
  */
 public abstract class BaseMockServerSyncStage extends ServerSyncStage {
 
   public Repository local;
   public Repository remote;
   public String name;
@@ -61,13 +58,12 @@ public abstract class BaseMockServerSync
   }
 
   @Override
   protected Repository wrappedServerRepo()
   throws NoCollectionKeysSetException, URISyntaxException {
     return getRemoteRepository();
   }
 
-  public SynchronizerConfiguration leakConfig()
-  throws NonObjectJSONException, IOException, ParseException {
+  public SynchronizerConfiguration leakConfig() throws Exception {
     return this.getConfig();
   }
 }
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
@@ -1,37 +1,36 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
-import java.util.HashMap;
-
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
+import java.io.IOException;
+import java.util.HashMap;
+
 
 public class MockGlobalSession extends MockPrefsGlobalSession {
 
-  public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException {
+  public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException {
     this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
   }
 
   public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
-          throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
+          throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
     super(config, callback, null, null);
   }
 
   @Override
   public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) {
     return false;
   }
 
--- a/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit3/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -1,60 +1,59 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import java.io.IOException;
+import android.content.Context;
+import android.content.SharedPreferences;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
-import android.content.Context;
-import android.content.SharedPreferences;
+import java.io.IOException;
 
 /**
  * GlobalSession touches the Android prefs system. Stub that out.
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
   public MockPrefsGlobalSession(
       SyncConfiguration config, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      NonObjectJSONException {
     super(config, callback, context, clientsDelegate);
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, String password,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      NonObjectJSONException {
     return getSession(username, new BasicAuthHeaderProvider(username, password), null,
          syncKeyBundle, callback, context, clientsDelegate);
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, AuthHeaderProvider authHeaderProvider, String prefsPath,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
       throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      NonObjectJSONException {
 
     final SharedPreferences prefs = new MockSharedPreferences();
     final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
     config.syncKeyBundle = syncKeyBundle;
     return new MockPrefsGlobalSession(config, callback, context, clientsDelegate);
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestClientsEngineStage.java
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
 import ch.boye.httpclientandroidlib.HttpStatus;
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.junit.After;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.android.sync.test.helpers.MockSyncClientsEngineStage;
 import org.mozilla.gecko.background.common.log.Logger;
@@ -56,24 +55,24 @@ import static org.junit.Assert.assertEqu
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(TestRunner.class)
 public class TestClientsEngineStage extends MockSyncClientsEngineStage {
   public final static String LOG_TAG = "TestClientsEngSta";
 
-  public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
+  public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
     super();
     session = initializeSession();
   }
 
   // Static so we can set it during the constructor. This is so evil.
   private static MockGlobalSessionCallback callback;
-  private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
+  private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, URISyntaxException {
     callback = new MockGlobalSessionCallback();
     SyncConfiguration config = new SyncConfiguration(USERNAME, new BasicAuthHeaderProvider(USERNAME, PASSWORD), new MockSharedPreferences());
     config.syncKeyBundle = new KeyBundle(USERNAME, SYNC_KEY);
     GlobalSession session = new MockClientsGlobalSession(config, callback);
     session.config.setClusterURL(new URI(TEST_SERVER));
     session.config.setCollectionKeys(CollectionKeys.generateCollectionKeys());
     return session;
   }
@@ -173,17 +172,16 @@ public class TestClientsEngineStage exte
   public static class MockClientsGlobalSession extends MockGlobalSession {
     private ClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate();
 
     public MockClientsGlobalSession(SyncConfiguration config,
                                     GlobalSessionCallback callback)
         throws SyncConfigurationException,
                IllegalArgumentException,
                IOException,
-               ParseException,
                NonObjectJSONException {
       super(config, callback);
     }
 
     @Override
     public ClientsDataDelegate getClientsDelegate() {
       return clientsDataDelegate;
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestCredentialsEndToEnd.java
@@ -1,26 +1,26 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import ch.boye.httpclientandroidlib.Header;
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
 
+import ch.boye.httpclientandroidlib.Header;
+
 import static org.junit.Assert.assertEquals;
 
 /**
  * Test the transfer of a UTF-8 string from desktop, and ensure that it results in the
  * correct hashed Basic Auth header.
  */
 @RunWith(TestRunner.class)
 public class TestCredentialsEndToEnd {
@@ -41,17 +41,17 @@ public class TestCredentialsEndToEnd {
   @Test
   public void testUTF8() throws UnsupportedEncodingException {
     final String in  = "pïgéons1";
     final String out = "pïgéons1";
     assertEquals(out, Utils.decodeUTF8(in));
   }
 
   @Test
-  public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException, ParseException {
+  public void testAuthHeaderFromPassword() throws NonObjectJSONException, IOException {
     final ExtendedJSONObject parsed = new ExtendedJSONObject(DESKTOP_PASSWORD_JSON);
 
     final String password = parsed.getString("password");
     final String decoded = Utils.decodeUTF8(password);
 
     final byte[] expectedBytes = Utils.decodeBase64(BTOA_PASSWORD);
     final String expected = new String(expectedBytes, "UTF-8");
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestGlobalSession.java
@@ -3,17 +3,16 @@
 
 package org.mozilla.android.sync.net.test;
 
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.ProtocolVersion;
 import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
 import ch.boye.httpclientandroidlib.message.BasicStatusLine;
 import junit.framework.AssertionFailedError;
-import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockResourceDelegate;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.MockAbstractNonRepositorySyncStage;
@@ -67,17 +66,17 @@ public class TestGlobalSession {
   private final String TEST_SYNC_KEY            = "abcdeabcdeabcdeabcdeabcdea";
   private final long   TEST_BACKOFF_IN_SECONDS  = 2401;
 
   public static WaitHelper getTestWaiter() {
     return WaitHelper.getTestWaiter();
   }
 
   @Test
-  public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, NoSuchStageException {
+  public void testGetSyncStagesBy() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException, NoSuchStageException {
 
     final MockGlobalSessionCallback callback = new MockGlobalSessionCallback();
     GlobalSession s = MockPrefsGlobalSession.getSession(TEST_USERNAME, TEST_PASSWORD,
                                                  new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY),
                                                  callback, /* context */ null, null);
 
     assertTrue(s.getSyncStageByName(Stage.syncBookmarks) instanceof AndroidBrowserBookmarksServerSyncStage);
 
@@ -235,17 +234,17 @@ public class TestGlobalSession {
           r.get();
         } catch (URISyntaxException e) {
           innerWaitHelper.performNotify(e);
         }
       }
     });
   }
 
-  public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
+  public MockGlobalSessionCallback doTestSuccess(final boolean stageShouldBackoff, final boolean stageShouldAdvance) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
     MockServer server = new MockServer() {
       @Override
       public void handle(Request request, Response response) {
         if (stageShouldBackoff) {
           response.addValue("X-Weave-Backoff", Long.toString(TEST_BACKOFF_IN_SECONDS));
         }
         super.handle(request, response);
       }
@@ -290,49 +289,49 @@ public class TestGlobalSession {
     assertFalse(BaseResource.isHttpResponseObserver(session));
 
     return callback;
   }
 
   @Test
   public void testOnSuccessBackoffAdvanced() throws SyncConfigurationException,
       IllegalArgumentException, NonObjectJSONException, IOException,
-      ParseException, CryptoException {
+      CryptoException {
     MockGlobalSessionCallback callback = doTestSuccess(true, true);
 
     assertTrue(callback.calledError); // TODO: this should be calledAborted.
     assertTrue(callback.calledRequestBackoff);
     assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
   }
 
   @Test
   public void testOnSuccessBackoffAborted() throws SyncConfigurationException,
       IllegalArgumentException, NonObjectJSONException, IOException,
-      ParseException, CryptoException {
+      CryptoException {
     MockGlobalSessionCallback callback = doTestSuccess(true, false);
 
     assertTrue(callback.calledError); // TODO: this should be calledAborted.
     assertTrue(callback.calledRequestBackoff);
     assertEquals(1000 * TEST_BACKOFF_IN_SECONDS, callback.weaveBackoff);
   }
 
   @Test
   public void testOnSuccessNoBackoffAdvanced() throws SyncConfigurationException,
       IllegalArgumentException, NonObjectJSONException, IOException,
-      ParseException, CryptoException {
+      CryptoException {
     MockGlobalSessionCallback callback = doTestSuccess(false, true);
 
     assertTrue(callback.calledSuccess);
     assertFalse(callback.calledRequestBackoff);
   }
 
   @Test
   public void testOnSuccessNoBackoffAborted() throws SyncConfigurationException,
       IllegalArgumentException, NonObjectJSONException, IOException,
-      ParseException, CryptoException {
+      CryptoException {
     MockGlobalSessionCallback callback = doTestSuccess(false, false);
 
     assertTrue(callback.calledError); // TODO: this should be calledAborted.
     assertFalse(callback.calledRequestBackoff);
   }
 
   @Test
   public void testGenerateNewMetaGlobalNonePersisted() throws Exception {
@@ -391,17 +390,17 @@ public class TestGlobalSession {
     session.enginesToUpdate.clear();
 
     // Set enabledEngines in meta/global, including a "new engine."
     String[] origEngines = new String[] { "bookmarks", "clients", "forms", "history", "tabs", "new-engine" };
 
     ExtendedJSONObject origEnginesJSONObject = new ExtendedJSONObject();
     for (String engineName : origEngines) {
       EngineSettings mockEngineSettings = new EngineSettings(Utils.generateGuid(), Integer.valueOf(0));
-      origEnginesJSONObject.put(engineName, mockEngineSettings);
+      origEnginesJSONObject.put(engineName, mockEngineSettings.toJSONObject());
     }
     session.config.metaGlobal.setEngines(origEnginesJSONObject);
 
     // Engines to remove.
     String[] toRemove = new String[] { "bookmarks", "tabs" };
     for (String name : toRemove) {
       session.removeEngineFromMetaGlobal(name);
     }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/net/test/TestMetaGlobal.java
@@ -1,24 +1,24 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.net.test;
 
-import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.background.testhelpers.WaitHelper;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.MetaGlobal;
+import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.delegates.MetaGlobalDelegate;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.simpleframework.http.Request;
 import org.simpleframework.http.Response;
 
 import java.util.HashSet;
@@ -28,18 +28,16 @@ import java.util.concurrent.atomic.Atomi
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestMetaGlobal {
-  public static Object monitor = new Object();
-
   private static final int    TEST_PORT    = HTTPServerTestHelper.getTestPort();
   private static final String TEST_SERVER  = "http://localhost:" + TEST_PORT;
   private static final String TEST_SYNC_ID = "foobar";
 
   public static final String USER_PASS = "c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd:password";
   public static final String META_URL  = TEST_SERVER + "/1.1/c6o7dvmr2c4ud2fyv6woz2u4zi22bcyd/storage/meta/global";
   private HTTPServerTestHelper data    = new HTTPServerTestHelper();
 
@@ -218,17 +216,17 @@ public class TestMetaGlobal {
     MockServer existingMetaGlobalServer = new MockServer(200, TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE);
 
     data.startHTTPServer(existingMetaGlobalServer);
     final MockMetaGlobalFetchDelegate delegate = doFetch(global);
     data.stopHTTPServer();
 
     assertTrue(delegate.errorCalled);
     assertNotNull(delegate.errorException);
-    assertEquals(ParseException.class, delegate.errorException.getClass());
+    assertEquals(NonObjectJSONException.class, delegate.errorException.getClass());
   }
 
   @SuppressWarnings("static-method")
   @Test
   public void testSetFromRecord() throws Exception {
     MetaGlobal mg = new MetaGlobal(null, null);
     mg.setFromRecord(CryptoRecord.fromJSONRecord(TEST_META_GLOBAL_RESPONSE));
     assertEquals("zPSQTm7WBVWB", mg.getSyncID());
@@ -314,17 +312,17 @@ public class TestMetaGlobal {
 
     final AtomicBoolean mgUploaded = new AtomicBoolean(false);
     final MetaGlobal uploadedMg = new MetaGlobal(null, null);
 
     MockServer server = new MockServer() {
       public void handle(Request request, Response response) {
         if (request.getMethod().equals("PUT")) {
           try {
-            ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent());
+            ExtendedJSONObject body = new ExtendedJSONObject(request.getContent());
             System.out.println(body.toJSONString());
             assertTrue(body.containsKey("payload"));
             assertFalse(body.containsKey("default"));
 
             CryptoRecord rec = CryptoRecord.fromJSONRecord(body);
             uploadedMg.setFromRecord(rec);
             mgUploaded.set(true);
           } catch (Exception e) {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCollectionKeys.java
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CollectionKeys;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
@@ -66,49 +65,49 @@ public class TestCollectionKeys {
   }
 
   public static void assertSame(byte[] arrayOne, byte[] arrayTwo) {
     assertTrue(Arrays.equals(arrayOne, arrayTwo));
   }
 
 
   @Test
-  public void testSetKeysFromWBO() throws IOException, ParseException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException {
+  public void testSetKeysFromWBO() throws IOException, NonObjectJSONException, CryptoException, NoCollectionKeysSetException {
     String json = "{\"default\":[\"3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=\",\"/AMaoCX4hzic28WY94XtokNi7N4T0nv+moS1y5wlbug=\"],\"collections\":{},\"collection\":\"crypto\",\"id\":\"keys\"}";
     CryptoRecord rec = new CryptoRecord(json);
 
     KeyBundle syncKeyBundle = new KeyBundle("slyjcrjednxd6rf4cr63vqilmkus6zbe", "6m8mv8ex2brqnrmsb9fjuvfg7y");
     rec.keyBundle = syncKeyBundle;
 
     rec.encrypt();
     CollectionKeys ck = new CollectionKeys();
     ck.setKeyPairsFromWBO(rec, syncKeyBundle);
     byte[] input = "3fI6k1exImMgAKjilmMaAWxGqEIzFX/9K5EjEgH99vc=".getBytes("UTF-8");
     byte[] expected = Base64.decodeBase64(input);
     assertSame(expected, ck.defaultKeyBundle().getEncryptionKey());
   }
 
   @Test
-  public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, ParseException, NonObjectJSONException {
+  public void testCryptoRecordFromCollectionKeys() throws CryptoException, NoCollectionKeysSetException, IOException, NonObjectJSONException {
     CollectionKeys ck1 = CollectionKeys.generateCollectionKeys();
     assertNotNull(ck1.defaultKeyBundle());
     assertEquals(ck1.keyBundleForCollection("foobar"), ck1.defaultKeyBundle());
     CryptoRecord rec = ck1.asCryptoRecord();
     assertEquals(rec.collection, "crypto");
     assertEquals(rec.guid, "keys");
     JSONArray defaultKey = (JSONArray) rec.payload.get("default");
 
     assertSame(Base64.decodeBase64((String) (defaultKey.get(0))), ck1.defaultKeyBundle().getEncryptionKey());
     CollectionKeys ck2 = new CollectionKeys();
     ck2.setKeyPairsFromWBO(rec, null);
     assertSame(ck1.defaultKeyBundle().getEncryptionKey(), ck2.defaultKeyBundle().getEncryptionKey());
   }
 
   @Test
-  public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, ParseException, NoCollectionKeysSetException {
+  public void testCreateKeysBundle() throws CryptoException, NonObjectJSONException, IOException, NoCollectionKeysSetException {
     String username =                       "b6evr62dptbxz7fvebek7btljyu322wp";
     String friendlyBase32SyncKey =          "basuxv2426eqj7frhvpcwkavdi";
 
     KeyBundle syncKeyBundle = new KeyBundle(username, friendlyBase32SyncKey);
 
     CollectionKeys ck = CollectionKeys.generateCollectionKeys();
     CryptoRecord unencrypted = ck.asCryptoRecord();
     unencrypted.keyBundle = syncKeyBundle;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCommandProcessor.java
@@ -1,14 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CommandProcessor;
 import org.mozilla.gecko.sync.CommandRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
@@ -44,70 +43,70 @@ public class TestCommandProcessor extend
 
     @Override
     public void executeCommand(final GlobalSession session, List<String> args) {
       commandExecuted = true;
     }
   }
 
   @Test
-  public void testRegisterCommand() throws NonObjectJSONException, IOException, ParseException {
+  public void testRegisterCommand() throws NonObjectJSONException, IOException {
     assertNull(commands.get(commandType));
     this.registerCommand(commandType, new MockCommandRunner(1));
     assertNotNull(commands.get(commandType));
   }
 
   @Test
-  public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException, ParseException {
+  public void testProcessRegisteredCommand() throws NonObjectJSONException, IOException {
     commandExecuted = false;
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
     this.registerCommand(commandType, new MockCommandRunner(1));
     this.processCommand(session, unparsedCommand);
     assertTrue(commandExecuted);
   }
 
   @Test
-  public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException, ParseException {
+  public void testProcessUnregisteredCommand() throws NonObjectJSONException, IOException {
     commandExecuted = false;
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
     this.processCommand(session, unparsedCommand);
     assertFalse(commandExecuted);
   }
 
   @Test
-  public void testProcessInvalidCommand() throws NonObjectJSONException, IOException, ParseException {
+  public void testProcessInvalidCommand() throws NonObjectJSONException, IOException {
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
     this.registerCommand(commandType, new MockCommandRunner(1));
     this.processCommand(session, unparsedCommand);
     assertFalse(commandExecuted);
   }
 
   @Test
-  public void testParseCommandNoType() throws NonObjectJSONException, IOException, ParseException {
+  public void testParseCommandNoType() throws NonObjectJSONException, IOException {
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoType);
     assertNull(CommandProcessor.parseCommand(unparsedCommand));
   }
 
   @Test
-  public void testParseCommandNoArgs() throws NonObjectJSONException, IOException, ParseException {
+  public void testParseCommandNoArgs() throws NonObjectJSONException, IOException {
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(commandWithNoArgs);
     assertNull(CommandProcessor.parseCommand(unparsedCommand));
   }
 
   @Test
-  public void testParseWellFormedCommand() throws NonObjectJSONException, IOException, ParseException {
+  public void testParseWellFormedCommand() throws NonObjectJSONException, IOException {
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommand);
     Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
     assertNotNull(parsedCommand);
     assertEquals(2, parsedCommand.args.size());
     assertEquals(commandType, parsedCommand.commandType);
   }
 
   @Test
-  public void testParseCommandNullArg() throws NonObjectJSONException, IOException, ParseException {
+  public void testParseCommandNullArg() throws NonObjectJSONException, IOException {
     ExtendedJSONObject unparsedCommand = new ExtendedJSONObject(wellFormedCommandWithNullArgs);
     Command parsedCommand = CommandProcessor.parseCommand(unparsedCommand);
     assertNotNull(parsedCommand);
     assertEquals(4, parsedCommand.args.size());
     assertEquals(commandType, parsedCommand.commandType);
     final List<String> expectedArgs = new ArrayList<String>();
     expectedArgs.add("https://bugzilla.mozilla.org/show_bug.cgi?id=731341");
     expectedArgs.add(null);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestCryptoRecord.java
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.apache.commons.codec.binary.Base64;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.Utils;
@@ -30,18 +29,24 @@ import static org.junit.Assert.assertNul
 import static org.junit.Assert.assertTrue;
 
 @RunWith(TestRunner.class)
 public class TestCryptoRecord {
   String base64EncryptionKey = "9K/wLdXdw+nrTtXo4ZpECyHFNr4d7aYHqeg3KW9+m6Q=";
   String base64HmacKey = "MMntEfutgLTc8FlTLQFms8/xMPmCldqPlq/QQXEjx70=";
 
   @Test
-  public void testBaseCryptoRecordEncrypt() throws IOException, ParseException, NonObjectJSONException, CryptoException {
-    ExtendedJSONObject clearPayload = ExtendedJSONObject.parseJSONObject("{\"id\":\"5qRsgXWRJZXr\",\"title\":\"Index of file:///Users/jason/Library/Application Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1,\"date\":1319149012372425}]}");
+  public void testBaseCryptoRecordEncrypt() throws IOException, NonObjectJSONException, CryptoException {
+
+    ExtendedJSONObject clearPayload = new ExtendedJSONObject("{\"id\":\"5qRsgXWRJZXr\"," +
+            "\"title\":\"Index of file:///Users/jason/Library/Application " +
+            "Support/Firefox/Profiles/ksgd7wpk.LocalSyncServer/weave/logs/\"," +
+            "\"histUri\":\"file:///Users/jason/Library/Application%20Support/Firefox/Profiles" +
+            "/ksgd7wpk.LocalSyncServer/weave/logs/\",\"visits\":[{\"type\":1," +
+            "\"date\":1319149012372425}]}");
 
     CryptoRecord record = new CryptoRecord();
     record.payload = clearPayload;
     String expectedGUID = "5qRsgXWRJZXr";
     record.guid = expectedGUID;
     record.keyBundle = KeyBundle.fromBase64EncodedKeys(base64EncryptionKey, base64HmacKey);
     record.encrypt();
     assertTrue(record.payload.get("title") == null);
@@ -251,17 +256,17 @@ public class TestCryptoRecord {
     // We don't necessarily produce exactly the same JSON but we do have the same values.
     ExtendedJSONObject expectedJson = new ExtendedJSONObject(expectedDecryptedText);
     assertEquals(expectedJson.get("id"), decrypted.payload.get("id"));
     assertEquals(expectedJson.get("default"), decrypted.payload.get("default"));
     assertEquals(expectedJson.get("collection"), decrypted.payload.get("collection"));
     assertEquals(expectedJson.get("collections"), decrypted.payload.get("collections"));
 
     // Check that the extracted keys were as expected.
-    JSONArray keys = ExtendedJSONObject.parseJSONObject(decrypted.payload.toJSONString()).getArray("default");
+    JSONArray keys = new ExtendedJSONObject(decrypted.payload.toJSONString()).getArray("default");
     KeyBundle keyBundle = KeyBundle.fromBase64EncodedKeys((String)keys.get(0), (String)keys.get(1));
 
     assertArrayEquals(Base64.decodeBase64(expectedBase64EncryptionKey.getBytes("UTF-8")), keyBundle.getEncryptionKey());
     assertArrayEquals(Base64.decodeBase64(expectedBase64HmacKey.getBytes("UTF-8")), keyBundle.getHMACKey());
   }
 
   @Test
   public void testTTL() throws UnsupportedEncodingException, CryptoException {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestRecord.java
@@ -1,16 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.db.Tab;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.CryptoRecord;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
@@ -28,17 +27,17 @@ import static org.junit.Assert.assertFal
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(TestRunner.class)
 public class TestRecord {
 
   @SuppressWarnings("static-method")
   @Test
-  public void testQueryRecord() throws NonObjectJSONException, IOException, ParseException {
+  public void testQueryRecord() throws NonObjectJSONException, IOException {
     final String expectedGUID = "Bl3n3gpKag3s";
     final String testRecord =
         "{\"id\":\"" + expectedGUID + "\"," +
         " \"type\":\"query\"," +
         " \"title\":\"Downloads\"," +
         " \"parentName\":\"\"," +
         " \"bmkUri\":\"place:transition=7&sort=4\"," +
         " \"tags\":[]," +
@@ -188,28 +187,28 @@ public class TestRecord {
 
   @SuppressWarnings("static-method")
   @Test
   public void testTabParsing() throws Exception {
     String json = "{\"title\":\"mozilla-central mozilla/browser/base/content/syncSetup.js\"," +
                   " \"urlHistory\":[\"http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72\"]," +
                   " \"icon\":\"http://mxr.mozilla.org/mxr.png\"," +
                   " \"lastUsed\":\"1306374531\"}";
-    Tab tab = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(json).object);
+    Tab tab = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(json).object);
 
     assertEquals("mozilla-central mozilla/browser/base/content/syncSetup.js", tab.title);
     assertEquals("http://mxr.mozilla.org/mxr.png", tab.icon);
     assertEquals("http://mxr.mozilla.org/mozilla-central/source/browser/base/content/syncSetup.js#72", tab.history.get(0));
     assertEquals(1306374531000L, tab.lastUsed);
 
     String zeroJSON = "{\"title\":\"a\"," +
         " \"urlHistory\":[\"http://example.com\"]," +
         " \"icon\":\"\"," +
         " \"lastUsed\":0}";
-    Tab zero = TabsRecord.tabFromJSONObject(ExtendedJSONObject.parseJSONObject(zeroJSON).object);
+    Tab zero = TabsRecord.tabFromJSONObject(new ExtendedJSONObject(zeroJSON).object);
 
     assertEquals("a", zero.title);
     assertEquals("", zero.icon);
     assertEquals("http://example.com", zero.history.get(0));
     assertEquals(0L, zero.lastUsed);
   }
 
   @SuppressWarnings({ "unchecked", "static-method" })
@@ -275,34 +274,34 @@ public class TestRecord {
          "\"mNTdpgoRZMbW\", \"-L8Vci6CbkJY\", \"bVzudKSQERc1\", \"Gxl9lb4DXsmL\"," +
          "\"3Qr13GucOtEh\"]}";
 
   public class PayloadBookmarkRecord extends BookmarkRecord {
     public PayloadBookmarkRecord() {
       super("abcdefghijkl", "bookmarks", 1234, false);
     }
 
-    public void doTest() throws NonObjectJSONException, IOException, ParseException {
+    public void doTest() throws NonObjectJSONException, IOException {
       this.initFromPayload(new ExtendedJSONObject(payload));
       assertEquals("abcdefghijkl",      this.guid);              // Ignores payload.
       assertEquals("livemark",          this.type);
       assertEquals("Bookmarks Toolbar", this.parentName);
       assertEquals("toolbar",           this.parentID);
       assertEquals("",                  this.description);
       assertEquals(null,                this.children);
 
       final String encodedSite = "http%3A%2F%2Fwww.bbc.co.uk%2Fgo%2Frss%2Fint%2Fnews%2F-%2Fnews%2F";
       final String encodedFeed = "http%3A%2F%2Ffxfeeds.mozilla.com%2Fen-US%2Ffirefox%2Fheadlines.xml";
       final String expectedURI = "places:siteUri=" + encodedSite + "&feedUri=" + encodedFeed;
       assertEquals(expectedURI, this.bookmarkURI);
     }
   }
 
   @Test
-  public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException, ParseException {
+  public void testUnusualBookmarkRecords() throws NonObjectJSONException, IOException {
     PayloadBookmarkRecord record = new PayloadBookmarkRecord();
     record.doTest();
   }
 
   @SuppressWarnings("static-method")
   @Test
   public void testTTL() {
     Record record = new HistoryRecord();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/android/sync/test/TestResetCommands.java
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.android.sync.test;
 
 import android.content.SharedPreferences;
-import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.DefaultGlobalSessionCallback;
 import org.mozilla.gecko.background.testhelpers.MockPrefsGlobalSession;
 import org.mozilla.gecko.background.testhelpers.MockServerSyncStage;
 import org.mozilla.gecko.background.testhelpers.MockSharedPreferences;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
@@ -57,17 +56,17 @@ public class TestResetCommands {
   }
 
   @Before
   public void setUp() {
     assertTrue(WaitHelper.getTestWaiter().isIdle());
   }
 
   @Test
-  public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
+  public void testHandleResetCommand() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
     // Create a global session.
     // Set up stage mappings for a real stage name (because they're looked up by name
     // in an enumeration) pointing to our fake stage.
     // Send a reset command.
     // Verify that reset is called on our stage.
 
     class Result {
       public boolean called = false;
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java
@@ -1,14 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.NoCollectionKeysSetException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SynchronizerConfiguration;
 import org.mozilla.gecko.sync.repositories.RecordFactory;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.stage.ServerSyncStage;
 
 import java.io.IOException;
@@ -62,12 +61,12 @@ public abstract class BaseMockServerSync
 
   @Override
   protected Repository wrappedServerRepo()
   throws NoCollectionKeysSetException, URISyntaxException {
     return getRemoteRepository();
   }
 
   public SynchronizerConfiguration leakConfig()
-  throws NonObjectJSONException, IOException, ParseException {
+  throws NonObjectJSONException, IOException {
     return this.getConfig();
   }
 }
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java
@@ -1,14 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
-import org.json.simple.parser.ParseException;
 import org.mozilla.gecko.sync.EngineSettings;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
 import org.mozilla.gecko.sync.stage.CompletedStage;
@@ -16,22 +15,22 @@ import org.mozilla.gecko.sync.stage.Glob
 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
 
 import java.io.IOException;
 import java.util.HashMap;
 
 
 public class MockGlobalSession extends MockPrefsGlobalSession {
 
-  public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException {
+  public MockGlobalSession(String username, String password, KeyBundle keyBundle, GlobalSessionCallback callback) throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException {
     this(new SyncConfiguration(username, new BasicAuthHeaderProvider(username, password), new MockSharedPreferences(), keyBundle), callback);
   }
 
   public MockGlobalSession(SyncConfiguration config, GlobalSessionCallback callback)
-          throws SyncConfigurationException, IllegalArgumentException, IOException, ParseException, NonObjectJSONException {
+          throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
     super(config, callback, null, null);
   }
 
   @Override
   public boolean isEngineRemotelyEnabled(String engine, EngineSettings engineSettings) {
     return false;
   }
 
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.background.testhelpers;
 
 import android.content.Context;
 import android.content.SharedPreferences;
-import org.json.simple.parser.ParseException;
+
 import org.mozilla.gecko.sync.GlobalSession;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.SyncConfiguration;
 import org.mozilla.gecko.sync.SyncConfigurationException;
 import org.mozilla.gecko.sync.crypto.KeyBundle;
 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
 import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
@@ -23,37 +23,34 @@ import java.io.IOException;
  */
 public class MockPrefsGlobalSession extends GlobalSession {
 
   public MockSharedPreferences prefs;
 
   public MockPrefsGlobalSession(
       SyncConfiguration config, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
-      throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
     super(config, callback, context, clientsDelegate);
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, String password,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
-      throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
     return getSession(username, new BasicAuthHeaderProvider(username, password), null,
          syncKeyBundle, callback, context, clientsDelegate);
   }
 
   public static MockPrefsGlobalSession getSession(
       String username, AuthHeaderProvider authHeaderProvider, String prefsPath,
       KeyBundle syncKeyBundle, GlobalSessionCallback callback, Context context,
       ClientsDataDelegate clientsDelegate)
-      throws SyncConfigurationException, IllegalArgumentException, IOException,
-      ParseException, NonObjectJSONException {
+      throws SyncConfigurationException, IllegalArgumentException, IOException, NonObjectJSONException {
 
     final SharedPreferences prefs = new MockSharedPreferences();
     final SyncConfiguration config = new SyncConfiguration(username, authHeaderProvider, prefs);
     config.syncKeyBundle = syncKeyBundle;
     return new MockPrefsGlobalSession(config, callback, context, clientsDelegate);
   }
 
   @Override
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/middleware/test/TestCrypto5MiddlewareRepositorySession.java
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.middleware.test;
 
 import junit.framework.AssertionFailedError;
-import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionBeginDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionCreationDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFetchRecordsDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionFinishDelegate;
 import org.mozilla.android.sync.test.helpers.ExpectSuccessRepositorySessionStoreDelegate;
@@ -156,17 +155,17 @@ public class TestCrypto5MiddlewareReposi
     }));
     assertEquals(0, wboRepo.wbos.size());
   }
 
   @Test
   /**
    * Verify that store is actually writing encrypted data to the underlying repository.
    */
-  public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException, ParseException {
+  public void testStoreEncrypts() throws NonObjectJSONException, CryptoException, IOException {
     final BookmarkRecord record = new BookmarkRecord("nncdefghiaaa", "coll", System.currentTimeMillis(), false);
     record.title = "unencrypted title";
 
     runInOnBeginSucceeded(new Runnable() {
       @Override public void run() {
         try {
           try {
             cmwSession.setStoreDelegate(new ExpectSuccessRepositorySessionStoreDelegate(getTestWaiter()));
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestEnsureCrypto5KeysStage.java
@@ -82,17 +82,17 @@ public class TestEnsureCrypto5KeysStage 
       public void resetStagesByName(Collection<String> names) {
         calledResetStages = true;
         stagesReset = names;
       }
     };
     session.config.setClusterURL(new URI(TEST_CLUSTER_URL));
 
     // Set info collections to not have crypto.
-    final ExtendedJSONObject noCrypto = ExtendedJSONObject.parseJSONObject(TEST_JSON_NO_CRYPTO);
+    final ExtendedJSONObject noCrypto = new ExtendedJSONObject(TEST_JSON_NO_CRYPTO);
     session.config.infoCollections = new InfoCollections(noCrypto);
     calledResetStages = false;
     stagesReset = null;
   }
 
   public void doSession(MockServer server) {
     data.startHTTPServer(server);
     try {
@@ -108,17 +108,18 @@ public class TestEnsureCrypto5KeysStage 
       });
     } finally {
     data.stopHTTPServer();
     }
   }
 
   @Test
   public void testDownloadUsesPersisted() throws Exception {
-    session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_OLD_CRYPTO));
+    session.config.infoCollections = new InfoCollections(new ExtendedJSONObject
+            (TEST_JSON_OLD_CRYPTO));
     session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
 
     assertNull(session.config.collectionKeys);
     final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
     keys.setDefaultKeyBundle(syncKeyBundle);
     session.config.persistedCryptoKeys().persistKeys(keys);
 
     MockServer server = new MockServer() {
@@ -131,17 +132,17 @@ public class TestEnsureCrypto5KeysStage 
 
     assertTrue(callback.calledSuccess);
     assertNotNull(session.config.collectionKeys);
     assertTrue(CollectionKeys.differences(session.config.collectionKeys, keys).isEmpty());
   }
 
   @Test
   public void testDownloadFetchesNew() throws Exception {
-    session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO));
+    session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
     session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
 
     assertNull(session.config.collectionKeys);
     final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
     keys.setDefaultKeyBundle(syncKeyBundle);
     session.config.persistedCryptoKeys().persistKeys(keys);
 
     MockServer server = new MockServer() {
@@ -167,17 +168,17 @@ public class TestEnsureCrypto5KeysStage 
   /**
    * Change the default key but keep one collection key the same. Should reset
    * all but that one collection.
    */
   @Test
   public void testDownloadResetsOnDifferentDefaultKey() throws Exception {
     String TEST_COLLECTION = "bookmarks";
 
-    session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO));
+    session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
     session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
 
     KeyBundle keyBundle = KeyBundle.withRandomKeys();
     assertNull(session.config.collectionKeys);
     final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
     keys.setKeyBundleForCollection(TEST_COLLECTION, keyBundle);
     session.config.persistedCryptoKeys().persistKeys(keys);
     keys.setDefaultKeyBundle(syncKeyBundle); // Change the default key bundle, but keep "bookmarks" the same.
@@ -207,17 +208,17 @@ public class TestEnsureCrypto5KeysStage 
     assertTrue(allButCollection.containsAll(stagesReset));
     assertTrue(callback.calledError);
   }
 
   @Test
   public void testDownloadResetsEngineOnDifferentKey() throws Exception {
     final String TEST_COLLECTION = "history";
 
-    session.config.infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_JSON_NEW_CRYPTO));
+    session.config.infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_JSON_NEW_CRYPTO));
     session.config.persistedCryptoKeys().persistLastModified(System.currentTimeMillis());
 
     assertNull(session.config.collectionKeys);
     final CollectionKeys keys = CollectionKeys.generateCollectionKeys();
     session.config.persistedCryptoKeys().persistKeys(keys);
     keys.setKeyBundleForCollection(TEST_COLLECTION, syncKeyBundle); // Change one key bundle.
 
     CryptoRecord rec = keys.asCryptoRecord();
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/stage/test/TestFetchMetaGlobalStage.java
@@ -1,15 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.stage.test;
 
 import org.json.simple.JSONArray;
-import org.json.simple.parser.ParseException;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.android.sync.net.test.TestMetaGlobal;
 import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
 import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
 import org.mozilla.android.sync.test.helpers.MockServer;
 import org.mozilla.gecko.background.testhelpers.MockGlobalSession;
@@ -88,17 +87,17 @@ public class TestFetchMetaGlobalStage {
     calledRequiresUpgrade = false;
     calledProcessMissingMetaGlobal = false;
     calledFreshStart = false;
     calledWipeServer = false;
     calledUploadKeys = false;
     calledResetAllStages = false;
 
     // Set info collections to not have crypto.
-    infoCollections = new InfoCollections(ExtendedJSONObject.parseJSONObject(TEST_INFO_COLLECTIONS_JSON));
+    infoCollections = new InfoCollections(new ExtendedJSONObject(TEST_INFO_COLLECTIONS_JSON));
 
     syncKeyBundle = new KeyBundle(TEST_USERNAME, TEST_SYNC_KEY);
     callback = new MockGlobalSessionCallback();
     session = new MockGlobalSession(TEST_USERNAME, TEST_PASSWORD,
       syncKeyBundle, callback) {
       @Override
       protected void prepareStages() {
         super.prepareStages();
@@ -330,42 +329,42 @@ public class TestFetchMetaGlobalStage {
    * @throws Exception
    */
   @Test
   public void testFetchMalformedPayload() throws Exception {
     MockServer server = new MockServer(200, TestMetaGlobal.TEST_META_GLOBAL_MALFORMED_PAYLOAD_RESPONSE);
     doSession(server);
 
     assertEquals(true, callback.calledError);
-    assertEquals(ParseException.class, callback.calledErrorException.getClass());
+    assertEquals(NonObjectJSONException.class, callback.calledErrorException.getClass());
   }
 
   protected void doFreshStart(MockServer server) {
     data.startHTTPServer(server);
     WaitHelper.getTestWaiter().performWait(WaitHelper.onThreadRunnable(new Runnable() {
       @Override
       public void run() {
         session.freshStart();
       }
     }));
     data.stopHTTPServer();
   }
 
   @Test
-  public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException {
+  public void testFreshStart() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, CryptoException {
     final AtomicBoolean mgUploaded = new AtomicBoolean(false);
     final AtomicBoolean mgDownloaded = new AtomicBoolean(false);
     final MetaGlobal uploadedMg = new MetaGlobal(null, null);
 
     MockServer server = new MockServer() {
       @Override
       public void handle(Request request, Response response) {
         if (request.getMethod().equals("PUT")) {
           try {
-            ExtendedJSONObject body = ExtendedJSONObject.parseJSONObject(request.getContent());
+            ExtendedJSONObject body = new ExtendedJSONObject(request.getContent());
             assertTrue(body.containsKey("payload"));
             assertFalse(body.containsKey("default"));
 
             CryptoRecord rec = CryptoRecord.fromJSONRecord(body);
             uploadedMg.setFromRecord(rec);
             mgUploaded.set(true);
           } catch (Exception e) {
             throw new RuntimeException(e);
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestExtendedJSONObject.java
@@ -1,87 +1,75 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.gecko.sync.test;
 
 import org.json.simple.JSONArray;
 import org.json.simple.JSONObject;
-import org.json.simple.parser.ParseException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mozilla.gecko.background.testhelpers.TestRunner;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.NonArrayJSONException;
 import org.mozilla.gecko.sync.NonObjectJSONException;
 import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
 
 import java.io.IOException;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 @RunWith(TestRunner.class)
 public class TestExtendedJSONObject {
   public static String exampleJSON = "{\"modified\":1233702554.25,\"success\":[\"{GXS58IDC}12\",\"{GXS58IDC}13\",\"{GXS58IDC}15\",\"{GXS58IDC}16\",\"{GXS58IDC}18\",\"{GXS58IDC}19\"],\"failed\":{\"{GXS58IDC}11\":[\"invalid parentid\"],\"{GXS58IDC}14\":[\"invalid parentid\"],\"{GXS58IDC}17\":[\"invalid parentid\"],\"{GXS58IDC}20\":[\"invalid parentid\"]}}";
   public static String exampleIntegral = "{\"modified\":1233702554,}";
 
   @Test
-  public void testDeepCopy() throws NonObjectJSONException, IOException, ParseException, NonArrayJSONException {
-    ExtendedJSONObject a = new ExtendedJSONObject(exampleJSON);
-    ExtendedJSONObject c = a.deepCopy();
-    assertTrue(a != c);
-    assertTrue(a.equals(c));
-    assertTrue(a.get("modified") == c.get("modified"));
-    assertTrue(a.getArray("success") != c.getArray("success"));
-    assertTrue(a.getArray("success").equals(c.getArray("success")));
-  }
-
-  @Test
-  public void testFractional() throws IOException, ParseException, NonObjectJSONException {
+  public void testFractional() throws IOException, NonObjectJSONException {
     ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
     assertTrue(o.containsKey("modified"));
     assertTrue(o.containsKey("success"));
     assertTrue(o.containsKey("failed"));
     assertFalse(o.containsKey(" "));
     assertFalse(o.containsKey(""));
     assertFalse(o.containsKey("foo"));
     assertTrue(o.get("modified") instanceof Number);
     assertTrue(o.get("modified").equals(Double.parseDouble("1233702554.25")));
     assertEquals(Long.valueOf(1233702554250L), o.getTimestamp("modified"));
     assertEquals(null, o.getTimestamp("foo"));
   }
 
   @Test
-  public void testIntegral() throws IOException, ParseException, NonObjectJSONException {
+  public void testIntegral() throws IOException, NonObjectJSONException {
     ExtendedJSONObject o = new ExtendedJSONObject(exampleIntegral);
     assertTrue(o.containsKey("modified"));
     assertFalse(o.containsKey("success"));
     assertTrue(o.get("modified") instanceof Number);
     assertTrue(o.get("modified").equals(Long.parseLong("1233702554")));
     assertEquals(Long.valueOf(1233702554000L), o.getTimestamp("modified"));
     assertEquals(null, o.getTimestamp("foo"));
   }
 
   @Test
   public void testSafeInteger() {
     ExtendedJSONObject o = new ExtendedJSONObject();
     o.put("integer", Integer.valueOf(5));
-    o.put("double",  Double.valueOf(1.2));
     o.put("string",  "66");
     o.put("object",  new ExtendedJSONObject());
-    o.put("null",    null);
+    o.put("null", (JSONArray) null);
 
     assertEquals(Integer.valueOf(5),  o.getIntegerSafely("integer"));
     assertEquals(Integer.valueOf(66), o.getIntegerSafely("string"));
     assertNull(o.getIntegerSafely(null));
   }
 
   @Test
   public void testParseJSONArray() throws Exception {
@@ -93,17 +81,17 @@ public class TestExtendedJSONObject {
     assertThat((Long) ((JSONObject) result.get(2)).get("test"), is(equalTo(2L)));
   }
 
   @Test
   public void testBadParseJSONArray() throws Exception {
     try {
       ExtendedJSONObject.parseJSONArray("[0, ");
       fail();
-    } catch (ParseException e) {
+    } catch (NonArrayJSONException e) {
       // Do nothing.
     }
 
     try {
       ExtendedJSONObject.parseJSONArray("{}");
       fail();
     } catch (NonArrayJSONException e) {
       // Do nothing.
@@ -119,24 +107,24 @@ public class TestExtendedJSONObject {
     assertEquals("value", o.getString("key"));
   }
 
   @Test
   public void testBadParseUTF8AsJSONObject() throws Exception {
     try {
       ExtendedJSONObject.parseUTF8AsJSONObject("{}".getBytes("UTF-16"));
       fail();
-    } catch (ParseException e) {
+    } catch (NonObjectJSONException e) {
       // Do nothing.
     }
 
     try {
       ExtendedJSONObject.parseUTF8AsJSONObject("{".getBytes("UTF-8"));
       fail();
-    } catch (ParseException e) {
+    } catch (NonObjectJSONException e) {
       // Do nothing.
     }
   }
 
   @Test
   public void testHashCode() throws Exception {
     ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
     assertEquals(o.hashCode(), o.hashCode());
@@ -152,18 +140,17 @@ public class TestExtendedJSONObject {
   public void testEquals() throws Exception {
     ExtendedJSONObject o = new ExtendedJSONObject(exampleJSON);
     ExtendedJSONObject p = new ExtendedJSONObject(exampleJSON);
     assertEquals(o, p);
 
     ExtendedJSONObject q = new ExtendedJSONObject(exampleJSON);
     q.put("modified", 0);
     assertNotSame(o, q);
-    q.put("modified", o.get("modified"));
-    assertEquals(o, q);
+    assertNotEquals(o, q);
   }
 
   @Test
   public void testGetBoolean() throws Exception {
     ExtendedJSONObject o = new ExtendedJSONObject("{\"truekey\":true, \"falsekey\":false, \"stringkey\":\"string\"}");
     assertEquals(true, o.getBoolean("truekey"));
     assertEquals(false, o.getBoolean("falsekey"));
     try {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/sync/test/TestInfoCollections.java
@@ -44,39 +44,39 @@ public class TestInfoCollections {
       "}";
 
   @SuppressWarnings("static-method")
   @Test
   public void testSetCountsFromRecord() throws Exception {
     InfoCounts infoCountsEmpty = new InfoCounts(new ExtendedJSONObject("{}"));
     assertEquals(null, infoCountsEmpty.getCount("bookmarks"));
 
-    ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COUNTS_JSON);
+    ExtendedJSONObject record = new ExtendedJSONObject(TEST_COUNTS_JSON);
     InfoCounts infoCountsFull = new InfoCounts(record);
     assertEquals(Integer.valueOf(766), infoCountsFull.getCount("bookmarks"));
     assertEquals(null, infoCountsFull.getCount("notpresent"));
   }
 
 
   @SuppressWarnings("static-method")
   @Test
   public void testSetCollectionsFromRecord() throws Exception {
-    ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COLLECTIONS_JSON);
+    ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON);
     InfoCollections infoCollections = new InfoCollections(record);
 
     assertEquals(Utils.decimalSecondsToMilliseconds(1.3319567131E9), infoCollections.getTimestamp("history").longValue());
     assertEquals(Utils.decimalSecondsToMilliseconds(1.321E9), infoCollections.getTimestamp("meta").longValue());
     assertEquals(Utils.decimalSecondsToMilliseconds(1.35E9), infoCollections.getTimestamp("tabs").longValue());
     assertNull(infoCollections.getTimestamp("missing"));
   }
 
   @SuppressWarnings("static-method")
   @Test
   public void testUpdateNeeded() throws Exception {
-    ExtendedJSONObject record = ExtendedJSONObject.parseJSONObject(TEST_COLLECTIONS_JSON);
+    ExtendedJSONObject record = new ExtendedJSONObject(TEST_COLLECTIONS_JSON);
     InfoCollections infoCollections = new InfoCollections(record);
 
     long none = -1;
     long past = Utils.decimalSecondsToMilliseconds(1.3E9);
     long same = Utils.decimalSecondsToMilliseconds(1.35E9);
     long future = Utils.decimalSecondsToMilliseconds(1.4E9);
 
 
--- a/services/common/hawkclient.js
+++ b/services/common/hawkclient.js
@@ -185,23 +185,26 @@ this.HawkClient.prototype = {
    *        API endpoint path
    * @param method
    *        The HTTP request method
    * @param credentials
    *        Hawk credentials
    * @param payloadObj
    *        An object that can be encodable as JSON as the payload of the
    *        request
+   * @param extraHeaders
+   *        An object with header/value pairs to send with the request.
    * @return Promise
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
-  request: function(path, method, credentials=null, payloadObj={}, retryOK=true) {
+  request: function(path, method, credentials=null, payloadObj={}, extraHeaders = {},
+                    retryOK=true) {
     method = method.toLowerCase();
 
     let deferred = Promise.defer();
     let uri = this.host + path;
     let self = this;
 
     function _onComplete(error) {
       // |error| can be either a normal caught error or an explicitly created
@@ -232,17 +235,17 @@ this.HawkClient.prototype = {
 
       self._updateClockOffset(restResponse.headers["date"]);
 
       if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
         // Retry once if we were rejected due to a bad timestamp.
         // Clock offset is adjusted already in the top of this function.
         log.debug("Received 401 for " + path + ": retrying");
         return deferred.resolve(
-            self.request(path, method, credentials, payloadObj, false));
+            self.request(path, method, credentials, payloadObj, extraHeaders, false));
       }
 
       // If the server returned a json error message, use it in the rejection
       // of the promise.
       //
       // In the case of a 401, in which we are probably being rejected for a
       // bad timestamp, retry exactly once, during which time clock offset will
       // be adjusted.
@@ -273,16 +276,17 @@ this.HawkClient.prototype = {
         log.error("Unhandled exception processing response", ex);
         deferred.reject(ex);
       }
     }
 
     let extra = {
       now: this.now(),
       localtimeOffsetMsec: this.localtimeOffsetMsec,
+      headers: extraHeaders
     };
 
     let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
     try {
       if (method == "post" || method == "put" || method == "patch") {
         request[method](payloadObj, onComplete);
       } else {
         request[method](onComplete);
--- a/services/common/hawkrequest.js
+++ b/services/common/hawkrequest.js
@@ -37,32 +37,35 @@ const Prefs = new Preferences("services.
  * @param payloadObj
  *        (Object) Optional object to be converted to JSON payload
  *
  * @param extra
  *        (Object) Optional extra params for HAWK header computation.
  *        Valid properties are:
  *
  *          now:                 <current time in milliseconds>,
- *          localtimeOffsetMsec: <local clock offset vs server>
+ *          localtimeOffsetMsec: <local clock offset vs server>,
+ *          headers:             <An object with header/value pairs to be sent
+ *                                as headers on the request>
  *
  * extra.localtimeOffsetMsec is the value in milliseconds that must be added to
  * the local clock to make it agree with the server's clock.  For instance, if
  * the local clock is two minutes ahead of the server, the time offset in
  * milliseconds will be -120000.
  */
 
 this.HAWKAuthenticatedRESTRequest =
  function HawkAuthenticatedRESTRequest(uri, credentials, extra={}) {
   RESTRequest.call(this, uri);
 
   this.credentials = credentials;
   this.now = extra.now || Date.now();
   this.localtimeOffsetMsec = extra.localtimeOffsetMsec || 0;
   this._log.trace("local time, offset: " + this.now + ", " + (this.localtimeOffsetMsec));
+  this.extraHeaders = extra.headers || {};
 
   // Expose for testing
   this._intl = getIntl();
 };
 HAWKAuthenticatedRESTRequest.prototype = {
   __proto__: RESTRequest.prototype,
 
   dispatch: function dispatch(method, data, onComplete, onProgress) {
@@ -78,16 +81,20 @@ HAWKAuthenticatedRESTRequest.prototype =
         payload: data && JSON.stringify(data) || "",
         contentType: contentType,
       };
       let header = CryptoUtils.computeHAWK(this.uri, method, options);
       this.setHeader("Authorization", header.field);
       this._log.trace("hawk auth header: " + header.field);
     }
 
+    for (let header in this.extraHeaders) {
+      this.setHeader(header, this.extraHeaders[header]);
+    }
+
     this.setHeader("Content-Type", contentType);
 
     this.setHeader("Accept-Language", this._intl.accept_languages);
 
     return RESTRequest.prototype.dispatch.call(
       this, method, data, onComplete, onProgress
     );
   }
--- a/services/common/tests/unit/test_hawkclient.js
+++ b/services/common/tests/unit/test_hawkclient.js
@@ -93,16 +93,39 @@ add_task(function test_authenticated_pos
 add_task(function test_authenticated_put_request() {
   check_authenticated_request("PUT");
 });
 
 add_task(function test_authenticated_patch_request() {
   check_authenticated_request("PATCH");
 });
 
+add_task(function* test_extra_headers() {
+  let server = httpd_setup({"/foo": (request, response) => {
+      do_check_true(request.hasHeader("Authorization"));
+      do_check_true(request.hasHeader("myHeader"));
+      do_check_eq(request.getHeader("myHeader"), "fake");
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+      response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
+    }
+  });
+
+  let client = new HawkClient(server.baseURI);
+
+  let response = yield client.request("/foo", "POST", TEST_CREDS, {foo: "bar"},
+                                      {"myHeader": "fake"});
+  let result = JSON.parse(response.body);
+
+  do_check_eq("bar", result.foo);
+
+  yield deferredStop(server);
+});
+
 add_task(function* test_credentials_optional() {
   let method = "GET";
   let server = httpd_setup({
     "/foo": (request, response) => {
       do_check_false(request.hasHeader("Authorization"));
 
       let message = JSON.stringify({msg: "you're in the friend zone"});
       response.setStatusLine(request.httpVersion, 200, "OK");
--- a/storage/test/unit/test_storage_connection.js
+++ b/storage/test/unit/test_storage_connection.js
@@ -63,42 +63,31 @@ add_task(function* test_indexExists_crea
   var msc = getOpenedDatabase();
   msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)");
   do_check_true(msc.indexExists("name_ind"));
 });
 
 add_task(function* test_createTable_already_created() {
   var msc = getOpenedDatabase();
   do_check_true(msc.tableExists("test"));
-  try {
-    msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
-    do_throw("We shouldn't get here!");
-  } catch (e) {
-    do_check_eq(Cr.NS_ERROR_FAILURE, e.result);
-  }
+  Assert.throws(() => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"),
+                /NS_ERROR_FAILURE/);
 });
 
 add_task(function* test_attach_createTable_tableExists_indexExists() {
   var msc = getOpenedDatabase();
   var file = do_get_file("storage_attach.sqlite", true);
   var msc2 = getDatabase(file);
   msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample");
 
   do_check_false(msc.tableExists("sample.test"));
   msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT");
   do_check_true(msc.tableExists("sample.test"));
-  try {
-    msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT");
-    do_throw("We shouldn't get here!");
-  } catch (e) {
-    if (e.result != Components.results.NS_ERROR_FAILURE) {
-      throw e;
-    }
-    // we expect to fail because this table should exist already.
-  }
+  Assert.throws(() => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"),
+                /NS_ERROR_FAILURE/);
 
   do_check_false(msc.indexExists("sample.test_ind"));
   msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)");
   do_check_true(msc.indexExists("sample.test_ind"));
 
   msc.executeSimpleSQL("DETACH DATABASE sample");
   msc2.close();
   try {
@@ -130,33 +119,23 @@ add_task(function* test_transactionInPro
   do_check_true(msc.transactionInProgress);
   msc.rollbackTransaction();
   do_check_false(msc.transactionInProgress);
 });
 
 add_task(function* test_commitTransaction_no_transaction() {
   var msc = getOpenedDatabase();
   do_check_false(msc.transactionInProgress);
-  try {
-    msc.commitTransaction();
-    do_throw("We should not get here!");
-  } catch (e) {
-    do_check_eq(Cr.NS_ERROR_UNEXPECTED, e.result);
-  }
+  Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/);
 });
 
 add_task(function* test_rollbackTransaction_no_transaction() {
   var msc = getOpenedDatabase();
   do_check_false(msc.transactionInProgress);
-  try {
-    msc.rollbackTransaction();
-    do_throw("We should not get here!");
-  } catch (e) {
-    do_check_eq(Cr.NS_ERROR_UNEXPECTED, e.result);
-  }
+  Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/);
 });
 
 add_task(function* test_get_schemaVersion_not_set() {
   do_check_eq(0, getOpenedDatabase().schemaVersion);
 });
 
 add_task(function* test_set_schemaVersion() {
   var msc = getOpenedDatabase();
@@ -287,31 +266,25 @@ add_task(function* test_asyncClose_then_
 
 add_task(function* test_close_fails_with_async_statement_ran() {
   let deferred = Promise.defer();
   let stmt = createStatement("SELECT * FROM test");
   stmt.executeAsync();
   stmt.finalize();
 
   let db = getOpenedDatabase();
-  try {
-    db.close();
-    do_throw("should have thrown");
-  }
-  catch (e) {
-    do_check_eq(e.result, Cr.NS_ERROR_UNEXPECTED);
-  }
-  finally {
-    // Clean up after ourselves.
-    db.asyncClose(function () {
-      // Reset gDBConn so that later tests will get a new connection object.
-      gDBConn = null;
-      deferred.resolve();
-    });
-  }
+  Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/);
+
+  // Clean up after ourselves.
+  db.asyncClose(function () {
+    // Reset gDBConn so that later tests will get a new connection object.
+    gDBConn = null;
+    deferred.resolve();
+  });
+
   yield deferred.promise;
 });
 
 add_task(function* test_clone_optional_param() {
   let db1 = getService().openUnsharedDatabase(getTestDB());
   let db2 = db1.clone();
   do_check_true(db2.connectionReady);
 
--- a/storage/test/unit/test_storage_value_array.js
+++ b/storage/test/unit/test_storage_value_array.js
@@ -1,16 +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 the functions of mozIStorageValueArray
 
-function setup()
-{
+add_task(function* setup() {
   getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT," +
                                           "number REAL, nuller NULL, blobber BLOB");
 
   var stmt = createStatement("INSERT INTO test (name, number, blobber) " +
                              "VALUES (?1, ?2, ?3)");
   stmt.bindByIndex(0, "foo");
   stmt.bindByIndex(1, 2.34);
   stmt.bindBlobByIndex(2, [], 0);
@@ -18,194 +17,166 @@ function setup()
 
   stmt.bindByIndex(0, "");
   stmt.bindByIndex(1, 1.23);
   stmt.bindBlobByIndex(2, [1, 2], 2);
   stmt.execute();
 
   stmt.reset();
   stmt.finalize();
-}
 
-function test_getIsNull_for_null()
-{
+  do_register_cleanup(cleanup);
+});
+
+add_task(function* test_getIsNull_for_null() {
   var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_true(stmt.getIsNull(0)); // null field
   do_check_true(stmt.getIsNull(1)); // data is null if size is 0
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getIsNull_for_non_null()
-{
+add_task(function* test_getIsNull_for_non_null() {
   var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_false(stmt.getIsNull(0));
   do_check_false(stmt.getIsNull(1));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_value_type_null()
-{
+add_task(function* test_value_type_null() {
   var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_NULL,
               stmt.getTypeOfIndex(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_value_type_integer()
-{
+add_task(function* test_value_type_integer() {
   var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER,
               stmt.getTypeOfIndex(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_value_type_float()
-{
+add_task(function* test_value_type_float() {
   var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT,
               stmt.getTypeOfIndex(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_value_type_text()
-{
+add_task(function* test_value_type_text() {
   var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_TEXT,
               stmt.getTypeOfIndex(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_value_type_blob()
-{
+add_task(function* test_value_type_blob() {
   var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq(Ci.mozIStorageValueArray.VALUE_TYPE_BLOB,
               stmt.getTypeOfIndex(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_numEntries_one()
-{
+add_task(function* test_numEntries_one() {
   var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq(1, stmt.numEntries);
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_numEntries_all()
-{
+add_task(function* test_numEntries_all() {
   var stmt = createStatement("SELECT * FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq(5, stmt.numEntries);
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getInt()
-{
+add_task(function* test_getInt() {
   var stmt = createStatement("SELECT id FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq(2, stmt.getInt32(0));
   do_check_eq(2, stmt.getInt64(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getDouble()
-{
+add_task(function* test_getDouble() {
   var stmt = createStatement("SELECT number FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq(1.23, stmt.getDouble(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getUTF8String()
-{
+add_task(function* test_getUTF8String() {
   var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 1);
   do_check_true(stmt.executeStep());
 
   do_check_eq("foo", stmt.getUTF8String(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getString()
-{
+add_task(function* test_getString() {
   var stmt = createStatement("SELECT name FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   do_check_eq("", stmt.getString(0));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_getBlob()
-{
+add_task(function* test_getBlob() {
   var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1");
   stmt.bindByIndex(0, 2);
   do_check_true(stmt.executeStep());
 
   var count = { value: 0 };
   var arr = { value: null };
   stmt.getBlob(0, count, arr);
   do_check_eq(2, count.value);
   do_check_eq(1, arr.value[0]);
   do_check_eq(2, arr.value[1]);
   stmt.reset();
   stmt.finalize();
-}
-
-var tests = [test_getIsNull_for_null, test_getIsNull_for_non_null,
-             test_value_type_null, test_value_type_integer,
-             test_value_type_float, test_value_type_text, test_value_type_blob,
-             test_numEntries_one, test_numEntries_all, test_getInt,
-             test_getDouble, test_getUTF8String, test_getString, test_getBlob];
+});
 
-function run_test()
-{
-  setup();
 
-  for (var i = 0; i < tests.length; i++) {
-    tests[i]();
-  }
-
-  cleanup();
-}
-
--- a/storage/test/unit/test_unicode.js
+++ b/storage/test/unit/test_unicode.js
@@ -2,103 +2,82 @@
  * 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 the unicode functions that we have added
 
 const LATIN1_AE = "\xc6"; // "Æ"
 const LATIN1_ae = "\xe6";  // "æ"
 
-function setup()
-{
+add_task(function* setup() {
   getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT");
 
   var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)");
   stmt.bindByIndex(0, LATIN1_AE);
   stmt.bindByIndex(1, 1);
   stmt.execute();
   stmt.bindByIndex(0, "A");
   stmt.bindByIndex(1, 2);
   stmt.execute();
   stmt.bindByIndex(0, "b");
   stmt.bindByIndex(1, 3);
   stmt.execute();
   stmt.bindByIndex(0, LATIN1_ae);
   stmt.bindByIndex(1, 4);
   stmt.execute();
   stmt.finalize();
-}
 
-function test_upper_ascii()
-{
+  do_register_cleanup(cleanup);
+});
+
+add_task(function* test_upper_ascii() {
   var stmt = createStatement("SELECT name, id FROM test WHERE name = upper('a')");
   do_check_true(stmt.executeStep());
   do_check_eq("A", stmt.getString(0));
   do_check_eq(2, stmt.getInt32(1));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_upper_non_ascii()
-{
+add_task(function* test_upper_non_ascii() {
   var stmt = createStatement("SELECT name, id FROM test WHERE name = upper(?1)");
   stmt.bindByIndex(0, LATIN1_ae);
   do_check_true(stmt.executeStep());
   do_check_eq(LATIN1_AE, stmt.getString(0));
   do_check_eq(1, stmt.getInt32(1));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_lower_ascii()
-{
+add_task(function* test_lower_ascii() {
   var stmt = createStatement("SELECT name, id FROM test WHERE name = lower('B')");
   do_check_true(stmt.executeStep());
   do_check_eq("b", stmt.getString(0));
   do_check_eq(3, stmt.getInt32(1));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_lower_non_ascii()
-{
+add_task(function* test_lower_non_ascii() {
   var stmt = createStatement("SELECT name, id FROM test WHERE name = lower(?1)");
   stmt.bindByIndex(0, LATIN1_AE);
   do_check_true(stmt.executeStep());
   do_check_eq(LATIN1_ae, stmt.getString(0));
   do_check_eq(4, stmt.getInt32(1));
   stmt.reset();
   stmt.finalize();
-}
+});
 
-function test_like_search_different()
-{
+add_task(function* test_like_search_different() {
   var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
   stmt.bindByIndex(0, LATIN1_AE);
   do_check_true(stmt.executeStep());
   do_check_eq(2, stmt.getInt32(0));
   stmt.finalize();
-}
+});
 
-function test_like_search_same()
-{
+add_task(function* test_like_search_same() {
   var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1");
   stmt.bindByIndex(0, LATIN1_ae);
   do_check_true(stmt.executeStep());
   do_check_eq(2, stmt.getInt32(0));
   stmt.finalize();
-}
-
-var tests = [test_upper_ascii, test_upper_non_ascii, test_lower_ascii,
-             test_lower_non_ascii, test_like_search_different,
-             test_like_search_same];
-
-function run_test()
-{
-  setup();
-
-  for (var i = 0; i < tests.length; i++) {
-    tests[i]();
-  }
-
-  cleanup();
-}
-
+});
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -47,16 +47,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
 
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 
 // Register built-in parts of the API. Other parts may be registered
 // in browser/, mobile/, or b2g/.
 ExtensionManagement.registerScript("chrome://extensions/content/ext-alarms.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-backgroundPage.js");
 ExtensionManagement.registerScript("chrome://extensions/content/ext-cookies.js");
@@ -306,31 +308,47 @@ ExtensionPage = class extends BaseContex
     Management.emit("page-unload", this);
 
     this.extension.views.delete(this);
 
     super.unload();
   }
 };
 
+// For extensions that have called setUninstallURL(), send an event
+// so the browser can display the URL.
+let UninstallObserver = {
+  init: function() {
+    AddonManager.addAddonListener(this);
+  },
+
+  onUninstalling: function(addon) {
+    let extension = GlobalManager.extensionMap.get(addon.id);
+    if (extension) {
+      Management.emit("uninstall", extension);
+    }
+  },
+};
+
 // Responsible for loading extension APIs into the right globals.
 GlobalManager = {
   // Number of extensions currently enabled.
   count: 0,
 
   // Map[docShell -> {extension, context}] where context is an ExtensionPage.
   docShells: new Map(),
 
   // Map[extension ID -> Extension]. Determines which extension is
   // responsible for content under a particular extension ID.
   extensionMap: new Map(),
 
   init(extension) {
     if (this.count == 0) {
       Services.obs.addObserver(this, "content-document-global-created", false);
+      UninstallObserver.init();
     }
     this.count++;
 
     this.extensionMap.set(extension.id, extension);
   },
 
   uninit(extension) {
     this.count--;
@@ -360,46 +378,45 @@ GlobalManager = {
       let injectObject = (name, defaultCallback) => {
         let browserObj = Cu.createObjectIn(contentWindow, {defineAs: name});
 
         let api = Management.generateAPIs(extension, context, Management.apis);
         injectAPI(api, browserObj);
 
         let schemaApi = Management.generateAPIs(extension, context, Management.schemaApis);
         let schemaWrapper = {
+          get cloneScope() {
+            return context.cloneScope;
+          },
+
           callFunction(ns, name, args) {
             return schemaApi[ns][name](...args);
           },
 
           callAsyncFunction(ns, name, args, callback) {
             // We pass an empty stub function as a default callback for
             // the `chrome` API, so promise objects are not returned,
             // and lastError values are reported immediately.
             if (callback === null) {
               callback = defaultCallback;
             }
 
             let promise;
             try {
-              // TODO: Stop passing the callback once all APIs return
-              // promises.
-              promise = schemaApi[ns][name](...args, callback);
+              promise = schemaApi[ns][name](...args);
             } catch (e) {
-              promise = Promise.reject(e);
-              // TODO: Certain tests are still expecting API methods to
-              // throw errors.
-              throw e;
+              if (e instanceof context.cloneScope.Error) {
+                promise = Promise.reject(e);
+              } else {
+                Cu.reportError(e);
+                promise = Promise.reject({ message: "An unexpected error occurred" });
+              }
             }
 
-            // TODO: This check should no longer be necessary
-            // once all async methods return promises.
-            if (promise) {
-              return context.wrapPromise(promise, callback);
-            }
-            return undefined;
+            return context.wrapPromise(promise || Promise.resolve(), callback);
           },
 
           getProperty(ns, name) {
             return schemaApi[ns][name];
           },
 
           setProperty(ns, name, value) {
             schemaApi[ns][name] = value;
@@ -600,16 +617,20 @@ ExtensionData.prototype = {
     return Promise.all([
       this.readJSON("manifest.json"),
       Management.lazyInit(),
     ]).then(([manifest]) => {
       let context = {
         url: this.baseURI && this.baseURI.spec,
 
         principal: this.principal,
+
+        logError: error => {
+          this.logger.warn(`Loading extension '${this.id}': Reading manifest: ${error}`);
+        },
       };
 
       let normalized = Schemas.normalize(manifest, "manifest.WebExtensionManifest", context);
       if (normalized.error) {
         this.manifestError(normalized.error);
         this.manifest = manifest;
       } else {
         this.manifest = normalized.value;
@@ -801,16 +822,18 @@ this.Extension = function(addonData) {
 
   this.views = new Set();
 
   this.onStartup = null;
 
   this.hasShutdown = false;
   this.onShutdown = new Set();
 
+  this.uninstallURL = null;
+
   this.permissions = new Set();
   this.whiteListedHosts = null;
   this.webAccessibleResources = new Set();
 
   this.emitter = new EventEmitter();
 };
 
 /**
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -89,16 +89,23 @@ class Context {
                  "getProperty", "setProperty"];
     for (let prop of props) {
       this[prop] = params[prop];
     }
 
     if ("checkLoadURL" in params) {
       this.checkLoadURL = params.checkLoadURL;
     }
+    if ("logError" in params) {
+      this.logError = params.logError;
+    }
+  }
+
+  get cloneScope() {
+    return this.params.cloneScope;
   }
 
   get url() {
     return this.params.url;
   }
 
   get principal() {
     return this.params.principal || Services.scriptSecurityManager.createNullPrincipal({});
@@ -110,27 +117,76 @@ class Context {
       ssm.checkLoadURIStrWithPrincipal(this.principal, url,
                                        ssm.DISALLOW_INHERIT_PRINCIPAL);
     } catch (e) {
       return false;
     }
     return true;
   }
 
+  /**
+   * Returns an error result object with the given message, for return
+   * by Type normalization functions.
+   *
+   * If the context has a `currentTarget` value, this is prepended to
+   * the message to indicate the location of the error.
+   */
   error(message) {
     if (this.currentTarget) {
       return {error: `Error processing ${this.currentTarget}: ${message}`};
     }
     return {error: message};
   }
 
+  /**
+   * Creates an `Error` object belonging to the current unprivileged
+   * scope. If there is no unprivileged scope associated with this
+   * context, the message is returned as a string.
+   *
+   * If the context has a `currentTarget` value, this is prepended to
+   * the message, in the same way as for the `error` method.
+   */
+  makeError(message) {
+    let {error} = this.error(message);
+    if (this.cloneScope) {
+      return new this.cloneScope.Error(error);
+    }
+    return error;
+  }
+
+  /**
+   * Logs the given error to the console. May be overridden to enable
+   * custom logging.
+   */
+  logError(error) {
+    Cu.reportError(error);
+  }
+
+  /**
+   * Returns the name of the value currently being normalized. For a
+   * nested object, this is usually approximately equivalent to the
+   * JavaScript property accessor for that property. Given:
+   *
+   *   { foo: { bar: [{ baz: x }] } }
+   *
+   * When processing the value for `x`, the currentTarget is
+   * 'foo.bar.0.baz'
+   */
   get currentTarget() {
     return this.path.join(".");
   }
 
+  /**
+   * Appends the given component to the `currentTarget` path to indicate
+   * that it is being processed, calls the given callback function, and
+   * then restores the original path.
+   *
+   * This is used to identify the path of the property being processed
+   * when reporting type errors.
+   */
   withPath(component, callback) {
     this.path.push(component);
     try {
       return callback();
     } finally {
       this.path.pop();
     }
   }
@@ -188,22 +244,73 @@ const FORMATS = {
     throw new SyntaxError(`String ${JSON.stringify(string)} must be a relative URL`);
   },
 };
 
 // Schema files contain namespaces, and each namespace contains types,
 // properties, functions, and events. An Entry is a base class for
 // types, properties, functions, and events.
 class Entry {
+  constructor(schema = {}) {
+    /**
+     * If set to any value which evaluates as true, this entry is
+     * deprecated, and any access to it will result in a deprecation
+     * warning being logged to the browser console.
+     *
+     * If the value is a string, it will be appended to the deprecation
+     * message. If it contains the substring "${value}", it will be
+     * replaced with a string representation of the value being
+     * processed.
+     *
+     * If the value is any other truthy value, a generic deprecation
+     * message will be emitted.
+     */
+    this.deprecated = false;
+    if ("deprecated" in schema) {
+      this.deprecated = schema.deprecated;
+    }
+  }
+
+  /**
+   * Logs a deprecation warning for this entry, based on the value of
+   * its `deprecated` property.
+   */
+  logDeprecation(context, value = null) {
+    let message = "This property is deprecated";
+    if (typeof(this.deprecated) == "string") {
+      message = this.deprecated;
+      if (message.includes("${value}")) {
+        try {
+          value = JSON.stringify(value);
+        } catch (e) {
+          value = String(value);
+        }
+        message = message.replace(/\$\{value\}/g, () => value);
+      }
+    }
+
+    context.logError(context.makeError(message));
+  }
+
+  /**
+   * Checks whether the entry is deprecated and, if so, logs a
+   * deprecation message.
+   */
+  checkDeprecated(context, value = null) {
+    if (this.deprecated) {
+      this.logDeprecation(context, value);
+    }
+  }
+
   // Injects JS values for the entry into the extension API
   // namespace. The default implementation is to do
-  // nothing. |wrapperFuncs| is used to call the actual implementation
+  // nothing. |context| is used to call the actual implementation
   // of a given function or event. It's an object with properties
   // callFunction, addListener, removeListener, and hasListener.
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
   }
 }
 
 // Corresponds either to a type declared in the "types" section of the
 // schema or else to any type object used throughout the schema.
 class Type extends Entry {
   // Takes a value, checks that it has the correct type, and returns a
   // "normalized" version of the value. The normalized version will
@@ -222,47 +329,51 @@ class Type extends Entry {
   checkBaseType(baseType) {
     return false;
   }
 
   // Helper method that simply relies on checkBaseType to implement
   // normalize. Subclasses can choose to use it or not.
   normalizeBase(type, value, context) {
     if (this.checkBaseType(getValueBaseType(value))) {
+      this.checkDeprecated(context, value);
       return {value};
     }
     return context.error(`Expected ${type} instead of ${JSON.stringify(value)}`);
   }
 }
 
 // Type that allows any value.
 class AnyType extends Type {
-  normalize(value) {
+  normalize(value, context) {
+    this.checkDeprecated(context, value);
     return {value};
   }
 
   checkBaseType(baseType) {
     return true;
   }
 }
 
 // An untagged union type.
 class ChoiceType extends Type {
-  constructor(choices) {
-    super();
+  constructor(schema, choices) {
+    super(schema);
     this.choices = choices;
   }
 
   extend(type) {
     this.choices.push(...type.choices);
 
     return this;
   }
 
   normalize(value, context) {
+    this.checkDeprecated(context, value);
+
     let error;
 
     let baseType = getValueBaseType(value);
     for (let choice of this.choices) {
       if (choice.checkBaseType(baseType)) {
         let r = choice.normalize(value, context);
         if (!r.error) {
           return r;
@@ -278,43 +389,44 @@ class ChoiceType extends Type {
     return this.choices.some(t => t.checkBaseType(baseType));
   }
 }
 
 // This is a reference to another type--essentially a typedef.
 class RefType extends Type {
   // For a reference to a type named T declared in namespace NS,
   // namespaceName will be NS and reference will be T.
-  constructor(namespaceName, reference) {
-    super();
+  constructor(schema, namespaceName, reference) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.reference = reference;
   }
 
   get targetType() {
     let ns = Schemas.namespaces.get(this.namespaceName);
     let type = ns.get(this.reference);
     if (!type) {
       throw new Error(`Internal error: Type ${this.reference} not found`);
     }
     return type;
   }
 
   normalize(value, context) {
+    this.checkDeprecated(context, value);
     return this.targetType.normalize(value, context);
   }
 
   checkBaseType(baseType) {
     return this.targetType.checkBaseType(baseType);
   }
 }
 
 class StringType extends Type {
-  constructor(enumeration, minLength, maxLength, pattern, format) {
-    super();
+  constructor(schema, enumeration, minLength, maxLength, pattern, format) {
+    super(schema);
     this.enumeration = enumeration;
     this.minLength = minLength;
     this.maxLength = maxLength;
     this.pattern = pattern;
     this.format = format;
   }
 
   normalize(value, context) {
@@ -351,30 +463,30 @@ class StringType extends Type {
 
     return r;
   }
 
   checkBaseType(baseType) {
     return baseType == "string";
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.enumeration) {
       let obj = Cu.createObjectIn(dest, {defineAs: name});
       for (let e of this.enumeration) {
         let key = e.toUpperCase();
         obj[key] = e;
       }
     }
   }
 }
 
 class ObjectType extends Type {
-  constructor(properties, additionalProperties, patternProperties, isInstanceOf) {
-    super();
+  constructor(schema, properties, additionalProperties, patternProperties, isInstanceOf) {
+    super(schema);
     this.properties = properties;
     this.additionalProperties = additionalProperties;
     this.patternProperties = patternProperties;
     this.isInstanceOf = isInstanceOf;
   }
 
   extend(type) {
     for (let key of Object.keys(type.properties)) {
@@ -526,18 +638,18 @@ class NumberType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "number" || baseType == "integer";
   }
 }
 
 class IntegerType extends Type {
-  constructor(minimum, maximum) {
-    super();
+  constructor(schema, minimum, maximum) {
+    super(schema);
     this.minimum = minimum;
     this.maximum = maximum;
   }
 
   normalize(value, context) {
     let r = this.normalizeBase("integer", value, context);
     if (r.error) {
       return r;
@@ -569,18 +681,18 @@ class BooleanType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "boolean";
   }
 }
 
 class ArrayType extends Type {
-  constructor(itemType, minItems, maxItems) {
-    super();
+  constructor(schema, itemType, minItems, maxItems) {
+    super(schema);
     this.itemType = itemType;
     this.minItems = minItems;
     this.maxItems = maxItems;
   }
 
   normalize(value, context) {
     let v = this.normalizeBase("array", value, context);
     if (v.error) {
@@ -608,112 +720,111 @@ class ArrayType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "array";
   }
 }
 
 class FunctionType extends Type {
-  constructor(parameters, isAsync) {
-    super();
+  constructor(schema, parameters, isAsync) {
+    super(schema);
     this.parameters = parameters;
     this.isAsync = isAsync;
   }
 
   normalize(value, context) {
     return this.normalizeBase("function", value, context);
   }
 
   checkBaseType(baseType) {
     return baseType == "function";
   }
 }
 
 // Represents a "property" defined in a schema namespace with a
 // particular value. Essentially this is a constant.
 class ValueProperty extends Entry {
-  constructor(name, value) {
-    super();
+  constructor(schema, name, value) {
+    super(schema);
     this.name = name;
     this.value = value;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     dest[name] = this.value;
   }
 }
 
 // Represents a "property" defined in a schema namespace that is not a
 // constant.
 class TypeProperty extends Entry {
-  constructor(namespaceName, name, type, writable) {
-    super();
+  constructor(schema, namespaceName, name, type, writable) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.name = name;
     this.type = type;
     this.writable = writable;
   }
 
-  throwError(global, msg) {
-    global = Cu.getGlobalForObject(global);
-    throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
+  throwError(context, msg) {
+    throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     let getStub = () => {
-      return wrapperFuncs.getProperty(this.namespaceName, name);
+      this.checkDeprecated(context);
+      return context.getProperty(this.namespaceName, name);
     };
 
     let desc = {
       configurable: false,
       enumerable: true,
 
       get: Cu.exportFunction(getStub, dest),
     };
 
     if (this.writable) {
       let setStub = (value) => {
-        let normalized = this.type.normalize(value);
+        let normalized = this.type.normalize(value, context);
         if (normalized.error) {
-          this.throwError(dest, normalized.error);
+          this.throwError(context, normalized.error);
         }
 
-        wrapperFuncs.setProperty(this.namespaceName, name, normalized.value);
+        context.setProperty(this.namespaceName, name, normalized.value);
       };
 
       desc.set = Cu.exportFunction(setStub, dest);
     }
 
     Object.defineProperty(dest, name, desc);
   }
 }
 
 // This class is a base class for FunctionEntrys and Events. It takes
 // care of validating parameter lists (i.e., handling of optional
 // parameters and parameter type checking).
 class CallEntry extends Entry {
-  constructor(namespaceName, name, parameters, allowAmbiguousOptionalArguments) {
-    super();
+  constructor(schema, namespaceName, name, parameters, allowAmbiguousOptionalArguments) {
+    super(schema);
     this.namespaceName = namespaceName;
     this.name = name;
     this.parameters = parameters;
     this.allowAmbiguousOptionalArguments = allowAmbiguousOptionalArguments;
   }
 
-  throwError(global, msg) {
-    global = Cu.getGlobalForObject(global);
-    throw new global.Error(`${msg} for ${this.namespaceName}.${this.name}.`);
+  throwError(context, msg) {
+    throw context.makeError(`${msg} for ${this.namespaceName}.${this.name}.`);
   }
 
-  checkParameters(args, global, context) {
+  checkParameters(args, context) {
     let fixedArgs = [];
 
     // First we create a new array, fixedArgs, that is the same as
     // |args| but with null values in place of omitted optional
     // parameters.
     let check = (parameterIndex, argIndex) => {
       if (parameterIndex == this.parameters.length) {
         if (argIndex == args.length) {
@@ -751,108 +862,107 @@ class CallEntry extends Entry {
 
     if (this.allowAmbiguousOptionalArguments) {
       // When this option is set, it's up to the implementation to
       // parse arguments.
       return args;
     } else {
       let success = check(0, 0);
       if (!success) {
-        this.throwError(global, "Incorrect argument types");
+        this.throwError(context, "Incorrect argument types");
       }
     }
 
     // Now we normalize (and fully type check) all non-omitted arguments.
     fixedArgs = fixedArgs.map((arg, parameterIndex) => {
       if (arg === null) {
         return null;
       } else {
         let parameter = this.parameters[parameterIndex];
         let r = parameter.type.normalize(arg, context);
         if (r.error) {
-          this.throwError(global, `Type error for parameter ${parameter.name} (${r.error})`);
+          this.throwError(context, `Type error for parameter ${parameter.name} (${r.error})`);
         }
         return r.value;
       }
     });
 
     return fixedArgs;
   }
 }
 
 // Represents a "function" defined in a schema namespace.
 class FunctionEntry extends CallEntry {
-  constructor(namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments, returns) {
-    super(namespaceName, name, type.parameters, allowAmbiguousOptionalArguments);
+  constructor(schema, namespaceName, name, type, unsupported, allowAmbiguousOptionalArguments, returns) {
+    super(schema, namespaceName, name, type.parameters, allowAmbiguousOptionalArguments);
     this.unsupported = unsupported;
     this.returns = returns;
 
     this.isAsync = type.isAsync;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
-    let context = new Context(wrapperFuncs);
     let stub;
     if (this.isAsync) {
       stub = (...args) => {
-        let actuals = this.checkParameters(args, dest, context);
+        this.checkDeprecated(context);
+        let actuals = this.checkParameters(args, context);
         let callback = actuals.pop();
-        return wrapperFuncs.callAsyncFunction(this.namespaceName, name, actuals, callback);
+        return context.callAsyncFunction(this.namespaceName, name, actuals, callback);
       };
     } else {
       stub = (...args) => {
-        let actuals = this.checkParameters(args, dest, context);
-        return wrapperFuncs.callFunction(this.namespaceName, name, actuals);
+        this.checkDeprecated(context);
+        let actuals = this.checkParameters(args, context);
+        return context.callFunction(this.namespaceName, name, actuals);
       };
     }
     Cu.exportFunction(stub, dest, {defineAs: name});
   }
 }
 
 // Represents an "event" defined in a schema namespace.
 class Event extends CallEntry {
-  constructor(namespaceName, name, type, extraParameters, unsupported) {
-    super(namespaceName, name, extraParameters);
+  constructor(schema, namespaceName, name, type, extraParameters, unsupported) {
+    super(schema, namespaceName, name, extraParameters);
     this.type = type;
     this.unsupported = unsupported;
   }
 
-  checkListener(global, listener, context) {
+  checkListener(listener, context) {
     let r = this.type.normalize(listener, context);
     if (r.error) {
-      this.throwError(global, "Invalid listener");
+      this.throwError(context, "Invalid listener");
     }
     return r.value;
   }
 
-  inject(name, dest, wrapperFuncs) {
+  inject(name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
-    let context = new Context(wrapperFuncs);
-
     let addStub = (listener, ...args) => {
-      listener = this.checkListener(dest, listener, context);
-      let actuals = this.checkParameters(args, dest, context);
-      return wrapperFuncs.addListener(this.namespaceName, name, listener, actuals);
+      listener = this.checkListener(listener, context);
+      let actuals = this.checkParameters(args, context);
+      return context.addListener(this.namespaceName, name, listener, actuals);
     };
 
     let removeStub = (listener) => {
-      listener = this.checkListener(dest, listener, context);
-      return wrapperFuncs.removeListener(this.namespaceName, name, listener);
+      listener = this.checkListener(listener, context);
+      return context.removeListener(this.namespaceName, name, listener);
     };
 
     let hasStub = (listener) => {
-      listener = this.checkListener(dest, listener, context);
-      return wrapperFuncs.hasListener(this.namespaceName, name, listener);
+      listener = this.checkListener(listener, context);
+      return context.hasListener(this.namespaceName, name, listener);
     };
 
     let obj = Cu.createObjectIn(dest, {defineAs: name});
     Cu.exportFunction(addStub, obj, {defineAs: "addListener"});
     Cu.exportFunction(removeStub, obj, {defineAs: "removeListener"});
     Cu.exportFunction(hasStub, obj, {defineAs: "hasListener"});
   }
 }
@@ -871,37 +981,37 @@ this.Schemas = {
     ns.set(symbol, value);
   },
 
   parseType(namespaceName, type, extraProperties = []) {
     let allowedProperties = new Set(extraProperties);
 
     // Do some simple validation of our own schemas.
     function checkTypeProperties(...extra) {
-      let allowedSet = new Set([...allowedProperties, ...extra, "description"]);
+      let allowedSet = new Set([...allowedProperties, ...extra, "description", "deprecated"]);
       for (let prop of Object.keys(type)) {
         if (!allowedSet.has(prop)) {
           throw new Error(`Internal error: Namespace ${namespaceName} has invalid type property "${prop}" in type "${type.id || JSON.stringify(type)}"`);
         }
       }
     }
 
     if ("choices" in type) {
       checkTypeProperties("choices");
 
       let choices = type.choices.map(t => this.parseType(namespaceName, t));
-      return new ChoiceType(choices);
+      return new ChoiceType(type, choices);
     } else if ("$ref" in type) {
       checkTypeProperties("$ref");
       let ref = type.$ref;
       let ns = namespaceName;
       if (ref.includes(".")) {
         [ns, ref] = ref.split(".");
       }
-      return new RefType(ns, ref);
+      return new RefType(type, ns, ref);
     }
 
     if (!("type" in type)) {
       throw new Error(`Unexpected value for type: ${JSON.stringify(type)}`);
     }
 
     allowedProperties.add("type");
 
@@ -934,26 +1044,26 @@ this.Schemas = {
 
       let format = null;
       if (type.format) {
         if (!(type.format in FORMATS)) {
           throw new Error(`Internal error: Invalid string format ${type.format}`);
         }
         format = FORMATS[type.format];
       }
-      return new StringType(enumeration,
+      return new StringType(type, enumeration,
                             type.minLength || 0,
                             type.maxLength || Infinity,
                             pattern,
                             format);
     } else if (type.type == "object") {
       let parseProperty = (type, extraProps = []) => {
         return {
           type: this.parseType(namespaceName, type,
-                               ["unsupported", "deprecated", ...extraProps]),
+                               ["unsupported", ...extraProps]),
           optional: type.optional || false,
           unsupported: type.unsupported || false,
         };
       };
 
       let properties = Object.create(null);
       for (let propName of Object.keys(type.properties || {})) {
         properties[propName] = parseProperty(type.properties[propName], ["optional"]);
@@ -980,30 +1090,30 @@ this.Schemas = {
       }
 
       if ("$extend" in type) {
         // Only allow extending "properties" and "patternProperties".
         checkTypeProperties("properties", "patternProperties");
       } else {
         checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf");
       }
-      return new ObjectType(properties, additionalProperties, patternProperties, type.isInstanceOf || null);
+      return new ObjectType(type, properties, additionalProperties, patternProperties, type.isInstanceOf || null);
     } else if (type.type == "array") {
       checkTypeProperties("items", "minItems", "maxItems");
-      return new ArrayType(this.parseType(namespaceName, type.items),
+      return new ArrayType(type, this.parseType(namespaceName, type.items),
                            type.minItems || 0, type.maxItems || Infinity);
     } else if (type.type == "number") {
       checkTypeProperties();
-      return new NumberType();
+      return new NumberType(type);
     } else if (type.type == "integer") {
       checkTypeProperties("minimum", "maximum");
-      return new IntegerType(type.minimum || 0, type.maximum || Infinity);
+      return new IntegerType(type, type.minimum || 0, type.maximum || Infinity);
     } else if (type.type == "boolean") {
       checkTypeProperties();
-      return new BooleanType();
+      return new BooleanType(type);
     } else if (type.type == "function") {
       let isAsync = typeof(type.async) == "string";
 
       let parameters = null;
       if ("parameters" in type) {
         parameters = [];
         for (let param of type.parameters) {
           // Callbacks default to optional for now, because of promise
@@ -1023,21 +1133,21 @@ this.Schemas = {
           throw new Error(`Internal error: "async" property must name the last parameter of the function.`);
         }
         if (type.returns || type.allowAmbiguousOptionalArguments) {
           throw new Error(`Internal error: Async functions must not have return values or ambiguous arguments.`);
         }
       }
 
       checkTypeProperties("parameters", "async", "returns");
-      return new FunctionType(parameters, isAsync);
+      return new FunctionType(type, parameters, isAsync);
     } else if (type.type == "any") {
       // Need to see what minimum and maximum are supposed to do here.
       checkTypeProperties("minimum", "maximum");
-      return new AnyType();
+      return new AnyType(type);
     } else {
       throw new Error(`Unexpected type ${type.type}`);
     }
   },
 
   loadType(namespaceName, type) {
     if ("$extend" in type) {
       this.extendType(namespaceName, type);
@@ -1064,30 +1174,29 @@ this.Schemas = {
       throw new Error(`Internal error: Bad attempt to extend ${type.$extend}`);
     }
 
     targetType.extend(parsed);
   },
 
   loadProperty(namespaceName, name, prop) {
     if ("value" in prop) {
-      this.register(namespaceName, name, new ValueProperty(name, prop.value));
+      this.register(namespaceName, name, new ValueProperty(prop, name, prop.value));
     } else {
       // We ignore the "optional" attribute on properties since we
       // don't inject anything here anyway.
       let type = this.parseType(namespaceName, prop, ["optional", "writable"]);
-      this.register(namespaceName, name, new TypeProperty(namespaceName, name, type),
-                    prop.writable);
+      this.register(namespaceName, name, new TypeProperty(prop, namespaceName, name, type, prop.writable || false));
     }
   },
 
   loadFunction(namespaceName, fun) {
-    let f = new FunctionEntry(namespaceName, fun.name,
+    let f = new FunctionEntry(fun, namespaceName, fun.name,
                               this.parseType(namespaceName, fun,
-                                             ["name", "unsupported", "deprecated", "returns",
+                                             ["name", "unsupported", "returns",
                                               "allowAmbiguousOptionalArguments"]),
                               fun.unsupported || false,
                               fun.allowAmbiguousOptionalArguments || false,
                               fun.returns || null);
     this.register(namespaceName, fun.name, f);
   },
 
   loadEvent(namespaceName, event) {
@@ -1102,20 +1211,20 @@ this.Schemas = {
 
     // We ignore these properties for now.
     /* eslint-disable no-unused-vars */
     let returns = event.returns;
     let filters = event.filters;
     /* eslint-enable no-unused-vars */
 
     let type = this.parseType(namespaceName, event,
-                              ["name", "unsupported", "deprecated",
+                              ["name", "unsupported",
                                "extraParameters", "returns", "filters"]);
 
-    let e = new Event(namespaceName, event.name, type, extras,
+    let e = new Event(event, namespaceName, event.name, type, extras,
                       event.unsupported || false);
     this.register(namespaceName, event.name, e);
   },
 
   load(uri) {
     return readJSON(uri).then(json => {
       for (let namespace of json) {
         let name = namespace.namespace;
--- a/toolkit/components/extensions/ext-runtime.js
+++ b/toolkit/components/extensions/ext-runtime.js
@@ -80,11 +80,31 @@ extensions.registerSchemaAPI("runtime", 
           arch = "x86-32";
         } else if (arch == "x86_64") {
           arch = "x86-64";
         }
 
         let info = {os, arch};
         return Promise.resolve(info);
       },
+
+      setUninstallURL: function(url) {
+        if (url.length == 0) {
+          return Promise.resolve();
+        }
+
+        let uri;
+        try {
+          uri = NetUtil.newURI(url);
+        } catch (e) {
+          return Promise.reject({ message: `Invalid URL: ${JSON.stringify(url)}` });
+        }
+
+        if (uri.scheme != "http" && uri.scheme != "https") {
+          return Promise.reject({ message: "url must have the scheme http or https" });
+        }
+
+        extension.uninstallURL = url;
+        return Promise.resolve();
+      },
     },
   };
 });
--- a/toolkit/components/extensions/schemas/manifest.json
+++ b/toolkit/components/extensions/schemas/manifest.json
@@ -97,31 +97,35 @@
             "items": { "$ref": "ContentScript" }
           },
 
           "permissions": {
             "type": "array",
             "items": {
               "choices": [
                 { "$ref": "Permission" },
-                { "type": "string" }
+                {
+                  "type": "string",
+                  "deprecated": "Unknown permission ${value}"
+                }
               ]
             },
             "optional": true
           },
 
           "web_accessible_resources": {
             "type": "array",
             "items": { "type": "string" },
             "optional": true
           }
         },
 
         "additionalProperties": {
-          "type": "any"
+          "type": "any",
+          "deprecated": "An unexpected property was found in the WebExtension manifest"
         }
       },
       {
         "id": "Permission",
         "choices": [
           {
             "type": "string",
             "enum": [
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -170,17 +170,16 @@
         ],
         "returns": {
           "type": "string",
           "description": "The fully-qualified URL to the resource."
         }
       },
       {
         "name": "setUninstallURL",
-        "unsupported": true,
         "type": "function",
         "description": "Sets the URL to be visited upon uninstallation. This may be used to clean up server-side data, do analytics, and implement surveys. Maximum 255 characters.",
         "async": "callback",
         "parameters": [
           {
             "type": "string",
             "name": "url",
             "maxLength": 255,
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -20,16 +20,17 @@ support-files =
   file_sample.html
   redirection.sjs
   file_privilege_escalation.html
   file_ext_test_api_injection.js
   file_permission_xhr.html
 
 [test_ext_simple.html]
 [test_ext_schema.html]
+skip-if = e10s # Uses a console montitor. Actual code does not depend on e10s.
 [test_ext_geturl.html]
 [test_ext_contentscript.html]
 skip-if = buildapp == 'b2g' # runat != document_idle is not supported.
 [test_ext_contentscript_create_iframe.html]
 [test_ext_contentscript_api_injection.html]
 [test_ext_downloads.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
--- a/toolkit/components/extensions/test/mochitest/test_ext_schema.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_schema.html
@@ -8,29 +8,63 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-add_task(function* testSchema() {
+add_task(function* testEmptySchema() {
   function background() {
     browser.test.assertTrue(!("manifest" in browser), "browser.manifest is not defined");
     browser.test.notifyPass("schema");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${background})()`,
   });
 
 
   yield extension.startup();
 
   yield extension.awaitFinish("schema");
 
   yield extension.unload();
 });
+
+add_task(function* testUnknownProperties() {
+  function background() {
+    browser.test.notifyPass("loaded");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["unknownPermission"],
+
+      unknown_property: {},
+    },
+
+    background: `(${background})()`,
+  });
+
+  let messages = [
+    {message: /processing permissions\.0: Unknown permission "unknownPermission"/},
+    {message: /processing unknown_property: An unexpected property was found in the WebExtension manifest/},
+  ];
+
+  let waitForConsole = new Promise(resolve => {
+    SimpleTest.monitorConsole(resolve, messages);
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitFinish("loaded");
+
+  yield extension.unload();
+
+  SimpleTest.endMonitorConsole();
+  yield waitForConsole;
+});
 </script>
 
 </body>
 </html>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -270,27 +270,51 @@ function tally(kind, ns, name, args) {
   tallied = [kind, ns, name, args];
 }
 
 function verify(...args) {
   do_check_eq(JSON.stringify(tallied), JSON.stringify(args));
   tallied = null;
 }
 
+let talliedErrors = [];
+
+function checkErrors(errors) {
+  do_check_eq(talliedErrors.length, errors.length, "Got expected number of errors");
+  for (let [i, error] of errors.entries()) {
+    do_check_true(i in talliedErrors && talliedErrors[i].includes(error),
+                  `${JSON.stringify(error)} is a substring of error ${JSON.stringify(talliedErrors[i])}`);
+  }
+
+  talliedErrors.length = 0;
+}
+
 let wrapper = {
   url: "moz-extension://b66e3509-cdb3-44f6-8eb8-c8b39b3a1d27/",
 
   checkLoadURL(url) {
     return !url.startsWith("chrome:");
   },
 
+  logError(message) {
+    talliedErrors.push(message);
+  },
+
   callFunction(ns, name, args) {
     tally("call", ns, name, args);
   },
 
+  getProperty(ns, name) {
+    tally("get", ns, name);
+  },
+
+  setProperty(ns, name, value) {
+    tally("set", ns, name, value);
+  },
+
   addListener(ns, name, listener, args) {
     tally("addListener", ns, name, [listener, args]);
   },
   removeListener(ns, name, listener) {
     tally("removeListener", ns, name, [listener]);
   },
   hasListener(ns, name, listener) {
     tally("hasListener", ns, name, [listener]);
@@ -560,8 +584,175 @@ add_task(function* () {
   root.testing.extended2(12);
   verify("call", "testing", "extended2", [12]);
   tallied = null;
 
   Assert.throws(() => root.testing.extended2(true),
                 /Incorrect argument types/,
                 "should throw for wrong argument type");
 });
+
+let deprecatedJson = [
+  {namespace: "deprecated",
+
+   properties: {
+     accessor: {
+       type: "string",
+       writable: true,
+       deprecated: "This is not the property you are looking for",
+     },
+   },
+
+   types: [
+     {
+       "id": "Type",
+       "type": "string",
+     },
+   ],
+
+   functions: [
+     {
+       name: "property",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           type: "object",
+           properties: {
+             foo: {
+               type: "string",
+             },
+           },
+           additionalProperties: {
+             type: "any",
+             deprecated: "Unknown property",
+           },
+         },
+       ],
+     },
+
+     {
+       name: "value",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           choices: [
+             {
+               type: "integer",
+             },
+             {
+               type: "string",
+               deprecated: "Please use an integer, not ${value}",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "choices",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           deprecated: "You have no choices",
+           choices: [
+             {
+               type: "integer",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "ref",
+       type: "function",
+       parameters: [
+         {
+           name: "arg",
+           choices: [
+             {
+               $ref: "Type",
+               deprecated: "Deprecated alias",
+             },
+           ],
+         },
+       ],
+     },
+
+     {
+       name: "method",
+       type: "function",
+       deprecated: "Do not call this method",
+       parameters: [
+       ],
+     },
+   ],
+
+   events: [
+     {
+       name: "onDeprecated",
+       type: "function",
+       deprecated: "This event does not work",
+     },
+   ],
+  },
+];
+
+add_task(function* testDeprecation() {
+  let url = "data:," + JSON.stringify(deprecatedJson);
+  let uri = BrowserUtils.makeURI(url);
+  yield Schemas.load(uri);
+
+  let root = {};
+  Schemas.inject(root, wrapper);
+
+  talliedErrors.length = 0;
+
+
+  root.deprecated.property({foo: "bar", xxx: "any", yyy: "property"});
+  verify("call", "deprecated", "property", [{foo: "bar", xxx: "any", yyy: "property"}]);
+  checkErrors([
+    "Error processing xxx: Unknown property",
+    "Error processing yyy: Unknown property",
+  ]);
+
+  root.deprecated.value(12);
+  verify("call", "deprecated", "value", [12]);
+  checkErrors([]);
+
+  root.deprecated.value("12");
+  verify("call", "deprecated", "value", ["12"]);
+  checkErrors(["Please use an integer, not \"12\""]);
+
+  root.deprecated.choices(12);
+  verify("call", "deprecated", "choices", [12]);
+  checkErrors(["You have no choices"]);
+
+  root.deprecated.ref("12");
+  verify("call", "deprecated", "ref", ["12"]);
+  checkErrors(["Deprecated alias"]);
+
+  root.deprecated.method();
+  verify("call", "deprecated", "method", []);
+  checkErrors(["Do not call this method"]);
+
+
+  void root.deprecated.accessor;
+  verify("get", "deprecated", "accessor", null);
+  checkErrors(["This is not the property you are looking for"]);
+
+  root.deprecated.accessor = "x";
+  verify("set", "deprecated", "accessor", "x");
+  checkErrors(["This is not the property you are looking for"]);
+
+
+  root.deprecated.onDeprecated.addListener(() => {});
+  checkErrors(["This event does not work"]);
+
+  root.deprecated.onDeprecated.removeListener(() => {});
+  checkErrors(["This event does not work"]);
+
+  root.deprecated.onDeprecated.hasListener(() => {});
+  checkErrors(["This event does not work"]);
+});
--- a/toolkit/components/places/nsPlacesAutoComplete.js
+++ b/toolkit/components/places/nsPlacesAutoComplete.js
@@ -167,17 +167,17 @@ function stripPrefix(aURIString)
 
   if (uri.indexOf("www.") == 0) {
     uri = uri.slice(4);
   }
   return uri;
 }
 
 /**
- * safePrefGetter get the pref with typo safety.
+ * safePrefGetter get the pref with type safety.
  * This will return the default value provided if no pref is set.
  *
  * @param aPrefBranch
  *        The nsIPrefBranch containing the required preference
  * @param aName
  *        A preference name
  * @param aDefault
  *        The preference's default value
@@ -189,17 +189,21 @@ function safePrefGetter(aPrefBranch, aNa
     boolean: "Bool",
     number: "Int",
     string: "Char"
   };
   let type = types[typeof(aDefault)];
   if (!type) {
     throw "Unknown type!";
   }
+
   // If the pref isn't set, we want to use the default.
+  if (aPrefBranch.getPrefType(aName) == Ci.nsIPrefBranch.PREF_INVALID) {
+    return aDefault;
+  }
   try {
     return aPrefBranch["get" + type + "Pref"](aName);
   }
   catch (e) {
     return aDefault;
   }
 }
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8328,24 +8328,16 @@
   "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1": {
     "alert_emails": ["firefox-dev@mozilla.org", "dmose@mozilla.com"],
     "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 8,
     "releaseChannelCollection": "opt-out",
     "description": "Connection length for bi-directionally connected media (0=SHORTER_THAN_10S, 1=BETWEEN_10S_AND_30S, 2=BETWEEN_30S_AND_5M, 3=MORE_THAN_5M)"
   },
-  "LOOP_SHARING_STATE_CHANGE_1": {
-    "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
-    "expires_in_version": "48",
-    "kind": "enumerated",
-    "n_values": 8,
-    "releaseChannelCollection": "opt-out",
-    "description": "Number of times the sharing feature has been enabled and disabled (0=WINDOW_ENABLED, 1=WINDOW_DISABLED, 2=BROWSER_ENABLED, 3=BROWSER_DISABLED)"
-  },
   "LOOP_SHARING_ROOM_URL": {
     "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
     "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 8,
     "releaseChannelCollection": "opt-out",
     "description": "Number of times a room URL is shared (0=COPY_FROM_PANEL, 1=COPY_FROM_CONVERSATION, 2=EMAIL_FROM_CALLFAILED, 3=EMAIL_FROM_CONVERSATION, 4=FACEBOOK_FROM_CONVERSATION)"
   },
@@ -8360,31 +8352,16 @@
   "LOOP_ROOM_DELETE": {
     "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
     "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 4,
     "releaseChannelCollection": "opt-out",
     "description": "Number of times a room delete action is performed (0=DELETE_SUCCESS, 2=DELETE_FAIL)"
   },
-  "LOOP_ROOM_CONTEXT_ADD": {
-    "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
-    "expires_in_version": "48",
-    "kind": "enumerated",
-    "n_values": 8,
-    "releaseChannelCollection": "opt-out",
-    "description": "Number of times a room context action is performed (0=ADD_FROM_PANEL, 1=ADD_FROM_CONVERSATION)"
-  },
-  "LOOP_ROOM_CONTEXT_CLICK": {
-    "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
-    "expires_in_version": "48",
-    "kind": "count",
-    "releaseChannelCollection": "opt-out",
-    "description": "Number times room context is clicked to visit the attached URL"
-  },
   "LOOP_ROOM_SESSION_WITHCHAT": {
     "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
     "expires_in_version": "50",
     "kind": "count",
     "releaseChannelCollection": "opt-out",
     "description": "Number of sessions where at least one chat message was exchanged"
   },
   "E10S_STATUS": {
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -1193,17 +1193,17 @@ EnvironmentCache.prototype = {
   },
 
   /**
    * Get the device information, if we are on a portable device.
    * @return Object containing the device information data, or null if
    * not a portable device.
    */
   _getDeviceData: function () {
-    if (["gonk", "android"].indexOf(AppConstants.platform) === -1) {
+    if (!["gonk", "android"].includes(AppConstants.platform)) {
       return null;
     }
 
     return {
       model: getSysinfoProperty("device", null),
       manufacturer: getSysinfoProperty("manufacturer", null),
       hardware: getSysinfoProperty("hardware", null),
       isTablet: getSysinfoProperty("tablet", null),
@@ -1216,17 +1216,17 @@ EnvironmentCache.prototype = {
    */
   _getOSData: function () {
     let data = {
       name: getSysinfoProperty("name", null),
       version: getSysinfoProperty("version", null),
       locale: getSystemLocale(),
     };
 
-    if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) {
+    if (["gonk", "android"].includes(AppConstants.platform)) {
       data.kernelVersion = getSysinfoProperty("kernel_version", null);
     } else if (AppConstants.platform === "win") {
       let servicePack = getServicePack();
       data.servicePackMajor = servicePack.major;
       data.servicePackMinor = servicePack.minor;
       data.installYear = getSysinfoProperty("installYear", null);
     }
 
@@ -1265,17 +1265,17 @@ EnvironmentCache.prototype = {
       // The following line is disabled due to main thread jank and will be enabled
       // again as part of bug 1154500.
       //DWriteVersion: getGfxField("DWriteVersion", null),
       adapters: [],
       monitors: [],
       features: {},
     };
 
-    if (["gonk", "android", "linux"].indexOf(AppConstants.platform) === -1) {
+    if (!["gonk", "android", "linux"].includes(AppConstants.platform)) {
       let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
       try {
         gfxData.monitors = gfxInfo.getMonitors();
       } catch (e) {
         this._log.error("nsIGfxInfo.getMonitors() caught error", e);
       }
     }
 
@@ -1330,17 +1330,17 @@ EnvironmentCache.prototype = {
       cpu: this._getCpuData(),
       os: this._getOSData(),
       hdd: this._getHDDData(),
       gfx: this._getGFXData(),
     };
 
     if (AppConstants.platform === "win") {
       data.isWow64 = getSysinfoProperty("isWow64", null);
-    } else if (["gonk", "android"].indexOf(AppConstants.platform) !== -1) {
+    } else if (["gonk", "android"].includes(AppConstants.platform)) {
       data.device = this._getDeviceData();
     }
 
     return data;
   },
 
   _onEnvironmentChange: function (what, oldEnvironment) {
     this._log.trace("_onEnvironmentChange for " + what);
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1271,17 +1271,17 @@ var Impl = {
     this._subsessionId = Policy.generateSubsessionUUID();
     this._subsessionCounter++;
     this._profileSubsessionCounter++;
   },
 
   getSessionPayload: function getSessionPayload(reason, clearSubsession) {
     this._log.trace("getSessionPayload - reason: " + reason + ", clearSubsession: " + clearSubsession);
 
-    const isMobile = ["gonk", "android"].indexOf(AppConstants.platform) !== -1;
+    const isMobile = ["gonk", "android"].includes(AppConstants.platform);
     const isSubsession = isMobile ? false : !this._isClassicReason(reason);
 
     if (isMobile) {
       clearSubsession = false;
     }
 
     let measurements =
       this.getSimpleMeasurements(reason == REASON_SAVED_SESSION, isSubsession, clearSubsession);
@@ -1993,17 +1993,17 @@ var Impl = {
   },
 
   _isClassicReason: function(reason) {
     const classicReasons = [
       REASON_SAVED_SESSION,
       REASON_GATHER_PAYLOAD,
       REASON_TEST_PING,
     ];
-    return classicReasons.indexOf(reason) != -1;
+    return classicReasons.includes(reason);
   },
 
   /**
    * Get an object describing the current state of this module for AsyncShutdown diagnostics.
    */
   _getState: function() {
     return {
       initialized: this._initialized,
--- a/toolkit/components/telemetry/histogram_tools.py
+++ b/toolkit/components/telemetry/histogram_tools.py
@@ -266,17 +266,17 @@ associated with the histogram.  Returns 
                 return v
         self._low = try_to_coerce_to_number(low)
         self._high = try_to_coerce_to_number(high)
         self._n_buckets = try_to_coerce_to_number(n_buckets)
         if n_buckets_whitelist is not None and self._n_buckets > 100 and type(self._n_buckets) is int:
             if self._name not in n_buckets_whitelist:
                 raise KeyError, ('New histogram %s is not permitted to have more than 100 buckets. '
                                 'Histograms with large numbers of buckets use disproportionately high amounts of resources. '
-                                'Contact :vladan or the Perf team if you think an exception ought to be made.' % self._name)
+                                'Contact the Telemetry team (e.g. in #telemetry) if you think an exception ought to be made.' % self._name)
 
     @staticmethod
     def boolean_flag_bucket_parameters(definition):
         return (1, 2, 3)
 
     @staticmethod
     def linear_bucket_parameters(definition):
         return (definition.get('low', 1),
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -401,17 +401,17 @@ function checkPartnerSection(data, isIni
     Assert.strictEqual(data.partner[f], expected, f + " must have the correct value.");
   }
 
   // Check that "partnerNames" exists and contains the correct element.
   Assert.ok(Array.isArray(data.partner.partnerNames));
   if (isInitial) {
     Assert.equal(data.partner.partnerNames.length, 0);
   } else {
-    Assert.ok(data.partner.partnerNames.indexOf(PARTNER_NAME) >= 0);
+    Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME));
   }
 }
 
 function checkGfxAdapter(data) {
   const EXPECTED_ADAPTER_FIELDS_TYPES = {
     description: "string",
     vendorID: "string",
     deviceID: "string",
--- a/toolkit/content/widgets/toolbarbutton.xml
+++ b/toolkit/content/widgets/toolbarbutton.xml
@@ -82,32 +82,32 @@
   </binding>
 
   <binding id="toolbarbutton-badged"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
     <content>
       <children includes="observes|template|menupopup|panel|tooltip"/>
       <xul:stack class="toolbarbutton-badge-stack">
         <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
-        <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0"/>
+        <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
       </xul:stack>
       <xul:label class="toolbarbutton-text" crop="right" flex="1"
                  xbl:inherits="value=label,accesskey,crop,wrap"/>
       <xul:label class="toolbarbutton-multiline-text" flex="1"
                  xbl:inherits="xbl:text=label,accesskey,wrap"/>
     </content>
   </binding>
 
   <binding id="toolbarbutton-badged-menu" display="xul:menu"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
     <content>
       <children includes="observes|template|menupopup|panel|tooltip"/>
       <xul:stack class="toolbarbutton-badge-stack">
         <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label,consumeanchor"/>
-        <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0"/>
+        <xul:label class="toolbarbutton-badge" xbl:inherits="value=badge" top="0" end="0" crop="none"/>
       </xul:stack>
       <xul:label class="toolbarbutton-text" crop="right" flex="1"
                  xbl:inherits="value=label,accesskey,crop,dragover-top,wrap"/>
       <xul:label class="toolbarbutton-multiline-text" flex="1"
                  xbl:inherits="xbl:text=label,accesskey,wrap"/>
       <xul:dropmarker anonid="dropmarker" type="menu"
                       class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
     </content>