Merge fx-team to m-c.
authorRyan VanderMeulen <ryanvm@gmail.com>
Tue, 29 Apr 2014 16:52:33 -0400
changeset 181253 e79ce804e9ec42cbf0bf8f3092db6bdf4105836b
parent 181202 ea9a192b47aca64a957506f536040940d9a5a0d2 (current diff)
parent 181252 83c3a747105114dce8ebc0ca6674819583ba47ef (diff)
child 181272 90267d87c37592209e879db91a6c636d501f9c96
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
milestone32.0a1
Merge fx-team to m-c.
browser/components/sessionstore/test/browser_480148.js
mobile/android/base/RemoteTabsContainer.java
mobile/android/base/RemoteTabsList.java
mobile/android/base/TabsPanel.java
mobile/android/base/TabsTray.java
toolkit/components/search/nsSearchService.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 995411 moves some files around in gfx/layers and widget/xpwidget
+Bug 958889 moves files into the new mobile/android/base/tabspanel/ package.
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1423,17 +1423,17 @@ pref("browser.newtab.preload", true);
 pref("browser.newtabpage.enabled", true);
 
 // number of rows of newtab grid
 pref("browser.newtabpage.rows", 3);
 
 // number of columns of newtab grid
 pref("browser.newtabpage.columns", 3);
 
-pref("browser.newtabpage.directorySource", "chrome://global/content/directoryLinks.json");
+pref("browser.newtabpage.directorySource", "data:application/json,{}");
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // True if the fullscreen API requires approval upon a domain entering fullscreen.
 // Domains that have already had fullscreen permission granted won't re-request
 // approval.
 pref("full-screen-api.approval-required", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5175,17 +5175,18 @@ function UpdateCurrentCharset(target) {
 
     var menuitem = charsetMenuGetElement(target, CharsetMenu.foldCharset(wnd.document.characterSet));
     if (menuitem) {
         menuitem.setAttribute('checked', 'true');
     }
 }
 
 function charsetLoadListener() {
-  var charset = CharsetMenu.foldCharset(window.content.document.characterSet);
+  let currCharset = gBrowser.selectedBrowser.characterSet;
+  let charset = CharsetMenu.foldCharset(currCharset);
 
   if (charset.length > 0 && (charset != gLastBrowserCharset)) {
     gPrevCharset = gLastBrowserCharset;
     gLastBrowserCharset = charset;
   }
 }
 
 var gPageStyleMenu = {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -634,17 +634,17 @@
              overflowable="true"
              overflowbutton="nav-bar-overflow-button"
              overflowtarget="widget-overflow-list"
              overflowpanel="widget-overflow"
              context="toolbar-context-menu">
 
       <hbox id="nav-bar-customization-target" flex="1">
         <toolbaritem id="urlbar-container" flex="400" persist="width"
-                     forwarddisabled="true" title="&locationItem.title;" removable="false"
+                     title="&locationItem.title;" removable="false"
                      cui-areatype="toolbar"
                      class="chromeclass-location" overflows="false">
           <toolbarbutton id="back-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                          label="&backCmd.label;"
                          command="Browser:BackOrBackDuplicate"
                          cui-areatype="toolbar"
                          onclick="checkForMiddleClick(this, event);"
                          tooltip="back-button-tooltip"
--- a/browser/base/content/test/social/browser_social_workercrash.js
+++ b/browser/base/content/test/social/browser_social_workercrash.js
@@ -4,23 +4,27 @@
 
 // This tests our recovery if a child content process hosting providers
 // crashes.
 
 // A content script we inject into one of our browsers
 const TEST_CONTENT_HELPER = "chrome://mochitests/content/browser/browser/base/content/test/social/social_crash_content_helper.js";
 
 let {getFrameWorkerHandle} = Cu.import("resource://gre/modules/FrameWorker.jsm", {});
+let {Promise} = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
 function test() {
   waitForExplicitFinish();
 
   // We need to ensure all our workers are in the same content process.
   Services.prefs.setIntPref("dom.ipc.processCount", 1);
 
+  // This test generates many uncaught promises that should not cause failures.
+  Promise.Debugging.clearUncaughtErrorObservers();
+
   runSocialTestWithProvider(gProviders, function (finishcb) {
     runSocialTests(tests, undefined, undefined, function() {
       Services.prefs.clearUserPref("dom.ipc.processCount");
       finishcb();
     });
   });
 }
 
--- a/browser/components/sessionstore/src/nsSessionStartup.js
+++ b/browser/components/sessionstore/src/nsSessionStartup.js
@@ -121,25 +121,26 @@ SessionStartup.prototype = {
     // No valid session found.
     if (!stateString) {
       this._sessionType = Ci.nsISessionStartup.NO_SESSION;
       Services.obs.notifyObservers(null, "sessionstore-state-finalized", "");
       gOnceInitializedDeferred.resolve();
       return;
     }
 
-    this._initialState =  this._parseStateString(stateString);
+    this._initialState = this._parseStateString(stateString);
 
     let shouldResumeSessionOnce = Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
     let shouldResumeSession = shouldResumeSessionOnce ||
           Services.prefs.getIntPref("browser.startup.page") == BROWSER_STARTUP_RESUME_SESSION;
 
     // If this is a normal restore then throw away any previous session
-    if (!shouldResumeSessionOnce)
+    if (!shouldResumeSessionOnce && this._initialState) {
       delete this._initialState.lastSessionState;
+    }
 
     let resumeFromCrash = Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash");
 
     CrashMonitor.previousCheckpoints.then(checkpoints => {
       if (checkpoints) {
         // If the previous session finished writing the final state, we'll
         // assume there was no crash.
         this._previousSessionCrashed = !checkpoints["sessionstore-final-state-write-complete"];
--- a/browser/components/sessionstore/test/browser_522545.js
+++ b/browser/components/sessionstore/test/browser_522545.js
@@ -241,26 +241,25 @@ function test() {
     });
   }
 
 
   let tests = [test_newTabFocused, test_newTabNotFocused,
                test_existingSHEnd_noClear, test_existingSHMiddle_noClear,
                test_getBrowserState_lotsOfTabsOpening,
                test_getBrowserState_userTypedValue, test_userTypedClearLoadURI];
-  let originalState = ss.getBrowserState();
+  let originalState = JSON.parse(ss.getBrowserState());
   let state = {
     windows: [{
       tabs: [{ entries: [{ url: "about:blank" }] }]
     }]
   };
   function runNextTest() {
     if (tests.length) {
       waitForBrowserState(state, tests.shift());
     } else {
-      ss.setBrowserState(originalState);
-      executeSoon(finish);
+      waitForBrowserState(originalState, finish);
     }
   }
 
   // Run the tests!
   runNextTest();
 }
--- a/browser/components/sessionstore/test/browser_600545.js
+++ b/browser/components/sessionstore/test/browser_600545.js
@@ -1,13 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-let stateBackup = ss.getBrowserState();
+let stateBackup = JSON.parse(ss.getBrowserState());
 
 function test() {
   /** Test for Bug 600545 **/
   waitForExplicitFinish();
   testBug600545();
 }
 
 function testBug600545() {
@@ -67,18 +67,17 @@ function done() {
   // use waitForFocus() because apparently it's buggy. See bug 599253.
   let windowsEnum = Services.wm.getEnumerator("navigator:browser");
   while (windowsEnum.hasMoreElements()) {
     let currentWindow = windowsEnum.getNext();
     if (currentWindow != window)
       currentWindow.close();
   }
 
-  ss.setBrowserState(stateBackup);
-  executeSoon(finish);
+  waitForBrowserState(stateBackup, finish);
 }
 
 // Count up the number of tabs in the state data
 function getStateTabCount(aState) {
   let tabCount = 0;
   for (let i in aState.windows)
     tabCount += aState.windows[i].tabs.length;
   return tabCount;
--- a/browser/components/sessionstore/test/browser_601955.js
+++ b/browser/components/sessionstore/test/browser_601955.js
@@ -41,14 +41,14 @@ function testBug601955_3() {
   let state = JSON.parse(ss.getBrowserState());
   ok(!state.windows[0].tabs[0].pinned, "first tab should not be pinned");
   ok(!state.windows[0].tabs[1].pinned, "second tab should not be pinned");
 
   done();
 }
 
 function done() {
-  gBrowser.removeTab(window.gBrowser.tabs[0]);
+  gBrowser.removeTab(window.gBrowser.tabs[1]);
 
   Services.prefs.clearUserPref("browser.sessionstore.interval");
 
   executeSoon(finish);
 }
--- a/browser/components/sessionstore/test/head.js
+++ b/browser/components/sessionstore/test/head.js
@@ -160,16 +160,19 @@ function waitForBrowserState(aState, aSe
         win.gBrowser.tabContainer.removeEventListener("SSTabRestored", onSSTabRestored, true);
       });
     }
   });
   // Add the event listener for this window as well.
   listening = true;
   gBrowser.tabContainer.addEventListener("SSTabRestored", onSSTabRestored, true);
 
+  // Ensure setBrowserState() doesn't remove the initial tab.
+  gBrowser.selectedTab = gBrowser.tabs[0];
+
   // Finally, call setBrowserState
   ss.setBrowserState(JSON.stringify(aState));
 }
 
 // Doesn't assume that the tab needs to be closed in a cleanup function.
 // If that's the case, the test author should handle that in the test.
 function waitForTabState(aTab, aState, aCallback) {
   let listening = true;
@@ -515,16 +518,17 @@ let TestRunner = {
     }
   },
 
   /**
    * Finishes all tests and cleans up.
    */
   finish: function () {
     closeAllButPrimaryWindow();
+    gBrowser.selectedTab = gBrowser.tabs[0];
     waitForBrowserState(this.backupState, finish);
   }
 };
 
 function next() {
   TestRunner.next();
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/data/sessionstore_invalid.js
@@ -0,0 +1,3 @@
+{
+  "windows": // invalid json
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/unit/test_startup_invalid_session.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+  let profd = do_get_profile();
+
+  let sourceSession = do_get_file("data/sessionstore_invalid.js");
+  sourceSession.copyTo(profd, "sessionstore.js");
+
+  let sourceCheckpoints = do_get_file("data/sessionCheckpoints_all.json");
+  sourceCheckpoints.copyTo(profd, "sessionCheckpoints.json");
+
+  do_test_pending();
+  let startup = Cc["@mozilla.org/browser/sessionstartup;1"].
+    getService(Ci.nsISessionStartup);
+
+  afterSessionStartupInitialization(function cb() {
+    do_check_eq(startup.sessionType, Ci.nsISessionStartup.NO_SESSION);
+    do_test_finished();
+  });
+}
--- a/browser/components/sessionstore/test/unit/xpcshell.ini
+++ b/browser/components/sessionstore/test/unit/xpcshell.ini
@@ -1,12 +1,14 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
 support-files =
   data/sessionCheckpoints_all.json
+  data/sessionstore_invalid.js
   data/sessionstore_valid.js
 
 [test_backup.js]
 [test_backup_once.js]
 [test_startup_nosession_async.js]
 [test_startup_session_async.js]
+[test_startup_invalid_session.js]
--- a/browser/components/tabview/test/browser_tabview_bug595601.js
+++ b/browser/components/tabview/test/browser_tabview_bug595601.js
@@ -32,16 +32,17 @@ function test() {
 
   TabsProgressListener.init();
 
   registerCleanupFunction(function () {
     TabsProgressListener.uninit();
 
     Services.prefs.clearUserPref("browser.sessionstore.restore_hidden_tabs");
 
+    gBrowser.selectedTab = gBrowser.tabs[0];
     ss.setBrowserState(stateBackup);
   });
 
   TabView._initFrame(function () {
     executeSoon(testRestoreWithHiddenTabs);
   });
 }
 
--- a/browser/components/tabview/test/browser_tabview_bug608153.js
+++ b/browser/components/tabview/test/browser_tabview_bug608153.js
@@ -4,16 +4,20 @@
 function test() {
   waitForExplicitFinish();
 
   let pinnedTab = gBrowser.addTab();
   gBrowser.pinTab(pinnedTab);
 
   registerCleanupFunction(function() {
     gBrowser.unpinTab(pinnedTab);
+
+    // Don't remove the initial tab.
+    gBrowser.moveTabTo(gBrowser.tabs[1], 0);
+
     while (gBrowser.tabs[1])
       gBrowser.removeTab(gBrowser.tabs[1]);
     hideTabView();
   });
 
   showTabView(function() {
     let cw = TabView.getContentWindow();
     let groupItemOne = cw.GroupItems.groupItems[0];
--- a/browser/components/tabview/test/browser_tabview_bug608158.js
+++ b/browser/components/tabview/test/browser_tabview_bug608158.js
@@ -1,36 +1,28 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   waitForExplicitFinish();
-
-  window.addEventListener("tabviewshown", onTabViewWindowLoaded, false);
-  TabView.toggle();
+  newWindowWithTabView(onTabViewWindowLoaded);
 }
 
-function onTabViewWindowLoaded() {
-  window.removeEventListener("tabviewshown", onTabViewWindowLoaded, false);
-
-  let contentWindow = document.getElementById("tab-view").contentWindow;
+function onTabViewWindowLoaded(win) {
+  let contentWindow = win.TabView.getContentWindow();
 
   is(contentWindow.GroupItems.groupItems.length, 1, 
      "There is one group item on startup");
-  is(gBrowser.tabs.length, 1, "There is one tab on startup");
+  is(win.gBrowser.tabs.length, 1, "There is one tab on startup");
   let groupItem = contentWindow.GroupItems.groupItems[0];
 
   hideGroupItem(groupItem, function () {
-    let onTabViewHidden = function() {
-      window.removeEventListener("tabviewhidden", onTabViewHidden, false);
+    hideTabView(() => {
       is(contentWindow.GroupItems.groupItems.length, 1, 
          "There is still one group item");
       isnot(groupItem, contentWindow.GroupItems.groupItems[0], 
             "The initial group item is not the same as the final group item");
-      is(gBrowser.tabs.length, 1, "There is only one tab");
-      ok(!TabView.isVisible(), "Tab View is hidden");
-      finish();
-    };
-    window.addEventListener("tabviewhidden", onTabViewHidden, false);
-
-    TabView.hide();
+      is(win.gBrowser.tabs.length, 1, "There is only one tab");
+      ok(!win.TabView.isVisible(), "Tab View is hidden");
+      promiseWindowClosed(win).then(finish);
+    }, win);
   });
 }
--- a/browser/components/tabview/test/browser_tabview_bug608405.js
+++ b/browser/components/tabview/test/browser_tabview_bug608405.js
@@ -35,17 +35,17 @@ function test() {
       targetGroup.add(tabItem);
 
       // check state after adding tabItem to targetGroup
       is(tabItem.parent, targetGroup, 'tabItem changed groups');
       is(cw.GroupItems.groupItems.length, 1, 'source group was closed automatically');
       is(targetGroup.getChildren().length, 2, 'target group has now two children');
 
       // cleanup and finish
-      tabItem.close();
+      targetGroup.getChild(0).close();
       hideTabView(finishTest);
     });
   }
 
   waitForExplicitFinish();
 
   showTabView(function () {
     cw = TabView.getContentWindow();
--- a/browser/components/tabview/test/browser_tabview_bug613541.js
+++ b/browser/components/tabview/test/browser_tabview_bug613541.js
@@ -1,26 +1,27 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   let cw;
+  let win;
   let currentTest;
 
   let getGroupItem = function (index) {
     return cw.GroupItems.groupItems[index];
   }
 
   let createGroupItem = function (numTabs) {
     let bounds = new cw.Rect(20, 20, 200, 200);
     let groupItem = new cw.GroupItem([], {bounds: bounds, immediately: true});
     cw.UI.setActive(groupItem);
 
     for (let i=0; i<numTabs || 0; i++)
-      gBrowser.loadOneTab('about:blank', {inBackground: true});
+      win.gBrowser.loadOneTab('about:blank', {inBackground: true});
 
     return groupItem;
   }
 
   let tests = [];
 
   let next = function () {
     let test = tests.shift();
@@ -30,31 +31,31 @@ function test() {
       if (currentTest) {
         currentTest += ' (post-check)';
         assertTabViewIsHidden();
         assertNumberOfGroupItems(1);
         assertNumberOfTabs(1);
       }
 
       currentTest = test.name;
-      showTabView(test.func);
+      showTabView(test.func, win);
     } else
-      hideTabView(finish);
+      promiseWindowClosed(win).then(finish);
   }
 
   let assertTabViewIsHidden = function () {
-    ok(!TabView.isVisible(), currentTest + ': tabview is hidden');
+    ok(!win.TabView.isVisible(), currentTest + ': tabview is hidden');
   }
 
   let assertNumberOfGroupItems = function (num) {
     is(cw.GroupItems.groupItems.length, num, currentTest + ': number of groupItems is equal to ' + num);
   }
 
   let assertNumberOfTabs = function (num) {
-    is(gBrowser.tabs.length, num, currentTest + ': number of tabs is equal to ' + num);
+    is(win.gBrowser.tabs.length, num, currentTest + ': number of tabs is equal to ' + num);
   }
 
   let assertGroupItemRemoved = function (groupItem) {
     is(cw.GroupItems.groupItems.indexOf(groupItem), -1, currentTest + ': groupItem was removed');
   }
 
   let assertGroupItemExists = function (groupItem) {
     isnot(cw.GroupItems.groupItems.indexOf(groupItem), -1, currentTest + ': groupItem still exists');
@@ -63,46 +64,46 @@ function test() {
   // setup: 1 non-empty group
   // action: close the group
   // expected: new group with blank tab is created and zoomed into
   let testSingleGroup1 = function () {
     let groupItem = getGroupItem(0);
     closeGroupItem(groupItem, function () {
       assertNumberOfGroupItems(1);
       assertGroupItemRemoved(groupItem);
-      whenTabViewIsHidden(next);
+      whenTabViewIsHidden(next, win);
     });
   }
 
   // setup: 1 non-empty group
   // action: hide the group, exit panorama
   // expected: new group with blank tab is created and zoomed into
   let testSingleGroup2 = function () {
     let groupItem = getGroupItem(0);
     hideGroupItem(groupItem, function () {
       hideTabView(function () {
         assertNumberOfGroupItems(1);
         assertGroupItemRemoved(groupItem);
         next();
-      });
+      }, win);
     });
   }
 
   // setup: 2 non-empty groups
   // action: close one group
   // expected: nothing should happen
   let testNonEmptyGroup1 = function () {
     let groupItem = getGroupItem(0);
     let newGroupItem = createGroupItem(1);
     assertNumberOfGroupItems(2);
 
     closeGroupItem(groupItem, function () {
       assertNumberOfGroupItems(1);
       assertGroupItemExists(newGroupItem);
-      hideTabView(next);
+      hideTabView(next, win);
     });
   }
 
   // setup: 2 non-empty groups
   // action: hide one group, exit panorama
   // expected: nothing should happen
   let testNonEmptyGroup2 = function () {
     let groupItem = getGroupItem(0);
@@ -110,102 +111,102 @@ function test() {
     assertNumberOfGroupItems(2);
 
     hideGroupItem(groupItem, function () {
       hideTabView(function () {
         assertNumberOfGroupItems(1);
         assertGroupItemRemoved(groupItem);
         assertGroupItemExists(newGroupItem);
         next();
-      });
+      }, win);
     });
   }
 
   // setup: 1 pinned tab, 1 empty group
   // action: exit panorama
   // expected: nothing should happen
   let testPinnedTab1 = function () {
-    gBrowser.pinTab(gBrowser.selectedTab);
+    win.gBrowser.pinTab(win.gBrowser.selectedTab);
 
     let groupItem = getGroupItem(0);
     hideTabView(function () {
       assertNumberOfGroupItems(1);
       assertGroupItemExists(groupItem);
-      gBrowser.unpinTab(gBrowser.selectedTab);
+      win.gBrowser.unpinTab(win.gBrowser.selectedTab);
       next();
-    });
+    }, win);
   }
 
   // setup: 1 pinned tab
   // action: exit panorama
   // expected: new blank group is created
   let testPinnedTab2 = function () {
-    gBrowser.pinTab(gBrowser.selectedTab);
+    win.gBrowser.pinTab(win.gBrowser.selectedTab);
     getGroupItem(0).close();
 
     hideTabView(function () {
       assertNumberOfTabs(1);
       assertNumberOfGroupItems(1);
-      gBrowser.unpinTab(gBrowser.selectedTab);
+      win.gBrowser.unpinTab(win.gBrowser.selectedTab);
       next();
-    });
+    }, win);
   }
 
   // setup: 1 pinned tab, 1 empty group, 1 non-empty group
   // action: close non-empty group
   // expected: nothing should happen
   let testPinnedTab3 = function () {
-    gBrowser.pinTab(gBrowser.selectedTab);
+    win.gBrowser.pinTab(win.gBrowser.selectedTab);
 
     let groupItem = getGroupItem(0);
     let newGroupItem = createGroupItem(1);
     assertNumberOfGroupItems(2);
 
     closeGroupItem(newGroupItem, function () {
       assertNumberOfGroupItems(1);
       assertGroupItemExists(groupItem);
 
-      gBrowser.unpinTab(gBrowser.selectedTab);
-      hideTabView(next);
+      win.gBrowser.unpinTab(win.gBrowser.selectedTab);
+      hideTabView(next, win);
     });
   }
 
   // setup: 1 pinned tab, 1 empty group, 1 non-empty group
   // action: hide non-empty group, exit panorama
   // expected: nothing should happen
   let testPinnedTab4 = function () {
-    gBrowser.pinTab(gBrowser.selectedTab);
+    win.gBrowser.pinTab(win.gBrowser.selectedTab);
 
     let groupItem = getGroupItem(0);
     let newGroupItem = createGroupItem(1);
     assertNumberOfGroupItems(2);
 
     hideGroupItem(newGroupItem, function () {
       hideTabView(function () {
         assertNumberOfGroupItems(1);
         assertGroupItemExists(groupItem);
         assertGroupItemRemoved(newGroupItem);
-        gBrowser.unpinTab(gBrowser.selectedTab);
+        win.gBrowser.unpinTab(win.gBrowser.selectedTab);
         next();
-      });
+      }, win);
     });
   }
 
   // setup: 1 non-empty group, 1 empty group
   // action: close non-empty group
   // expected: empty group is re-used, new tab is created and zoomed into
   let testEmptyGroup1 = function () {
     let groupItem = getGroupItem(0);
     let newGroupItem = createGroupItem(0);
     assertNumberOfGroupItems(2);
 
     closeGroupItem(groupItem, function () {
       assertNumberOfGroupItems(1);
       assertGroupItemExists(newGroupItem);
-      whenTabViewIsHidden(next);
+      whenTabViewIsHidden(next, win);
     });
   }
 
   // setup: 1 non-empty group, 1 empty group
   // action: hide non-empty group, exit panorama
   // expected: empty group is re-used, new tab is created and zoomed into
   let testEmptyGroup2 = function () {
     let groupItem = getGroupItem(0);
@@ -213,34 +214,34 @@ function test() {
     assertNumberOfGroupItems(2);
 
     hideGroupItem(groupItem, function () {
       hideTabView(function () {
         assertNumberOfGroupItems(1);
         assertGroupItemRemoved(groupItem);
         assertGroupItemExists(newGroupItem);
         next();
-      });
+      }, win);
     });
   }
 
   // setup: 1 hidden group, 1 non-empty group
   // action: close non-empty group
   // expected: nothing should happen
   let testHiddenGroup1 = function () {
     let groupItem = getGroupItem(0);
     let hiddenGroupItem = createGroupItem(1);
     assertNumberOfGroupItems(2);
 
     hideGroupItem(hiddenGroupItem, function () {
       closeGroupItem(groupItem, function () {
         assertNumberOfGroupItems(1);
         assertGroupItemRemoved(groupItem);
         assertGroupItemExists(hiddenGroupItem);
-        hideTabView(next);
+        hideTabView(next, win);
       });
     });
   }
 
   // setup: 1 hidden group, 1 non-empty group
   // action: hide non-empty group, exit panorama
   // expected: new group with blank tab is created and zoomed into
   let testHiddenGroup2 = function () {
@@ -250,17 +251,17 @@ function test() {
 
     hideGroupItem(hiddenGroupItem, function () {
       hideGroupItem(groupItem, function () {
         hideTabView(function () {
           assertNumberOfGroupItems(1);
           assertGroupItemRemoved(groupItem);
           assertGroupItemRemoved(hiddenGroupItem);
           next();
-        });
+        }, win);
       });
     });
   }
 
   tests.push({name: 'testSingleGroup1', func: testSingleGroup1});
   tests.push({name: 'testSingleGroup2', func: testSingleGroup2});
 
   tests.push({name: 'testNonEmptyGroup1', func: testNonEmptyGroup1});
@@ -274,13 +275,14 @@ function test() {
   tests.push({name: 'testEmptyGroup1', func: testEmptyGroup1});
   tests.push({name: 'testEmptyGroup2', func: testEmptyGroup2});
 
   tests.push({name: 'testHiddenGroup1', func: testHiddenGroup1});
   tests.push({name: 'testHiddenGroup2', func: testHiddenGroup2}),
 
   waitForExplicitFinish();
 
-  showTabView(function () {
-    cw = TabView.getContentWindow();
+  newWindowWithTabView(window => {
+    win = window;
+    cw = win.TabView.getContentWindow();
     next();
   });
 }
--- a/browser/components/tabview/test/browser_tabview_bug624847.js
+++ b/browser/components/tabview/test/browser_tabview_bug624847.js
@@ -1,113 +1,109 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   let cw;
+  let win;
   let groupItemId;
   let prefix = 'start';
 
   let assertTabViewIsHidden = function () {
-    ok(!TabView.isVisible(), prefix + ': tabview is hidden');
+    ok(!win.TabView.isVisible(), prefix + ': tabview is hidden');
   }
 
   let assertNumberOfGroups = function (num) {
     is(cw.GroupItems.groupItems.length, num, prefix + ': there should be ' + num + ' groups');
   }
 
   let assertNumberOfTabs = function (num) {
-    is(gBrowser.visibleTabs.length, num, prefix + ': there should be ' + num + ' tabs');
+    is(win.gBrowser.visibleTabs.length, num, prefix + ': there should be ' + num + ' tabs');
   }
 
   let assertNumberOfPinnedTabs = function (num) {
-    is(gBrowser._numPinnedTabs, num, prefix + ': there should be ' + num + ' pinned tabs');
+    is(win.gBrowser._numPinnedTabs, num, prefix + ': there should be ' + num + ' pinned tabs');
   }
 
   let assertGroupItemPreserved = function () {
     is(cw.GroupItems.groupItems[0].id, groupItemId, prefix + ': groupItem was preserved');
   }
 
   let assertValidPrerequisites = function () {
     assertNumberOfTabs(1);
     assertNumberOfGroups(1);
     assertNumberOfPinnedTabs(0);
     assertTabViewIsHidden();
   }
 
   let createTab = function (url) {
-    return gBrowser.loadOneTab(url || 'http://mochi.test:8888/', {inBackground: true});
+    return win.gBrowser.loadOneTab(url || 'http://mochi.test:8888/', {inBackground: true});
   }
 
   let createBlankTab = function () {
     return createTab('about:blank');
   }
 
   let finishTest = function () {
     prefix = 'finish';
     assertValidPrerequisites();
-    finish();
+    promiseWindowClosed(win).then(finish);
   }
 
   let testUndoCloseWithSelectedBlankTab = function () {
     prefix = 'unpinned';
     let tab = createTab();
     assertNumberOfTabs(2);
 
     afterAllTabsLoaded(function () {
-      gBrowser.removeTab(tab);
+      win.gBrowser.removeTab(tab);
       assertNumberOfTabs(1);
       assertNumberOfPinnedTabs(0);
 
       restoreTab(function () {
         prefix = 'unpinned-restored';
         assertValidPrerequisites();
         assertGroupItemPreserved();
 
         createBlankTab();
-        afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab);
-      });
-    });
+        afterAllTabsLoaded(testUndoCloseWithSelectedBlankPinnedTab, win);
+      }, 0, win);
+    }, win);
   }
 
   let testUndoCloseWithSelectedBlankPinnedTab = function () {
     prefix = 'pinned';
     assertNumberOfTabs(2);
 
     afterAllTabsLoaded(function () {
-      gBrowser.removeTab(gBrowser.tabs[0]);
-      gBrowser.pinTab(gBrowser.tabs[0]);
-
-      registerCleanupFunction(function () {
-        let tab = gBrowser.tabs[0];
-        if (tab.pinned)
-          gBrowser.unpinTab(tab);
-      });
+      win.gBrowser.removeTab(win.gBrowser.tabs[0]);
+      win.gBrowser.pinTab(win.gBrowser.tabs[0]);
 
       assertNumberOfTabs(1);
       assertNumberOfPinnedTabs(1);
 
       restoreTab(function () {
         prefix = 'pinned-restored';
         assertValidPrerequisites();
         assertGroupItemPreserved();
 
         createBlankTab();
-        gBrowser.removeTab(gBrowser.tabs[0]);
+        win.gBrowser.removeTab(win.gBrowser.tabs[0]);
 
-        afterAllTabsLoaded(finishTest);
-      });
-    });
+        afterAllTabsLoaded(finishTest, win);
+      }, 0, win);
+    }, win);
   }
 
   waitForExplicitFinish();
-  registerCleanupFunction(function () TabView.hide());
 
-  showTabView(function () {
+  newWindowWithTabView(window => {
+    win = window;
+
     hideTabView(function () {
-      cw = TabView.getContentWindow();
+      cw = win.TabView.getContentWindow();
       groupItemId = cw.GroupItems.groupItems[0].id;
 
       assertValidPrerequisites();
       testUndoCloseWithSelectedBlankTab();
-    });
+    }, win);
   });
 }
--- a/browser/components/tabview/test/browser_tabview_bug626455.js
+++ b/browser/components/tabview/test/browser_tabview_bug626455.js
@@ -13,87 +13,87 @@ const TEST_URL = 'data:text/html,<script
                  'function(e){e.returnValue="?"}</script>';
 
 let contentWindow;
 let activeGroup;
 
 function test() {
   waitForExplicitFinish();
 
-  showTabView(function () {
-    contentWindow = TabView.getContentWindow();
+  newWindowWithTabView(win => {
+    contentWindow = win.TabView.getContentWindow();
     activeGroup = contentWindow.GroupItems.getActiveGroupItem();
 
-    gBrowser.browsers[0].loadURI("data:text/html,<p>test for bug 626455, tab1");
+    win.gBrowser.browsers[0].loadURI("data:text/html,<p>test for bug 626455, tab1");
 
-    let tab = gBrowser.addTab(TEST_URL);
-    afterAllTabsLoaded(() => testStayOnPage(tab));
+    let tab = win.gBrowser.addTab(TEST_URL);
+    afterAllTabsLoaded(() => testStayOnPage(win, tab));
   });
 }
 
-function testStayOnPage(blockingTab) {
+function testStayOnPage(win, blockingTab) {
   let browser = blockingTab.linkedBrowser;
   waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
     // stay on page
     btnStay.click();
 
     executeSoon(function () {
       showTabView(function () {
-        is(gBrowser.tabs.length, 1,
+        is(win.gBrowser.tabs.length, 1,
            "The total number of tab is 1 when staying on the page");
 
         // The other initial tab has been closed when trying to close the tab
         // group. The only tab left is the one with the onbeforeunload dialog.
-        let url = gBrowser.browsers[0].currentURI.spec;
+        let url = win.gBrowser.browsers[0].currentURI.spec;
         ok(url.contains("onbeforeunload"), "The open tab is the expected one");
 
         is(contentWindow.GroupItems.getActiveGroupItem(), activeGroup,
            "Active group is still the same");
 
         is(contentWindow.GroupItems.groupItems.length, 1,
            "Only one group is open");
 
         // start the next test
-        testLeavePage(gBrowser.tabs[0]);
-      });
+        testLeavePage(win, win.gBrowser.tabs[0]);
+      }, win);
     });
   });
 
   closeGroupItem(activeGroup);
 }
 
-function testLeavePage(blockingTab) {
+function testLeavePage(win, blockingTab) {
   let browser = blockingTab.linkedBrowser;
   waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
     // Leave page
     btnLeave.click();
   });
 
-  whenGroupClosed(activeGroup, finishTest);
+  whenGroupClosed(activeGroup, () => finishTest(win));
   closeGroupItem(activeGroup);
 }
 
-function finishTest() {
-  is(gBrowser.tabs.length, 1,
+function finishTest(win) {
+  is(win.gBrowser.tabs.length, 1,
      "The total number of tab is 1 after leaving the page");
   is(contentWindow.TabItems.getItems().length, 1,
      "The total number of tab items is 1 after leaving the page");
 
-  let location = gBrowser.browsers[0].currentURI.spec;
+  let location = win.gBrowser.browsers[0].currentURI.spec;
   is(location, BROWSER_NEW_TAB_URL, "The open tab is the expected one");
 
   isnot(contentWindow.GroupItems.getActiveGroupItem(), activeGroup,
      "Active group is no longer the same");
 
   is(contentWindow.GroupItems.groupItems.length, 1,
      "Only one group is open");
 
   contentWindow = null;
   activeGroup = null;
-  finish();
+  promiseWindowClosed(win).then(finish);
 }
 
 // ----------
 function whenGroupClosed(group, callback) {
   group.addSubscriber("close", function onClose() {
     group.removeSubscriber("close", onClose);
     callback();
   });
--- a/browser/components/tabview/test/browser_tabview_bug633788.js
+++ b/browser/components/tabview/test/browser_tabview_bug633788.js
@@ -1,31 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   waitForExplicitFinish();
 
-  showTabView(function () {
-    is(gBrowser.tabs.length, 1, "There is only one tab");
+  newWindowWithTabView(win => {
+    is(win.gBrowser.tabs.length, 1, "There is only one tab");
 
-    let tab = gBrowser.tabs[0];
+    let tab = win.gBrowser.tabs[0];
     let tabItem = tab._tabViewTabItem;
     ok(tabItem.parent, "The tab item belongs to a group");
     let groupId = tabItem.parent.id;
 
     tab._tabViewTabItem.close();
 
     whenTabViewIsHidden(function() {
       // a new tab with group should be opened
-      is(gBrowser.tabs.length, 1, "There is still one tab");
-      isnot(gBrowser.selectedTab, tab, "The tab is different");
+      is(win.gBrowser.tabs.length, 1, "There is still one tab");
+      isnot(win.gBrowser.selectedTab, tab, "The tab is different");
 
-      tab = gBrowser.tabs[0];
+      tab = win.gBrowser.tabs[0];
       tabItem = tab._tabViewTabItem;
       ok(tabItem.parent, "This new tab item belongs to a group");
 
       is(tabItem.parent.id, groupId, "The group is different");
 
-      finish();
-    });
+      promiseWindowClosed(win).then(finish);
+    }, win);
   });
 }
--- a/browser/components/tabview/test/browser_tabview_bug685692.js
+++ b/browser/components/tabview/test/browser_tabview_bug685692.js
@@ -1,54 +1,48 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function test() {
   waitForExplicitFinish();
 
-  gBrowser.addTab("http://example.com/");
-  gBrowser.addTab("http://example.com/");
+  newWindowWithTabView(win => {
+    win.gBrowser.addTab("http://example.com/");
+    win.gBrowser.addTab("http://example.com/");
 
-  registerCleanupFunction(function () {
-    while (gBrowser.tabs.length > 1)
-      gBrowser.removeTab(gBrowser.tabs[1]);
-    hideTabView();
-  })
-
-  afterAllTabsLoaded(function() {
-    showTabView(function() {
-      let cw = TabView.getContentWindow();
+    afterAllTabsLoaded(function() {
+      let cw = win.TabView.getContentWindow();
 
       let groupItemOne = cw.GroupItems.groupItems[0];
       is(groupItemOne.getChildren().length, 3, "The number of tabs in group one is 3");
- 
+
       // create a group with a blank tab
-      let groupItemTwo = createGroupItemWithBlankTabs(window, 400, 400, 40, 1);
+      let groupItemTwo = createGroupItemWithBlankTabs(win, 400, 400, 40, 1);
       is(groupItemTwo.getChildren().length, 1, "The number of tabs in group two is 1");
 
       cw.UI.setActive(groupItemOne);
 
-      moveTabToAnotherGroup(groupItemOne.getChild(2).tab, groupItemOne, groupItemTwo, function() {
-        moveTabToAnotherGroup(groupItemOne.getChild(1).tab, groupItemOne, groupItemTwo, function() {
+      moveTabToAnotherGroup(win, groupItemOne.getChild(2).tab, groupItemOne, groupItemTwo, function() {
+        moveTabToAnotherGroup(win, groupItemOne.getChild(1).tab, groupItemOne, groupItemTwo, function() {
           groupItemOne.close();
-          hideTabView(finish);
+          promiseWindowClosed(win).then(finish);
         });
       });
     });
   });
 }
 
-function moveTabToAnotherGroup(targetTab, groupItemOne, groupItemTwo, callback) {
+function moveTabToAnotherGroup(win, targetTab, groupItemOne, groupItemTwo, callback) {
   hideTabView(function() {
     let tabCountInGroupItemOne = groupItemOne.getChildren().length;
     let tabCountInGroupItemTwo = groupItemTwo.getChildren().length;
 
-    TabView.moveTabTo(targetTab, groupItemTwo.id);
+    win.TabView.moveTabTo(targetTab, groupItemTwo.id);
 
     showTabView(function() {
       is(groupItemOne.getChildren().length, --tabCountInGroupItemOne, "The number of tab items in group one is decreased");
       is(groupItemTwo.getChildren().length, ++tabCountInGroupItemTwo, "The number of tab items in group two is increased");
       is(groupItemTwo.getChild(tabCountInGroupItemTwo-1).tab, targetTab, "The last tab is the moved tab");
 
       callback();
-    });
-  });
+    }, win);
+  }, win);
 }
--- a/browser/devtools/debugger/debugger-toolbar.js
+++ b/browser/devtools/debugger/debugger-toolbar.js
@@ -504,22 +504,23 @@ StackFramesView.prototype = Heritage.ext
   /**
    * The select listener for the stackframes container.
    */
   _onSelect: function(e) {
     let stackframeItem = this.selectedItem;
     if (stackframeItem) {
       // The container is not empty and an actual item was selected.
       let depth = stackframeItem.attachment.depth;
-      DebuggerController.StackFrames.selectFrame(depth);
 
       // Mirror the selected item in the classic list.
       this.suppressSelectionEvents = true;
       this._mirror.selectedItem = e => e.attachment.depth == depth;
       this.suppressSelectionEvents = false;
+
+      DebuggerController.StackFrames.selectFrame(depth);
     }
   },
 
   /**
    * The scroll listener for the stackframes container.
    */
   _onScroll: function() {
     // Update the stackframes container only if we have to.
--- a/browser/devtools/debugger/test/head.js
+++ b/browser/devtools/debugger/test/head.js
@@ -744,20 +744,21 @@ function openVarPopup(aPanel, aCoords, a
   let editor = aPanel.panelWin.DebuggerView.editor;
   let bubble = aPanel.panelWin.DebuggerView.VariableBubble;
   let tooltip = bubble._tooltip.panel;
 
   let popupShown = once(tooltip, "popupshown");
   let fetchedProperties = aWaitForFetchedProperties
     ? waitForDebuggerEvents(aPanel, events.FETCHED_BUBBLE_PROPERTIES)
     : promise.resolve(null);
+  let updatedFrame = waitForDebuggerEvents(aPanel, events.FETCHED_SCOPES);
 
   let { left, top } = editor.getCoordsFromPosition(aCoords);
   bubble._findIdentifier(left, top);
-  return promise.all([popupShown, fetchedProperties]).then(waitForTick);
+  return promise.all([popupShown, fetchedProperties, updatedFrame]).then(waitForTick);
 }
 
 // Simulates the mouse hovering a variable in the debugger
 // Takes in account the position of the cursor in the text, if the text is
 // selected and if a button is currently pushed (aButtonPushed > 0).
 // The function returns a promise which returns true if the popup opened or
 // false if it didn't
 function intendOpenVarPopup(aPanel, aPosition, aButtonPushed) {
--- a/browser/experiments/Experiments.jsm
+++ b/browser/experiments/Experiments.jsm
@@ -1697,23 +1697,27 @@ Experiments.ExperimentEntry.prototype = 
    */
   stop: Task.async(function* (terminationKind, terminationReason) {
     this._log.trace("stop() - id=" + this.id + ", terminationKind=" + terminationKind);
     if (!this._enabled) {
       throw new Error("Must not call stop() on an inactive experiment.");
     }
 
     this._enabled = false;
-
-    let changes = yield this.reconcileAddonState();
     let now = this._policy.now();
     this._lastChangedDate = now;
     this._endDate = now;
+
+    let changes = yield this.reconcileAddonState();
     this._logTermination(terminationKind, terminationReason);
 
+    if (terminationKind == TELEMETRY_LOG.TERMINATION.ADDON_UNINSTALLED) {
+      changes |= this.ADDON_CHANGE_UNINSTALL;
+    }
+
     return changes;
   }),
 
   /**
    * Reconcile the state of the add-on against what it's supposed to be.
    *
    * If we are active, ensure the add-on is enabled and up to date.
    *
--- a/browser/experiments/test/xpcshell/head.js
+++ b/browser/experiments/test/xpcshell/head.js
@@ -6,16 +6,17 @@ const {classes: Cc, interfaces: Ci, util
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://services-sync/healthreport.jsm", this);
 Cu.import("resource://testing-common/services/healthreport/utils.jsm", this);
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+Cu.import("resource://testing-common/AddonManagerTesting.jsm");
 
 const PREF_EXPERIMENTS_ENABLED  = "experiments.enabled";
 const PREF_LOGGING_LEVEL        = "experiments.logging.level";
 const PREF_LOGGING_DUMP         = "experiments.logging.dump";
 const PREF_MANIFEST_URI         = "experiments.manifest.uri";
 const PREF_FETCHINTERVAL        = "experiments.manifest.fetchIntervalSeconds";
 const PREF_TELEMETRY_ENABLED    = "toolkit.telemetry.enabled";
 const PREF_HEALTHREPORT_ENABLED = "datareporting.healthreport.service.enabled";
--- a/browser/experiments/test/xpcshell/test_api.js
+++ b/browser/experiments/test/xpcshell/test_api.js
@@ -20,16 +20,34 @@ let gHttpServer          = null;
 let gHttpRoot            = null;
 let gDataRoot            = null;
 let gReporter            = null;
 let gPolicy              = null;
 let gManifestObject      = null;
 let gManifestHandlerURI  = null;
 let gTimerScheduleOffset = -1;
 
+function uninstallExperimentAddons() {
+  return Task.spawn(function* () {
+    let addons = yield getExperimentAddons();
+    for (let a of addons) {
+      yield AddonTestUtils.uninstallAddonByID(a.id);
+    }
+  });
+}
+
+function testCleanup(experimentsInstance) {
+  return Task.spawn(function* () {
+    yield experimentsInstance.uninit();
+    yield removeCacheFile();
+    yield uninstallExperimentAddons();
+    restartManager();
+  });
+}
+
 function run_test() {
   run_next_test();
 }
 
 add_task(function* test_setup() {
   loadAddonManager();
   gProfileDir = do_get_profile();
 
@@ -251,18 +269,17 @@ add_task(function* test_getExperiments()
       Assert.equal(entry[k], list[i][k],
                    "Entry " + i + " - Property '" + k + "' should match reference data.");
     }
   }
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that we handle the experiments addon already being
 // installed properly.
 // We should just pave over them.
 
 add_task(function* test_addonAlreadyInstalled() {
   const OBSERVER_TOPIC = "experiments-changed";
@@ -332,18 +349,17 @@ add_task(function* test_addonAlreadyInst
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should still have 1 entry.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, true, "Experiment 1 should be active.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 add_task(function* test_lastActiveToday() {
   let experiments = new Experiments.Experiments(gPolicy);
 
   replaceExperiments(experiments, FAKE_EXPERIMENTS_1);
 
   let e = yield experiments.getExperiments();
@@ -359,18 +375,17 @@ add_task(function* test_lastActiveToday(
   Assert.equal(e.length, 2, "Monkeypatch successful.");
 
   defineNow(gPolicy, e[0].endDate);
 
   lastActive = yield experiments.lastActiveToday();
   Assert.ok(lastActive, "Have a last active experiment");
   Assert.equal(lastActive, e[0], "Last active object is expected.");
 
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test explicitly disabling experiments.
 
 add_task(function* test_disableExperiment() {
   // Dates this test is based on.
 
   let startDate = new Date(2004, 10, 9, 12);
@@ -445,20 +460,17 @@ add_task(function* test_disableExperimen
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry.");
 
   for (let k of Object.keys(experimentInfo)) {
     Assert.equal(experimentInfo[k], list[0][k],
                  "Property " + k + " should match reference data.");
   }
 
-  // Cleanup.
-
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 add_task(function* test_disableExperimentsFeature() {
   // Dates this test is based on.
 
   let startDate = new Date(2004, 10, 9, 12);
   let endDate   = futureDate(startDate, 100 * MS_IN_ONE_DAY);
 
@@ -536,20 +548,17 @@ add_task(function* test_disableExperimen
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry.");
 
   for (let k of Object.keys(experimentInfo)) {
     Assert.equal(experimentInfo[k], list[0][k],
                  "Property " + k + " should match reference data.");
   }
 
-  // Cleanup.
-
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that after a failed experiment install:
 // * the next applicable experiment gets installed
 // * changing the experiments data later triggers re-evaluation
 
 add_task(function* test_installFailure() {
   const OBSERVER_TOPIC = "experiments-changed";
@@ -671,21 +680,17 @@ add_task(function* test_installFailure()
   for (let i=0; i<experimentListData.length; ++i) {
     let entry = experimentListData[i];
     for (let k of Object.keys(entry)) {
       Assert.equal(entry[k], list[i][k],
                    "Entry " + i + " - Property '" + k + "' should match reference data.");
     }
   }
 
-  // Cleanup.
-
-  Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that after an experiment was disabled by user action,
 // the experiment is not activated again if manifest data changes.
 
 add_task(function* test_userDisabledAndUpdated() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -774,18 +779,17 @@ add_task(function* test_userDisabledAndU
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, false, "Experiment should still be inactive.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that changing the hash for an active experiments triggers an
 // update for it.
 
 add_task(function* test_updateActiveExperiment() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -866,18 +870,17 @@ add_task(function* test_updateActiveExpe
   Assert.equal(list[0].active, true, "Experiment 1 should still be active.");
   Assert.equal(list[0].name, EXPERIMENT1A_NAME, "Experiments name should have been updated.");
   todayActive = yield experiments.lastActiveToday();
   Assert.equal(todayActive.id, list[0].id, "last active today is still sane.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Tests that setting the disable flag for an active experiment
 // stops it.
 
 add_task(function* test_disableActiveExperiment() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -958,18 +961,17 @@ add_task(function* test_disableActiveExp
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, false, "Experiment 1 should still be disabled.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that:
 // * setting the frozen flag for a not-yet-started experiment keeps
 //   it from starting
 // * after a removing the frozen flag, the experiment can still start
 
 add_task(function* test_freezePendingExperiment() {
@@ -1039,18 +1041,17 @@ add_task(function* test_freezePendingExp
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, true, "Experiment 1 should be active now.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that setting the frozen flag for an active experiment doesn't
 // stop it.
 
 add_task(function* test_freezeActiveExperiment() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -1120,18 +1121,17 @@ add_task(function* test_freezeActiveExpe
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, true, "Experiment 1 should still be active.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that removing an active experiment from the manifest doesn't
 // stop it.
 
 add_task(function* test_removeActiveExperiment() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -1213,18 +1213,17 @@ add_task(function* test_removeActiveExpe
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, true, "Experiment 1 should still be active.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that we correctly handle experiment start & install failures.
 
 add_task(function* test_invalidUrl() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
   let expectedObserverFireCount = 0;
@@ -1269,18 +1268,17 @@ add_task(function* test_invalidUrl() {
   Assert.equal(gTimerScheduleOffset, null, "No new timer should have been scheduled.");
 
   let list = yield experiments.getExperiments();
   Assert.equal(list.length, 0, "Experiment list should be empty.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Test that we handle it properly when active experiment addons are being
 // uninstalled.
 
 add_task(function* test_unexpectedUninstall() {
   const OBSERVER_TOPIC = "experiments-changed";
   let observerFireCount = 0;
@@ -1348,18 +1346,17 @@ add_task(function* test_unexpectedUninst
   list = yield experiments.getExperiments();
   Assert.equal(list.length, 1, "Experiment list should have 1 entry now.");
   Assert.equal(list[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
   Assert.equal(list[0].active, false, "Experiment 1 should not be active anymore.");
 
   // Cleanup.
 
   Services.obs.removeObserver(observer, OBSERVER_TOPIC);
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // If the Addon Manager knows of an experiment that we don't, it should get
 // uninstalled.
 add_task(function* testUnknownExperimentsUninstalled() {
   let experiments = new Experiments.Experiments(gPolicy);
 
   let addons = yield getExperimentAddons();
@@ -1382,18 +1379,17 @@ add_task(function* testUnknownExperiment
   yield experiments.updateManifest();
   let fromManifest = yield experiments.getExperiments();
   Assert.equal(fromManifest.length, 0, "No experiments known in manifest.");
 
   // And the unknown add-on should be gone.
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "Experiment 1 was uninstalled.");
 
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // If someone else installs an experiment add-on, we detect and stop that.
 add_task(function* testForeignExperimentInstall() {
   let experiments = new Experiments.Experiments(gPolicy);
 
   gManifestObject = {
     "version": 1,
@@ -1410,18 +1406,17 @@ add_task(function* testForeignExperiment
     yield AddonTestUtils.installXPIFromURL(gDataRoot + EXPERIMENT1_XPI_NAME, EXPERIMENT1_XPI_SHA1);
   } catch (ex) {
     failed = true;
   }
   Assert.ok(failed, "Add-on install should not have completed successfully");
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 0, "Add-on install should have been cancelled.");
 
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
 
 // Experiment add-ons will be disabled after Addon Manager restarts. Ensure
 // we enable them automatically.
 add_task(function* testEnabledAfterRestart() {
   let experiments = new Experiments.Experiments(gPolicy);
 
   gManifestObject = {
@@ -1436,17 +1431,17 @@ add_task(function* testEnabledAfterResta
         maxActiveSeconds: 10 * SEC_IN_ONE_DAY,
         appName: ["XPCShell"],
         channel: ["nightly"],
       },
     ],
   };
 
   let addons = yield getExperimentAddons();
-  Assert.equal(addons.length, 0, "Precondition: No experimenta add-ons installed.");
+  Assert.equal(addons.length, 0, "Precondition: No experiment add-ons installed.");
 
   yield experiments.updateManifest();
   let fromManifest = yield experiments.getExperiments();
   Assert.equal(fromManifest.length, 1, "A single experiment is known.");
 
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 1, "A single experiment add-on is installed.");
   Assert.ok(addons[0].isActive, "That experiment is active.");
@@ -1459,11 +1454,70 @@ add_task(function* testEnabledAfterResta
   addons = yield getExperimentAddons();
   Assert.equal(addons.length, 1, "The experiment is still there after restart.");
   Assert.ok(addons[0].userDisabled, "But it is disabled.");
   Assert.equal(addons[0].isActive, false, "And not active.");
 
   yield experiments.updateManifest();
   Assert.ok(addons[0].isActive, "It activates when the manifest is evaluated.");
 
-  yield experiments.uninit();
-  yield removeCacheFile();
+  yield testCleanup(experiments);
 });
+
+// Test coverage for an add-on uninstall disabling the experiment and that it stays
+// disabled over restarts.
+add_task(function* test_foreignUninstallAndRestart() {
+  let experiments = new Experiments.Experiments(gPolicy);
+
+  gManifestObject = {
+    "version": 1,
+    experiments: [
+      {
+        id: EXPERIMENT1_ID,
+        xpiURL: gDataRoot + EXPERIMENT1_XPI_NAME,
+        xpiHash: EXPERIMENT1_XPI_SHA1,
+        startTime: gPolicy.now().getTime() / 1000 - 60,
+        endTime: gPolicy.now().getTime() / 1000 + 60,
+        maxActiveSeconds: 10 * SEC_IN_ONE_DAY,
+        appName: ["XPCShell"],
+        channel: ["nightly"],
+      },
+    ],
+  };
+
+  let addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 0, "Precondition: No experiment add-ons installed.");
+
+  yield experiments.updateManifest();
+  let experimentList = yield experiments.getExperiments();
+  Assert.equal(experimentList.length, 1, "A single experiment is known.");
+
+  addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 1, "A single experiment add-on is installed.");
+  Assert.ok(addons[0].isActive, "That experiment is active.");
+
+  yield AddonTestUtils.uninstallAddonByID(EXPERIMENT1_ID);
+  yield experiments._mainTask;
+
+  let addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 0, "Experiment add-on should have been removed.");
+
+  experimentList = yield experiments.getExperiments();
+  Assert.equal(experimentList.length, 1, "A single experiment is known.");
+  Assert.equal(experimentList[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
+  Assert.ok(!experimentList[0].active, "Experiment 1 should not be active anymore.");
+
+  // Fake restart behaviour.
+  experiments.uninit();
+  restartManager();
+  experiments = new Experiments.Experiments(gPolicy);
+  yield experiments.updateManifest();
+
+  let addons = yield getExperimentAddons();
+  Assert.equal(addons.length, 0, "No experiment add-ons installed.");
+
+  experimentList = yield experiments.getExperiments();
+  Assert.equal(experimentList.length, 1, "A single experiment is known.");
+  Assert.equal(experimentList[0].id, EXPERIMENT1_ID, "Experiment 1 should be the sole entry.");
+  Assert.ok(!experimentList[0].active, "Experiment 1 should not be active.");
+
+  yield testCleanup(experiments);
+});
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1260,26 +1260,30 @@ toolbarbutton[sdk-button="true"][cui-are
   list-style-image: url(chrome://browser/skin/pointerLock-64.png);
 }
 
 /* Notification icon box */
 #notification-popup-box {
   position: relative;
   background-color: #fff;
   background-clip: padding-box;
-  padding-left: 4px;
+  padding-left: 3px;
   border-radius: 2.5px 0 0 2.5px;
   border-width: 0 8px 0 0;
   border-style: solid;
   border-image: url("chrome://browser/skin/urlbar-arrow.png") 0 8 0 0 fill;
   -moz-margin-end: -8px;
   margin-top: -1px;
   margin-bottom: -1px;
 }
 
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
+  padding-left: 7px;
+}
+
 #notification-popup-box:not([hidden]) + #identity-box {
   -moz-padding-start: 10px;
   border-radius: 0;
 }
 
 #notification-popup-box:-moz-locale-dir(rtl),
 .notification-anchor-icon:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3379,17 +3379,17 @@ toolbarbutton.chevron > .toolbarbutton-m
 }
 
 @media (min-resolution: 2dppx) {
   #notification-popup-box {
     border-image: url("chrome://browser/skin/urlbar-arrow@2x.png") 0 16 0 0 fill;
   }
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar > #notification-popup-box {
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
   padding-left: 7px;
 }
 
 #notification-popup-box:-moz-locale-dir(rtl),
 .notification-anchor-icon:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
 }
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -2269,18 +2269,18 @@ toolbarbutton.bookmark-item[dragover="tr
   padding-left: 3px;
   border-radius: 2.5px 0 0 2.5px;
   border-width: 0 8px 0 0;
   border-style: solid;
   border-image: url("chrome://browser/skin/urlbar-arrow.png") 0 8 0 0 fill;
   -moz-margin-end: -8px;
 }
 
-@conditionalForwardWithUrlbar@[forwarddisabled] > #urlbar-wrapper > #urlbar > #notification-popup-box {
-  padding-left: 5px;
+@conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box {
+  padding-left: 7px;
 }
 
 #notification-popup-box:-moz-locale-dir(rtl),
 .notification-anchor-icon:-moz-locale-dir(rtl) {
   transform: scaleX(-1);
 }
 
 .notification-anchor-icon {
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -43,16 +43,17 @@ import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.sync.setup.SyncAccounts;
+import org.mozilla.gecko.tabspanel.TabsPanel;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.StringUtils;
@@ -413,16 +414,41 @@ abstract public class BrowserApp extends
                 showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
 
                 final int count = BrowserDB.getReadingListCount(getContentResolver());
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count)));
             }
         });
     }
 
+    private void handleReaderFaviconRequest(final String url) {
+        (new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
+            @Override
+            public String doInBackground(Void... params) {
+                return Favicons.getFaviconURLForPageURL(url);
+            }
+
+            @Override
+            public void onPostExecute(String faviconUrl) {
+                JSONObject args = new JSONObject();
+
+                if (faviconUrl != null) {
+                    try {
+                        args.put("url", url);
+                        args.put("faviconUrl", faviconUrl);
+                    } catch (JSONException e) {
+                        Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
+                    }
+                }
+
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
+            }
+        }).execute();
+    }
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME");
 
         final Intent intent = getIntent();
 
         String args = intent.getStringExtra("args");
         if (args != null && args.contains(GUEST_BROWSING_ARG)) {
@@ -492,16 +518,21 @@ abstract public class BrowserApp extends
         registerEventListener("Settings:Show");
         registerEventListener("Updater:Launch");
         registerEventListener("Menu:Add");
         registerEventListener("Menu:Remove");
         registerEventListener("Menu:Update");
         registerEventListener("Accounts:Create");
         registerEventListener("Accounts:Exist");
         registerEventListener("Prompt:ShowTop");
+        registerEventListener("Reader:ListStatusRequest");
+        registerEventListener("Reader:Added");
+        registerEventListener("Reader:Removed");
+        registerEventListener("Reader:Share");
+        registerEventListener("Reader:FaviconRequest");
 
         Distribution.init(this);
         JavaAddonManager.getInstance().init(getApplicationContext());
         mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
         mBrowserHealthReporter = new BrowserHealthReporter();
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
@@ -838,16 +869,21 @@ abstract public class BrowserApp extends
         unregisterEventListener("Telemetry:Gather");
         unregisterEventListener("Settings:Show");
         unregisterEventListener("Updater:Launch");
         unregisterEventListener("Menu:Add");
         unregisterEventListener("Menu:Remove");
         unregisterEventListener("Menu:Update");
         unregisterEventListener("Accounts:Create");
         unregisterEventListener("Accounts:Exist");
+        unregisterEventListener("Reader:ListStatusRequest");
+        unregisterEventListener("Reader:Added");
+        unregisterEventListener("Reader:Removed");
+        unregisterEventListener("Reader:Share");
+        unregisterEventListener("Reader:FaviconRequest");
 
         if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
             NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
             if (nfc != null) {
                 // null this out even though the docs say it's not needed,
                 // because the source code looks like it will only do this
                 // automatically on API 14+
                 nfc.setNdefPushMessageCallback(null, this);
@@ -1201,16 +1237,19 @@ abstract public class BrowserApp extends
             } else if (event.equals("Reader:Removed")) {
                 final String url = message.getString("url");
                 handleReaderRemoved(url);
             } else if (event.equals("Reader:Share")) {
                 final String title = message.getString("title");
                 final String url = message.getString("url");
                 GeckoAppShell.openUriExternal(url, "text/plain", "", "",
                                               Intent.ACTION_SEND, title);
+            } else if (event.equals("Reader:FaviconRequest")) {
+                final String url = message.getString("url");
+                handleReaderFaviconRequest(url);
             } else if (event.equals("Settings:Show")) {
                 // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830)
                 String resource = null;
                 if (!message.isNull(GeckoPreferences.INTENT_EXTRA_RESOURCES)) {
                     resource = message.getString(GeckoPreferences.INTENT_EXTRA_RESOURCES);
                 }
                 Intent settingsIntent = new Intent(this, GeckoPreferences.class);
                 GeckoPreferences.setResourceToOpen(settingsIntent, resource);
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -491,41 +491,16 @@ public abstract class GeckoApp
         if (mToast != null) {
             mToast.onSaveInstanceState(outState);
         }
 
         outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
         outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
     }
 
-    void handleFaviconRequest(final String url) {
-        (new UiAsyncTask<Void, Void, String>(ThreadUtils.getBackgroundHandler()) {
-            @Override
-            public String doInBackground(Void... params) {
-                return Favicons.getFaviconURLForPageURL(url);
-            }
-
-            @Override
-            public void onPostExecute(String faviconUrl) {
-                JSONObject args = new JSONObject();
-
-                if (faviconUrl != null) {
-                    try {
-                        args.put("url", url);
-                        args.put("faviconUrl", faviconUrl);
-                    } catch (JSONException e) {
-                        Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
-                    }
-                }
-
-                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:FaviconReturn", args.toString()));
-            }
-        }).execute();
-    }
-
     void handleClearHistory() {
         BrowserDB.clearHistory(getContentResolver());
     }
 
     public void addTab() { }
 
     public void addPrivateTab() { }
 
@@ -560,19 +535,16 @@ public abstract class GeckoApp
                 } else {
                     final String duration = message.getString("duration");
                     showNormalToast(msg, duration);
                 }
             } else if (event.equals("log")) {
                 // generic log listener
                 final String msg = message.getString("msg");
                 Log.d(LOGTAG, "Log: " + msg);
-            } else if (event.equals("Reader:FaviconRequest")) {
-                final String url = message.getString("url");
-                handleFaviconRequest(url);
             } else if (event.equals("Gecko:DelayedStartup")) {
                 ThreadUtils.postToBackgroundThread(new UninstallListener.DelayedStartupTask(this));
             } else if (event.equals("Gecko:Ready")) {
                 mGeckoReadyStartupTimer.stop();
                 geckoConnected();
 
                 // This method is already running on the background thread, so we
                 // know that mHealthRecorder will exist. That doesn't stop us being
@@ -1568,21 +1540,16 @@ public abstract class GeckoApp
             startActivity(settingsIntent);
         }
 
         //app state callbacks
         mAppStateListeners = new LinkedList<GeckoAppShell.AppStateListener>();
 
         //register for events
         registerEventListener("log");
-        registerEventListener("Reader:ListStatusRequest");
-        registerEventListener("Reader:Added");
-        registerEventListener("Reader:Removed");
-        registerEventListener("Reader:Share");
-        registerEventListener("Reader:FaviconRequest");
         registerEventListener("onCameraCapture");
         registerEventListener("Gecko:Ready");
         registerEventListener("Gecko:DelayedStartup");
         registerEventListener("Toast:Show");
         registerEventListener("DOMFullScreen:Start");
         registerEventListener("DOMFullScreen:Stop");
         registerEventListener("ToggleChrome:Hide");
         registerEventListener("ToggleChrome:Show");
@@ -2109,21 +2076,16 @@ public abstract class GeckoApp
 
         super.onRestart();
     }
 
     @Override
     public void onDestroy()
     {
         unregisterEventListener("log");
-        unregisterEventListener("Reader:ListStatusRequest");
-        unregisterEventListener("Reader:Added");
-        unregisterEventListener("Reader:Removed");
-        unregisterEventListener("Reader:Share");
-        unregisterEventListener("Reader:FaviconRequest");
         unregisterEventListener("onCameraCapture");
         unregisterEventListener("Gecko:Ready");
         unregisterEventListener("Gecko:DelayedStartup");
         unregisterEventListener("Toast:Show");
         unregisterEventListener("DOMFullScreen:Start");
         unregisterEventListener("DOMFullScreen:Stop");
         unregisterEventListener("ToggleChrome:Hide");
         unregisterEventListener("ToggleChrome:Show");
--- a/mobile/android/base/TelemetryContract.java
+++ b/mobile/android/base/TelemetryContract.java
@@ -24,23 +24,29 @@ public interface TelemetryContract {
         // Top site un-pinned.
         public static final String TOP_SITES_UNPIN = "unpin.1";
 
         // Top site edited.
         public static final String TOP_SITES_EDIT = "edit.1";
 
         // Set default panel.
         public static final String PANEL_SET_DEFAULT = "setdefault.1";
+
+        // Sanitizing private data.
+        public static final String SANITIZE = "sanitize.1";
     }
 
     /**
      * Holds event methods. Intended for use in
      * Telemetry.sendUIEvent() as the "method" parameter.
      */
-    public interface Method {}
+    public interface Method {
+        // Action triggered from a dialog.
+        public static final String DIALOG = "dialog";
+    }
 
     /**
      * Holds session names. Intended for use with
      * Telemetry.startUISession() as the "sessionName" parameter.
      */
     public interface Session {
         // Started when a user enters about:home.
         public static final String HOME = "home.1";
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -335,35 +335,35 @@ gbjar.sources += [
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
     'prompts/PromptService.java',
     'prompts/TabInput.java',
     'ReaderModeUtils.java',
     'ReferrerReceiver.java',
-    'RemoteTabsContainer.java',
-    'RemoteTabsList.java',
     'Restarter.java',
     'ScrollAnimator.java',
     'ServiceNotificationClient.java',
     'SessionParser.java',
     'SharedPreferencesHelper.java',
     'SiteIdentity.java',
     'SmsManager.java',
     'sqlite/ByteBufferInputStream.java',
     'sqlite/MatrixBlobCursor.java',
     'sqlite/SQLiteBridge.java',
     'sqlite/SQLiteBridgeException.java',
     'SurfaceBits.java',
     'Tab.java',
     'Tabs.java',
     'TabsAccessor.java',
-    'TabsPanel.java',
-    'TabsTray.java',
+    'tabspanel/RemoteTabsContainer.java',
+    'tabspanel/RemoteTabsList.java',
+    'tabspanel/TabsPanel.java',
+    'tabspanel/TabsTray.java',
     'Telemetry.java',
     'TelemetryContract.java',
     'TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'toolbar/AutocompleteHandler.java',
     'toolbar/BackButton.java',
     'toolbar/BrowserToolbar.java',
--- a/mobile/android/base/preferences/PrivateDataPreference.java
+++ b/mobile/android/base/preferences/PrivateDataPreference.java
@@ -2,42 +2,43 @@
  * 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.preferences;
 
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.util.AttributeSet;
 import android.util.Log;
 
 class PrivateDataPreference extends MultiChoicePreference {
     private static final String LOGTAG = "GeckoPrivateDataPreference";
     private static final String PREF_KEY_PREFIX = "private.data.";
 
-    private Context mContext;
-
     public PrivateDataPreference(Context context, AttributeSet attrs) {
         super(context, attrs);
-        mContext = context;
     }
 
     @Override
     protected void onDialogClosed(boolean positiveResult) {
         super.onDialogClosed(positiveResult);
 
         if (!positiveResult)
             return;
 
+        Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.DIALOG);
+
         CharSequence keys[] = getEntryKeys();
         boolean values[] = getValues();
         JSONObject json = new JSONObject();
 
         for (int i = 0; i < keys.length; i++) {
             // Privacy pref checkbox values are stored in Android prefs to
             // remember their check states. The key names are private.data.X,
             // where X is a string from Gecko sanitization. This prefix is
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel.xml
@@ -5,76 +5,76 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
     <RelativeLayout android:id="@+id/tabs_panel_header"
                     android:layout_width="fill_parent"
                     android:layout_height="@dimen/browser_toolbar_height">
 
-        <view class="org.mozilla.gecko.TabsPanel$TabsPanelToolbar"
+        <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsPanelToolbar"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:background="@color/background_tabs">
 
             <include layout="@layout/tabs_panel_header"/>
 
         </view>
 
         <View android:layout_width="fill_parent"
               android:layout_height="2dp"
               android:layout_alignParentBottom="true"
               android:background="#1A000000"/>
 
     </RelativeLayout>
 
-    <view class="org.mozilla.gecko.TabsPanel$TabsListContainer"
+    <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsListContainer"
           android:id="@+id/tabs_container"
           android:layout_width="fill_parent"
           android:layout_height="0dip"
           android:layout_weight="1.0">
 
-        <org.mozilla.gecko.TabsTray android:id="@+id/normal_tabs"
-                                    style="@style/TabsList"
-                                    android:layout_width="fill_parent"
-                                    android:layout_height="fill_parent"
-                                    android:choiceMode="singleChoice"
-                                    android:visibility="gone"
-                                    gecko:tabs="tabs_normal"/>
+        <org.mozilla.gecko.tabspanel.TabsTray android:id="@+id/normal_tabs"
+                                              style="@style/TabsList"
+                                              android:layout_width="fill_parent"
+                                              android:layout_height="fill_parent"
+                                              android:choiceMode="singleChoice"
+                                              android:visibility="gone"
+                                              gecko:tabs="tabs_normal"/>
 
-        <org.mozilla.gecko.TabsTray android:id="@+id/private_tabs"
-                                    style="@style/TabsList"
-                                    android:layout_width="fill_parent"
-                                    android:layout_height="fill_parent"
-                                    android:choiceMode="singleChoice"
-                                    android:visibility="gone"
-                                    gecko:tabs="tabs_private"/>
+        <org.mozilla.gecko.tabspanel.TabsTray android:id="@+id/private_tabs"
+                                              style="@style/TabsList"
+                                              android:layout_width="fill_parent"
+                                              android:layout_height="fill_parent"
+                                              android:choiceMode="singleChoice"
+                                              android:visibility="gone"
+                                              gecko:tabs="tabs_private"/>
 
-        <org.mozilla.gecko.RemoteTabsContainer android:id="@+id/synced_tabs"
-                                               android:layout_width="fill_parent"
-                                               android:layout_height="fill_parent"
-                                               android:visibility="gone">
+        <org.mozilla.gecko.tabspanel.RemoteTabsContainer android:id="@+id/synced_tabs"
+                                                         android:layout_width="fill_parent"
+                                                         android:layout_height="fill_parent"
+                                                         android:visibility="gone">
 
-            <org.mozilla.gecko.RemoteTabsList android:id="@+id/synced_tabs_list"
-                                          style="@style/RemoteTabsList"
-                                          android:layout_width="fill_parent"
-                                          android:layout_height="fill_parent"
-                                          android:paddingLeft="@dimen/tabs_panel_list_padding"
-                                          android:paddingRight="@dimen/tabs_panel_list_padding"
-                                          android:scrollbarStyle="outsideOverlay"/>
+            <org.mozilla.gecko.tabspanel.RemoteTabsList android:id="@+id/synced_tabs_list"
+                                                        style="@style/RemoteTabsList"
+                                                        android:layout_width="fill_parent"
+                                                        android:layout_height="fill_parent"
+                                                        android:paddingLeft="@dimen/tabs_panel_list_padding"
+                                                        android:paddingRight="@dimen/tabs_panel_list_padding"
+                                                        android:scrollbarStyle="outsideOverlay"/>
 
-        </org.mozilla.gecko.RemoteTabsContainer>
+        </org.mozilla.gecko.tabspanel.RemoteTabsContainer>
 
     </view>
 
     <RelativeLayout android:id="@+id/tabs_panel_footer"
                     android:layout_width="fill_parent"
                     android:layout_height="@dimen/browser_toolbar_height">
 
-        <view class="org.mozilla.gecko.TabsPanel$TabsPanelToolbar"
+        <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsPanelToolbar"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:background="@color/background_tabs">
 
             <include layout="@layout/tabs_panel_footer"/>
 
         </view>
 
--- a/mobile/android/base/resources/layout/tabs_panel.xml
+++ b/mobile/android/base/resources/layout/tabs_panel.xml
@@ -5,63 +5,63 @@
 
 <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:gecko="http://schemas.android.com/apk/res-auto">
 
     <RelativeLayout android:id="@+id/tabs_panel_header"
                     android:layout_width="fill_parent"
                     android:layout_height="@dimen/browser_toolbar_height">
 
-        <view class="org.mozilla.gecko.TabsPanel$TabsPanelToolbar"
+        <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsPanelToolbar"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:background="@color/background_tabs">
 
             <include layout="@layout/tabs_panel_header"/>
 
         </view>
 
         <View android:layout_width="fill_parent"
               android:layout_height="2dp"
               android:layout_alignParentBottom="true"
               android:background="#1A000000"/>
 
     </RelativeLayout>
 
-    <view class="org.mozilla.gecko.TabsPanel$TabsListContainer"
+    <view class="org.mozilla.gecko.tabspanel.TabsPanel$TabsListContainer"
           android:id="@+id/tabs_container"
           android:layout_width="fill_parent"
           android:layout_height="wrap_content">
 
-        <org.mozilla.gecko.TabsTray android:id="@+id/normal_tabs"
-                                    style="@style/TabsList"
-                                    android:layout_width="fill_parent"
-                                    android:layout_height="fill_parent"
-                                    android:choiceMode="singleChoice"
-                                    android:visibility="gone"
-                                    gecko:tabs="tabs_normal"/>
+        <org.mozilla.gecko.tabspanel.TabsTray android:id="@+id/normal_tabs"
+                                              style="@style/TabsList"
+                                              android:layout_width="fill_parent"
+                                              android:layout_height="fill_parent"
+                                              android:choiceMode="singleChoice"
+                                              android:visibility="gone"
+                                              gecko:tabs="tabs_normal"/>
 
-        <org.mozilla.gecko.TabsTray android:id="@+id/private_tabs"
-                                    style="@style/TabsList"
-                                    android:layout_width="fill_parent"
-                                    android:layout_height="fill_parent"
-                                    android:choiceMode="singleChoice"
-                                    android:visibility="gone"
-                                    gecko:tabs="tabs_private"/>
+        <org.mozilla.gecko.tabspanel.TabsTray android:id="@+id/private_tabs"
+                                              style="@style/TabsList"
+                                              android:layout_width="fill_parent"
+                                              android:layout_height="fill_parent"
+                                              android:choiceMode="singleChoice"
+                                              android:visibility="gone"
+                                              gecko:tabs="tabs_private"/>
 
-        <org.mozilla.gecko.RemoteTabsContainer android:id="@+id/synced_tabs"
-                                               android:layout_width="fill_parent"
-                                               android:layout_height="fill_parent"
-                                               android:visibility="gone">
+        <org.mozilla.gecko.tabspanel.RemoteTabsContainer android:id="@+id/synced_tabs"
+                                                         android:layout_width="fill_parent"
+                                                         android:layout_height="fill_parent"
+                                                         android:visibility="gone">
 
-            <org.mozilla.gecko.RemoteTabsList android:id="@+id/synced_tabs_list"
-                                          style="@style/RemoteTabsList"
-                                          android:layout_width="fill_parent"
-                                          android:layout_height="fill_parent"
-                                          android:paddingLeft="@dimen/tabs_panel_list_padding"
-                                          android:paddingRight="@dimen/tabs_panel_list_padding"
-                                          android:scrollbarStyle="outsideOverlay"/>
+            <org.mozilla.gecko.tabspanel.RemoteTabsList android:id="@+id/synced_tabs"
+                                                        style="@style/RemoteTabsList"
+                                                        android:layout_width="fill_parent"
+                                                        android:layout_height="fill_parent"
+                                                        android:paddingLeft="@dimen/tabs_panel_list_padding"
+                                                        android:paddingRight="@dimen/tabs_panel_list_padding"
+                                                        android:scrollbarStyle="outsideOverlay"/>
 
-        </org.mozilla.gecko.RemoteTabsContainer>
+        </org.mozilla.gecko.tabspanel.RemoteTabsContainer>
 
     </view>
 
 </merge>
--- a/mobile/android/base/resources/layout/tabs_panel_view.xml
+++ b/mobile/android/base/resources/layout/tabs_panel_view.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
-<org.mozilla.gecko.TabsPanel xmlns:android="http://schemas.android.com/apk/res/android"
-                             android:id="@+id/tabs_panel"
-                             android:layout_width="fill_parent"
-                             android:layout_height="fill_parent"
-                             android:background="@color/background_tabs"/>
+<org.mozilla.gecko.tabspanel.TabsPanel xmlns:android="http://schemas.android.com/apk/res/android"
+                                       android:id="@+id/tabs_panel"
+                                       android:layout_width="fill_parent"
+                                       android:layout_height="fill_parent"
+                                       android:background="@color/background_tabs"/>
rename from mobile/android/base/RemoteTabsContainer.java
rename to mobile/android/base/tabspanel/RemoteTabsContainer.java
--- a/mobile/android/base/RemoteTabsContainer.java
+++ b/mobile/android/base/tabspanel/RemoteTabsContainer.java
@@ -1,14 +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;
+package org.mozilla.gecko.tabspanel;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.TabsAccessor;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout;
 
 import android.accounts.Account;
 import android.content.Context;
 import android.util.AttributeSet;
rename from mobile/android/base/RemoteTabsList.java
rename to mobile/android/base/tabspanel/RemoteTabsList.java
--- a/mobile/android/base/RemoteTabsList.java
+++ b/mobile/android/base/tabspanel/RemoteTabsList.java
@@ -1,34 +1,38 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-package org.mozilla.gecko;
+package org.mozilla.gecko.tabspanel;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ExpandableListView;
 import android.widget.SimpleExpandableListAdapter;
 
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.TabsAccessor;
+
 /**
  * The actual list of synced tabs. This serves as the only child view of {@link RemoteTabsContainer}
  * so it can be refreshed using a swipe-to-refresh gesture.
  */
 class RemoteTabsList extends ExpandableListView
-                            implements ExpandableListView.OnGroupClickListener,
-                                       ExpandableListView.OnChildClickListener,
-                                       TabsAccessor.OnQueryTabsCompleteListener {
+                     implements ExpandableListView.OnGroupClickListener,
+                                ExpandableListView.OnChildClickListener,
+                                TabsAccessor.OnQueryTabsCompleteListener {
     private static final String[] CLIENT_KEY = new String[] { "name" };
     private static final String[] TAB_KEY = new String[] { "title", "url" };
     private static final int[] CLIENT_RESOURCE = new int[] { R.id.client };
     private static final int[] TAB_RESOURCE = new int[] { R.id.tab, R.id.url };
 
     private final Context context;
     private TabsPanel tabsPanel;
 
rename from mobile/android/base/TabsPanel.java
rename to mobile/android/base/tabspanel/TabsPanel.java
--- a/mobile/android/base/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -1,15 +1,22 @@
 /* -*- 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;
+package org.mozilla.gecko.tabspanel;
 
+import org.mozilla.gecko.GeckoApp;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoApplication;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.LightweightTheme;
+import org.mozilla.gecko.LightweightThemeDrawable;
+import org.mozilla.gecko.R;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.widget.IconTabWidget;
 
 import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Rect;
 import android.os.Build;
@@ -21,17 +28,17 @@ import android.widget.Button;
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.RelativeLayout;
 
 public class TabsPanel extends LinearLayout
                        implements LightweightTheme.OnChangeListener,
                                   IconTabWidget.OnTabChangedListener {
-    private static final String LOGTAG = "GeckoTabsPanel";
+    private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
 
     public static enum Panel {
         NORMAL_TABS,
         PRIVATE_TABS,
         REMOTE_TABS
     }
 
     public static interface PanelView {
@@ -66,18 +73,18 @@ public class TabsPanel extends LinearLay
     private boolean mVisible;
 
     public TabsPanel(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mActivity = (GeckoApp) context;
         mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
 
-        setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT,
-                                                      LinearLayout.LayoutParams.FILL_PARENT));
+        setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
+                                                      LinearLayout.LayoutParams.MATCH_PARENT));
         setOrientation(LinearLayout.VERTICAL);
 
         mCurrentPanel = Panel.NORMAL_TABS;
         mVisible = false;
 
         mIsSideBar = false;
 
         LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
@@ -115,20 +122,21 @@ public class TabsPanel extends LinearLay
         if (!GeckoProfile.get(mContext).inGuestMode()) {
             mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
         }
 
         mTabWidget.setTabSelectionListener(this);
     }
 
     public void addTab() {
-        if (mCurrentPanel == Panel.NORMAL_TABS)
+        if (mCurrentPanel == Panel.NORMAL_TABS) {
            mActivity.addTab();
-        else
+        } else {
            mActivity.addPrivateTab();
+        }
 
         mActivity.autoHideTabs();
     }
 
     @Override
     public void onTabChanged(int index) {
         if (index == 0)
             show(Panel.NORMAL_TABS, false);
@@ -166,18 +174,19 @@ public class TabsPanel extends LinearLay
         mTheme.addListener(this);
     }
 
     @Override
     public void onDetachedFromWindow() {
         super.onDetachedFromWindow();
         mTheme.removeListener(this);
     }
-    
+
     @Override
+    @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
     public void onLightweightThemeChanged() {
         final int background = getResources().getColor(R.color.background_tabs);
         final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background, true);
         if (drawable == null)
             return;
 
         drawable.setAlpha(34, 0);
         setBackgroundDrawable(drawable);
@@ -190,17 +199,17 @@ public class TabsPanel extends LinearLay
 
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
         onLightweightThemeChanged();
     }
 
     // Tabs List Container holds the ListView
-    public static class TabsListContainer extends FrameLayout {
+    static class TabsListContainer extends FrameLayout {
         public TabsListContainer(Context context, AttributeSet attrs) {
             super(context, attrs);
         }
 
         public PanelView getCurrentPanelView() {
             final int childCount = getChildCount();
             for (int i = 0; i < childCount; i++) {
                 View child = getChildAt(i);
@@ -221,43 +230,44 @@ public class TabsPanel extends LinearLay
                 super.onMeasure(widthMeasureSpec, heightSpec);
             } else {
                 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
             }
         }
     }
 
     // Tabs Panel Toolbar contains the Buttons
-    public static class TabsPanelToolbar extends LinearLayout 
-                                         implements LightweightTheme.OnChangeListener {
+    static class TabsPanelToolbar extends LinearLayout
+                                  implements LightweightTheme.OnChangeListener {
         private final LightweightTheme mTheme;
 
         public TabsPanelToolbar(Context context, AttributeSet attrs) {
             super(context, attrs);
             mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
 
-            setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT,
+            setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                                                           (int) context.getResources().getDimension(R.dimen.browser_toolbar_height)));
 
             setOrientation(LinearLayout.HORIZONTAL);
         }
 
         @Override
         public void onAttachedToWindow() {
             super.onAttachedToWindow();
             mTheme.addListener(this);
         }
 
         @Override
         public void onDetachedFromWindow() {
             super.onDetachedFromWindow();
             mTheme.removeListener(this);
         }
-    
+
         @Override
+        @SuppressWarnings("deprecation") // setBackgroundDrawable deprecated by API level 16
         public void onLightweightThemeChanged() {
             final int background = getResources().getColor(R.color.background_tabs);
             final LightweightThemeDrawable drawable = mTheme.getColorDrawable(this, background);
             if (drawable == null)
                 return;
 
             drawable.setAlpha(34, 34);
             setBackgroundDrawable(drawable);
@@ -274,40 +284,46 @@ public class TabsPanel extends LinearLay
             onLightweightThemeChanged();
         }
     }
 
     public void show(Panel panel) {
         show(panel, true);
     }
 
-    public void show(Panel panel, boolean shouldResize) {
+    public void show(Panel panelToShow, boolean shouldResize) {
         if (!isShown())
             setVisibility(View.VISIBLE);
 
         if (mPanel != null) {
             // Hide the old panel.
             mPanel.hide();
         }
 
         final boolean showAnimation = !mVisible;
         mVisible = true;
-        mCurrentPanel = panel;
+        mCurrentPanel = panelToShow;
 
-        int index = panel.ordinal();
+        int index = panelToShow.ordinal();
         mTabWidget.setCurrentTab(index);
 
-        if (index == 0) {
-            mPanel = mPanelNormal;
-        } else if (index == 1) {
-            mPanel = mPanelPrivate;
-        } else {
-            mPanel = mPanelRemote;
+        switch (panelToShow) {
+            case NORMAL_TABS:
+                mPanel = mPanelNormal;
+                break;
+            case PRIVATE_TABS:
+                mPanel = mPanelPrivate;
+                break;
+            case REMOTE_TABS:
+                mPanel = mPanelRemote;
+                break;
+
+            default:
+                throw new IllegalArgumentException("Unknown panel type " + panelToShow);
         }
-
         mPanel.show();
 
         if (mCurrentPanel == Panel.REMOTE_TABS) {
             if (mFooter != null)
                 mFooter.setVisibility(View.GONE);
 
             mAddTab.setVisibility(View.INVISIBLE);
         } else {
rename from mobile/android/base/TabsTray.java
rename to mobile/android/base/tabspanel/TabsTray.java
--- a/mobile/android/base/TabsTray.java
+++ b/mobile/android/base/tabspanel/TabsTray.java
@@ -1,18 +1,24 @@
 /* -*- 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;
+package org.mozilla.gecko.tabspanel;
 
 import java.util.ArrayList;
 import java.util.List;
 
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.PropertyAnimator.Property;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.widget.TwoWayView;
 import org.mozilla.gecko.widget.TabThumbnailWrapper;
 
 import android.content.Context;
 import android.content.res.TypedArray;
@@ -26,34 +32,33 @@ import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.widget.BaseAdapter;
 import android.widget.Button;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.TextView;
 
-public class TabsTray extends TwoWayView
+class TabsTray extends TwoWayView
     implements TabsPanel.PanelView {
-    private static final String LOGTAG = "GeckoTabsTray";
+    private static final String LOGTAG = "Gecko" + TabsTray.class.getSimpleName();
 
     private Context mContext;
     private TabsPanel mTabsPanel;
 
     private TabsAdapter mTabsAdapter;
 
     private List<View> mPendingClosedTabs;
     private int mCloseAnimationCount;
 
     private TabSwipeGestureListener mSwipeListener;
 
     // Time to animate non-flinged tabs of screen, in milliseconds
     private static final int ANIMATION_DURATION = 250;
 
-    private static final String ABOUT_HOME = "about:home";
     private int mOriginalSize = 0;
 
     public TabsTray(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
 
         mCloseAnimationCount = 0;
         mPendingClosedTabs = new ArrayList<View>();
@@ -419,27 +424,25 @@ public class TabsTray extends TwoWayView
 
         private int mMaxFlingVelocity;
         private VelocityTracker mVelocityTracker;
 
         private int mListWidth = 1;
         private int mListHeight = 1;
 
         private View mSwipeView;
-        private int mSwipeViewPosition;
         private Runnable mPendingCheckForTap;
 
         private float mSwipeStartX;
         private float mSwipeStartY;
         private boolean mSwiping;
         private boolean mEnabled;
 
         public TabSwipeGestureListener() {
             mSwipeView = null;
-            mSwipeViewPosition = TwoWayView.INVALID_POSITION;
             mSwiping = false;
             mEnabled = true;
 
             ViewConfiguration vc = ViewConfiguration.get(TabsTray.this.getContext());
             mSwipeThreshold = vc.getScaledTouchSlop();
             mMinFlingVelocity = (int) (getContext().getResources().getDisplayMetrics().density * MIN_VELOCITY);
             mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
         }
@@ -481,17 +484,16 @@ public class TabsTray extends TwoWayView
                     final float y = e.getRawY();
 
                     // Find out which view is being touched
                     mSwipeView = findViewAt(x, y);
 
                     if (mSwipeView != null) {
                         mSwipeStartX = e.getRawX();
                         mSwipeStartY = e.getRawY();
-                        mSwipeViewPosition = TabsTray.this.getPositionForView(mSwipeView);
 
                         mVelocityTracker = VelocityTracker.obtain();
                         mVelocityTracker.addMovement(e);
                     }
 
                     view.onTouchEvent(e);
                     return true;
                 }
@@ -502,16 +504,19 @@ public class TabsTray extends TwoWayView
 
                     cancelCheckForTap();
                     mSwipeView.setPressed(false);
 
                     if (!mSwiping) {
                         TabRow tab = (TabRow) mSwipeView.getTag();
                         Tabs.getInstance().selectTab(tab.id);
                         autoHidePanel();
+
+                        mVelocityTracker.recycle();
+                        mVelocityTracker = null;
                         break;
                     }
 
                     mVelocityTracker.addMovement(e);
                     mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
 
                     float velocityX = Math.abs(mVelocityTracker.getXVelocity());
                     float velocityY = Math.abs(mVelocityTracker.getYVelocity());
@@ -548,19 +553,19 @@ public class TabsTray extends TwoWayView
                         dismissTranslation = (dismissDirection ? mListHeight : -mListHeight);
                      }
 
                     if (dismiss)
                         animateClose(mSwipeView, dismissTranslation);
                     else
                         animateCancel(mSwipeView);
 
+                    mVelocityTracker.recycle();
                     mVelocityTracker = null;
                     mSwipeView = null;
-                    mSwipeViewPosition = TwoWayView.INVALID_POSITION;
 
                     mSwipeStartX = 0;
                     mSwipeStartY = 0;
                     mSwiping = false;
 
                     break;
                 }
 
@@ -593,16 +598,17 @@ public class TabsTray extends TwoWayView
                         tab.close.setVisibility(View.INVISIBLE);
 
                         // Stops listview from highlighting the touched item
                         // in the list when swiping.
                         MotionEvent cancelEvent = MotionEvent.obtain(e);
                         cancelEvent.setAction(MotionEvent.ACTION_CANCEL |
                                 (e.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                         TabsTray.this.onTouchEvent(cancelEvent);
+                        cancelEvent.recycle();
                     }
 
                     if (mSwiping) {
                         if (isVertical)
                             ViewHelper.setTranslationX(mSwipeView, delta);
                         else
                             ViewHelper.setTranslationY(mSwipeView, delta);
 
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -113,16 +113,18 @@ skip-if = android_version == "10"
 [testBrowserDiscovery]
 [testDeviceSearchEngine]
 [testJNI]
 # [testMozPay] # see bug 945675
 [testOrderedBroadcast]
 [testSharedPreferences]
 [testSimpleDiscovery]
 [testUITelemetry]
+# disabled on 2.2, see bug 993813
+skip-if = android_version == "8"
 [testVideoDiscovery]
 
 # Used for Talos, please don't use in mochitest
 #[testPan]
 #[testCheck]
 #[testCheck2]
 #[testBrowserProviderPerf]
 
--- a/mobile/android/base/util/HardwareUtils.java
+++ b/mobile/android/base/util/HardwareUtils.java
@@ -69,16 +69,20 @@ public final class HardwareUtils {
             int screenLayout = sContext.getResources().getConfiguration().screenLayout;
             sIsSmallTablet = (Build.VERSION.SDK_INT >= 11 &&
                               ((screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == Configuration.SCREENLAYOUT_SIZE_LARGE));
         }
         return sIsSmallTablet;
     }
 
     public static boolean isTelevision() {
+        if (Build.VERSION.SDK_INT < 16) {
+            // System feature not supported before Jelly Bean.
+            return false;
+        }
         if (sIsTelevision == null) {
             sIsTelevision = sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION);
         }
         return sIsTelevision;
     }
 
     public static boolean hasMenuButton() {
         if (sHasMenuButton == null) {
--- a/mobile/android/base/widget/TwoWayView.java
+++ b/mobile/android/base/widget/TwoWayView.java
@@ -1324,17 +1324,17 @@ public class TwoWayView extends AdapterV
 
             final float diff = pos - mLastTouchPos + mTouchRemainderPos;
             final int delta = (int) diff;
             mTouchRemainderPos = diff - delta;
 
             if (maybeStartScrolling(delta)) {
                 return true;
             }
-            
+
             break;
         }
 
         case MotionEvent.ACTION_CANCEL:
         case MotionEvent.ACTION_UP:
             mActivePointerId = INVALID_POINTER;
             mTouchMode = TOUCH_MODE_REST;
             recycleVelocityTracker();
@@ -1443,16 +1443,18 @@ public class TwoWayView extends AdapterV
                 maybeScroll(delta);
                 break;
             }
 
             break;
         }
 
         case MotionEvent.ACTION_CANCEL:
+            // If this MotionEvent has been created by us, be sure not to store
+            // pointers to it outside of this method call because we recycle it.
             cancelCheckForTap();
             mTouchMode = TOUCH_MODE_REST;
             reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
 
             setPressed(false);
             View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
             if (motionView != null) {
                 motionView.setPressed(false);
@@ -2379,17 +2381,17 @@ public class TwoWayView extends AdapterV
                 // Item already has enough of it visible, changing selection is good enough
                 return 0;
             }
 
             int amountToScroll = (goalStart - viewToMakeVisibleStart);
 
             if (mFirstPosition == 0) {
                 final View firstChild = getChildAt(0);
-                final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft()); 
+                final int firstChildStart = (mIsVertical ? firstChild.getTop() : firstChild.getLeft());
 
                 // First is first in list -> make sure we don't scroll past it
                 final int max = start - firstChildStart;
                 amountToScroll = Math.min(amountToScroll,  max);
             }
 
             return Math.min(amountToScroll, getMaxScrollAmount());
         }
--- a/mobile/android/chrome/content/MemoryObserver.js
+++ b/mobile/android/chrome/content/MemoryObserver.js
@@ -24,16 +24,22 @@ var MemoryObserver = {
     let selected = BrowserApp.selectedTab;
     for (let i = 0; i < tabs.length; i++) {
       if (tabs[i] != selected) {
         this.zombify(tabs[i]);
         Telemetry.addData("FENNEC_TAB_ZOMBIFIED", (Date.now() - tabs[i].lastTouchedAt) / 1000);
       }
     }
     Telemetry.addData("FENNEC_LOWMEM_TAB_COUNT", tabs.length);
+
+    // Change some preferences temporarily for only this session
+    let defaults = Services.prefs.getDefaultBranch(null);
+
+    // Reduce the amount of decoded image data we keep around
+    defaults.setIntPref("image.mem.max_decoded_image_kb", 0);
   },
 
   zombify: function(tab) {
     let browser = tab.browser;
     let data = browser.__SS_data;
     let extra = browser.__SS_extdata;
 
     // We need this data to correctly create and position the new browser
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -64,62 +64,19 @@ this.CommonUtils = {
     for (let x of a) {
       if (!b.has(x)) {
         return false;
       }
     }
     return true;
   },
 
-  exceptionStr: function exceptionStr(e) {
-    if (!e) {
-      return "" + e;
-    }
-    let message = e.message ? e.message : e;
-    return message + " " + CommonUtils.stackTrace(e);
-  },
-
-  stackTrace: function stackTrace(e) {
-    // Wrapped nsIException
-    if (e.location) {
-      let frame = e.location;
-      let output = [];
-      while (frame) {
-        // Works on frames or exceptions, munges file:// URIs to shorten the paths
-        // FIXME: filename munging is sort of hackish, might be confusing if
-        // there are multiple extensions with similar filenames
-        let str = "<file:unknown>";
-
-        let file = frame.filename || frame.fileName;
-        if (file){
-          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
-        }
-
-        if (frame.lineNumber){
-          str += ":" + frame.lineNumber;
-        }
-        if (frame.name){
-          str = frame.name + "()@" + str;
-        }
-
-        if (str){
-          output.push(str);
-        }
-        frame = frame.caller;
-      }
-      return "Stack trace: " + output.join(" < ");
-    }
-    // Standard JS exception
-    if (e.stack){
-      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
-        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
-    }
-
-    return "No traceback available";
-  },
+  // Import these from Log.jsm for backward compatibility
+  exceptionStr: Log.exceptionStr,
+  stackTrace: Log.stackTrace,
 
   /**
    * Encode byte string as base64URL (RFC 4648).
    *
    * @param bytes
    *        (string) Raw byte string to encode.
    * @param pad
    *        (bool) Whether to include padding characters (=). Defaults
--- a/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
+++ b/services/metrics/tests/xpcshell/test_metrics_provider_manager.js
@@ -291,18 +291,19 @@ add_task(function test_category_manager_
     deferred.resolve(msg);
   };
 
   yield manager.registerProvidersFromCategoryManager("registration-errors");
   do_check_eq(manager.providers.length, 0);
   do_check_eq(errorCount, 1);
 
   let msg = yield deferred.promise;
-  do_check_true(msg.contains("Provider error: DummyThrowOnInitProvider: " +
-                             "Error registering provider from category manager: Dummy Error"));
+  do_check_true(msg.contains("Provider error: DummyThrowOnInitProvider: "
+                             + "Error registering provider from category manager: "
+                             + "Error: Dummy Error"));
 
   yield storage.close();
 });
 
 add_task(function test_pull_only_registration_error() {
   let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
   let manager = new Metrics.ProviderManager(storage);
 
@@ -317,17 +318,17 @@ add_task(function test_pull_only_registr
   yield manager.registerProviderFromType(DummyPullOnlyThrowsOnInitProvider);
   do_check_eq(errorCount, 0);
 
   yield manager.ensurePullOnlyProvidersRegistered();
   do_check_eq(errorCount, 1);
 
   let msg = yield deferred.promise;
   do_check_true(msg.contains("Provider error: DummyPullOnlyThrowsOnInitProvider: " +
-                             "Error registering pull-only provider: Dummy Error"));
+                             "Error registering pull-only provider: Error: Dummy Error"));
 
   yield storage.close();
 });
 
 add_task(function test_error_during_shutdown() {
   let storage = yield Metrics.Storage(REGISTRATION_ERRORS_DB);
   let manager = new Metrics.ProviderManager(storage);
 
@@ -345,12 +346,12 @@ add_task(function test_error_during_shut
   do_check_eq(manager.providers.length, 1);
 
   yield manager.ensurePullOnlyProvidersRegistered();
   do_check_eq(errorCount, 0);
   yield manager.ensurePullOnlyProvidersUnregistered();
   do_check_eq(errorCount, 1);
   let msg = yield deferred.promise;
   do_check_true(msg.contains("Provider error: DummyThrowOnShutdownProvider: " +
-                             "Error when shutting down provider: Dummy shutdown error"));
+                             "Error when shutting down provider: Error: Dummy shutdown error"));
 
   yield storage.close();
 });
--- a/storage/src/mozStoragePrivateHelpers.cpp
+++ b/storage/src/mozStoragePrivateHelpers.cpp
@@ -97,18 +97,23 @@ checkAndLogStatementPerformance(sqlite3_
 
   nsAutoCString message;
   message.AppendInt(count);
   if (count == 1)
     message.Append(" sort operation has ");
   else
     message.Append(" sort operations have ");
   message.Append("occurred for the SQL statement '");
+#ifdef MOZ_STORAGE_SORTWARNING_SQL_DUMP
+  message.Append("SQL command: ");
+  message.Append(sql);
+#else
   nsPrintfCString address("0x%p", aStatement);
   message.Append(address);
+#endif
   message.Append("'.  See https://developer.mozilla.org/En/Storage/Warnings "
                  "details.");
   NS_WARNING(message.get());
 }
 
 nsIVariant *
 convertJSValToVariant(
   JSContext *aCtx,
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -100,17 +100,17 @@
                 readonly="true"/>
 
       <field name="_contentTitle">""</field>
 
       <property name="contentTitle"
                 onget="return this._contentTitle"
                 readonly="true"/>
 
-      <field name="_characterSet">null</field>
+      <field name="_characterSet">""</field>
 
       <property name="characterSet"
                 onget="return this._characterSet"
                 readonly="true"/>
 
       <field name="_contentWindow">null</field>
 
       <property name="contentWindow"
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1,16 +1,28 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const Debugger = require("Debugger");
+const Services = require("Services");
+const { Cc, Ci, Cu, components } = require("chrome");
+const { ActorPool } = require("devtools/server/actors/common");
+const { DebuggerServer } = require("devtools/server/main");
+const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
+const { dbg_assert, dumpn } = DevToolsUtils;
+const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
+const { all, defer, resolve } = promise;
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
 let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
 
 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
       "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
       "Float64Array"];
 
 // Number of items to preview in objects, arrays, maps, sets, lists,
 // collections, etc.
@@ -33,17 +45,17 @@ function mapURIToAddonID(uri, id) {
     addonManager = Cc["@mozilla.org/addons/integration;1"].
                    getService(Ci.amIAddonManager);
   }
 
   try {
     return addonManager.mapURIToAddonID(uri, id);
   }
   catch (e) {
-    DevtoolsUtils.reportException("mapURIToAddonID", e);
+    DevToolsUtils.reportException("mapURIToAddonID", e);
     return false;
   }
 }
 
 /**
  * BreakpointStore objects keep track of all breakpoints that get set so that we
  * can reset them when the same script is introduced to the thread again (such
  * as after a refresh).
@@ -308,16 +320,18 @@ BreakpointStore.prototype = {
     } else {
       for (let column in this._breakpoints[aUrl][aLine]) {
         yield column;
       }
     }
   },
 };
 
+exports.BreakpointStore = BreakpointStore;
+
 /**
  * Manages pushing event loops and automatically pops and exits them in the
  * correct order as they are resolved.
  *
  * @param nsIJSInspector inspector
  *        The underlying JS inspector we use to enter and exit nested event
  *        loops.
  * @param ThreadActor thread
@@ -2393,16 +2407,17 @@ ThreadActor.prototype.requestTypes = {
   "eventListeners": ThreadActor.prototype.onEventListeners,
   "releaseMany": ThreadActor.prototype.onReleaseMany,
   "setBreakpoint": ThreadActor.prototype.onSetBreakpoint,
   "sources": ThreadActor.prototype.onSources,
   "threadGrips": ThreadActor.prototype.onThreadGrips,
   "prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
 };
 
+exports.ThreadActor = ThreadActor;
 
 /**
  * Creates a PauseActor.
  *
  * PauseActors exist for the lifetime of a given debuggee pause.  Used to
  * scope pause-lifetime grips.
  *
  * @param ActorPool aPool
@@ -3456,16 +3471,17 @@ ObjectActor.prototype.requestTypes = {
   "property": ObjectActor.prototype.onProperty,
   "displayString": ObjectActor.prototype.onDisplayString,
   "ownPropertyNames": ObjectActor.prototype.onOwnPropertyNames,
   "decompile": ObjectActor.prototype.onDecompile,
   "release": ObjectActor.prototype.onRelease,
   "scope": ObjectActor.prototype.onScope,
 };
 
+exports.ObjectActor = ObjectActor;
 
 /**
  * Functions for adding information to ObjectActor grips for the purpose of
  * having customized output. This object holds arrays mapped by
  * Debugger.Object.prototype.class.
  *
  * In each array you can add functions that take two
  * arguments:
@@ -4231,16 +4247,17 @@ LongStringActor.prototype = {
   },
 };
 
 LongStringActor.prototype.requestTypes = {
   "substring": LongStringActor.prototype.onSubstring,
   "release": LongStringActor.prototype.onRelease
 };
 
+exports.LongStringActor = LongStringActor;
 
 /**
  * Creates an actor for the specified stack frame.
  *
  * @param aFrame Debugger.Frame
  *        The debuggee frame.
  * @param aThreadActor ThreadActor
  *        The parent thread actor for this frame.
@@ -4642,16 +4659,18 @@ EnvironmentActor.prototype = {
   }
 };
 
 EnvironmentActor.prototype.requestTypes = {
   "assign": EnvironmentActor.prototype.onAssign,
   "bindings": EnvironmentActor.prototype.onBindings
 };
 
+exports.EnvironmentActor = EnvironmentActor;
+
 /**
  * Override the toString method in order to get more meaningful script output
  * for debugging the debugger.
  */
 Debugger.Script.prototype.toString = function() {
   let output = "";
   if (this.url) {
     output += this.url;
@@ -4743,16 +4762,18 @@ update(ChromeDebuggerActor.prototype, {
         type: "newGlobal",
         // TODO: after bug 801084 lands see if we need to JSONify this.
         hostAnnotations: aGlobal.hostAnnotations
       });
     }
   }
 });
 
+exports.ChromeDebuggerActor = ChromeDebuggerActor;
+
 /**
  * Creates an actor for handling add-on debugging. AddonThreadActor is
  * a thin wrapper over ThreadActor.
  *
  * @param aConnection object
  *        The DebuggerServerConnection with which this AddonThreadActor
  *        is associated. (Currently unused, but required to make this
  *        constructor usable with addGlobalActor.)
@@ -4918,16 +4939,18 @@ update(AddonThreadActor.prototype, {
   }
 });
 
 AddonThreadActor.prototype.requestTypes = Object.create(ThreadActor.prototype.requestTypes);
 update(AddonThreadActor.prototype.requestTypes, {
   "attach": AddonThreadActor.prototype.onAttach
 });
 
+exports.AddonThreadActor = AddonThreadActor;
+
 /**
  * Manages the sources for a thread. Handles source maps, locations in the
  * sources, etc for ThreadActors.
  */
 function ThreadSources(aThreadActor, aUseSourceMaps, aAllowPredicate,
                        aOnNewSource) {
   this._thread = aThreadActor;
   this._useSourceMaps = aUseSourceMaps;
@@ -5280,16 +5303,18 @@ ThreadSources.prototype = {
 
   iter: function* () {
     for (let url in this._sourceActors) {
       yield this._sourceActors[url];
     }
   }
 };
 
+exports.ThreadSources = ThreadSources;
+
 // Utility functions.
 
 // TODO bug 863089: use Debugger.Script.prototype.getOffsetColumn when it is
 // implemented.
 function getOffsetColumn(aOffset, aScript) {
   let bestOffsetMapping = null;
   for (let offsetMapping of aScript.getAllColumnOffsets()) {
     if (!bestOffsetMapping ||
@@ -5388,17 +5413,17 @@ function fetch(aURL, aOptions={ loadFrom
   }
 
   switch (scheme) {
     case "file":
     case "chrome":
     case "resource":
       try {
         NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
-          if (!Components.isSuccessCode(aStatus)) {
+          if (!components.isSuccessCode(aStatus)) {
             deferred.reject(new Error("Request failed with status code = "
                                       + aStatus
                                       + " after NetUtil.asyncFetch for url = "
                                       + url));
             return;
           }
 
           let source = NetUtil.readInputStreamToString(aStream, aStream.available());
@@ -5419,28 +5444,28 @@ function fetch(aURL, aOptions={ loadFrom
         // On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
         // newChannel won't be able to handle it.
         url = "file:///" + url;
         channel = Services.io.newChannel(url, null, null);
       }
       let chunks = [];
       let streamListener = {
         onStartRequest: function(aRequest, aContext, aStatusCode) {
-          if (!Components.isSuccessCode(aStatusCode)) {
+          if (!components.isSuccessCode(aStatusCode)) {
             deferred.reject(new Error("Request failed with status code = "
                                       + aStatusCode
                                       + " in onStartRequest handler for url = "
                                       + url));
           }
         },
         onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
           chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
         },
         onStopRequest: function(aRequest, aContext, aStatusCode) {
-          if (!Components.isSuccessCode(aStatusCode)) {
+          if (!components.isSuccessCode(aStatusCode)) {
             deferred.reject(new Error("Request failed with status code = "
                                       + aStatusCode
                                       + " in onStopRequest handler for url = "
                                       + url));
             return;
           }
 
           charset = channel.contentCharset;
@@ -5595,8 +5620,20 @@ function makeDebuggeeValueIfNeeded(obj, 
   }
   return value;
 }
 
 function getInnerId(window) {
   return window.QueryInterface(Ci.nsIInterfaceRequestor).
                 getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
 };
+
+exports.register = function(handle) {
+  ThreadActor.breakpointStore = new BreakpointStore();
+  ThreadSources._blackBoxedSources = new Set(["self-hosted"]);
+  ThreadSources._prettyPrintedSources = new Map();
+};
+
+exports.unregister = function(handle) {
+  ThreadActor.breakpointStore = null;
+  ThreadSources._blackBoxedSources.clear();
+  ThreadSources._prettyPrintedSources.clear();
+};
--- a/toolkit/devtools/server/actors/webbrowser.js
+++ b/toolkit/devtools/server/actors/webbrowser.js
@@ -1,22 +1,23 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-let {Ci,Cu} = require("chrome");
-let {createExtraActors, appendExtraActors} = require("devtools/server/actors/common");
+let { Ci, Cu } = require("chrome");
+let Services = require("Services");
+let { createExtraActors, appendExtraActors } = require("devtools/server/actors/common");
+let { AddonThreadActor, ThreadActor } = require("devtools/server/actors/script");
 let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
 
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
-Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
 // by calling the listeners in the order or registration.
 XPCOMUtils.defineLazyGetter(this, "events", () => {
--- a/toolkit/devtools/server/actors/webconsole.js
+++ b/toolkit/devtools/server/actors/webconsole.js
@@ -1,27 +1,23 @@
 /* -*- js2-basic-offset: 2; indent-tabs-mode: nil; -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-let {Cc, Ci, Cu} = require("chrome");
+const { Cc, Ci, Cu } = require("chrome");
+const Debugger = require("Debugger");
+const { DebuggerServer, ActorPool } = require("devtools/server/main");
+const { EnvironmentActor, LongStringActor, ObjectActor, ThreadActor } = require("devtools/server/actors/script");
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-let { DebuggerServer, ActorPool } = require("devtools/server/main");
-// Symbols from script.js
-let { ThreadActor, EnvironmentActor, ObjectActor, LongStringActor } = DebuggerServer;
-
-Cu.import("resource://gre/modules/jsdebugger.jsm");
-addDebuggerToGlobal(this);
-
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyGetter(this, "NetworkMonitor", () => {
   return require("devtools/toolkit/webconsole/network-monitor")
          .NetworkMonitor;
 });
 XPCOMUtils.defineLazyGetter(this, "NetworkMonitorChild", () => {
   return require("devtools/toolkit/webconsole/network-monitor")
--- a/toolkit/devtools/server/main.js
+++ b/toolkit/devtools/server/main.js
@@ -341,17 +341,18 @@ var DebuggerServer = {
    * root actor.
    */
   addBrowserActors: function(aWindowType = "navigator:browser", restrictPrivileges = false) {
     this.chromeWindowType = aWindowType;
     this.addActors("resource://gre/modules/devtools/server/actors/webbrowser.js");
 
     if (!restrictPrivileges) {
       this.addTabActors();
-      this.addGlobalActor(this.ChromeDebuggerActor, "chromeDebugger");
+      let { ChromeDebuggerActor } = require("devtools/server/actors/script");
+      this.addGlobalActor(ChromeDebuggerActor, "chromeDebugger");
       this.registerModule("devtools/server/actors/preference");
     }
 
     this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
     this.registerModule("devtools/server/actors/device");
   },
 
   /**
@@ -372,17 +373,17 @@ var DebuggerServer = {
       this.addActors("resource://gre/modules/devtools/server/actors/childtab.js");
     }
   },
 
   /**
    * Install tab actors.
    */
   addTabActors: function() {
-    this.addActors("resource://gre/modules/devtools/server/actors/script.js");
+    this.registerModule("devtools/server/actors/script");
     this.registerModule("devtools/server/actors/webconsole");
     this.registerModule("devtools/server/actors/inspector");
     this.registerModule("devtools/server/actors/call-watcher");
     this.registerModule("devtools/server/actors/canvas");
     this.registerModule("devtools/server/actors/webgl");
     this.registerModule("devtools/server/actors/webaudio");
     this.registerModule("devtools/server/actors/stylesheets");
     this.registerModule("devtools/server/actors/styleeditor");
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -3,20 +3,20 @@
 
 "use strict";
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+
 const Services = devtools.require("Services");
-const { ActorPool, createExtraActors, appendExtraActors } = devtools.require("devtools/server/actors/common");
 const DevToolsUtils = devtools.require("devtools/toolkit/DevToolsUtils.js");
-const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 // Always log packets when running tests. runxpcshelltests.py will throw
 // the output away anyway, unless you give it the --verbose flag.
 Services.prefs.setBoolPref("devtools.debugger.log", true);
 // Enable remote debugging for the relevant tests.
 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", true);
 
 function tryImport(url) {
@@ -29,16 +29,18 @@ function tryImport(url) {
   }
 }
 
 tryImport("resource://gre/modules/devtools/dbg-server.jsm");
 tryImport("resource://gre/modules/devtools/dbg-client.jsm");
 tryImport("resource://gre/modules/devtools/Loader.jsm");
 tryImport("resource://gre/modules/devtools/Console.jsm");
 
+let { BreakpointStore, LongStringActor, ThreadActor } = devtools.require("devtools/server/actors/script");
+
 function testExceptionHook(ex) {
   try {
     do_report_unexpected_exception(ex);
   } catch(ex) {
     return {throw: ex}
   }
   return undefined;
 }
@@ -177,26 +179,26 @@ function attachTestTabAndResume(aClient,
 }
 
 /**
  * Initialize the testing debugger server.
  */
 function initTestDebuggerServer()
 {
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
-  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
+  DebuggerServer.registerModule("devtools/server/actors/script");
   DebuggerServer.addActors("resource://test/testactors.js");
   // Allow incoming connections.
   DebuggerServer.init(function () { return true; });
 }
 
 function initTestTracerServer()
 {
   DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/root.js");
-  DebuggerServer.addActors("resource://gre/modules/devtools/server/actors/script.js");
+  DebuggerServer.registerModule("devtools/server/actors/script");
   DebuggerServer.addActors("resource://test/testactors.js");
   DebuggerServer.registerModule("devtools/server/actors/tracer");
   // Allow incoming connections.
   DebuggerServer.init(function () { return true; });
 }
 
 function finishClient(aClient)
 {
--- a/toolkit/devtools/server/tests/unit/test_breakpointstore.js
+++ b/toolkit/devtools/server/tests/unit/test_breakpointstore.js
@@ -3,19 +3,16 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test the functionality of the BreakpointStore object.
 
 function run_test()
 {
   Cu.import("resource://gre/modules/jsdebugger.jsm");
   addDebuggerToGlobal(this);
-  let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
-    .getService(Components.interfaces.mozIJSSubScriptLoader);
-  loader.loadSubScript("resource://gre/modules/devtools/server/actors/script.js");
 
   test_has_breakpoint();
   test_bug_754251();
   test_add_breakpoint();
   test_remove_breakpoint();
   test_find_breakpoints();
 }
 
--- a/toolkit/devtools/server/tests/unit/test_longstringactor.js
+++ b/toolkit/devtools/server/tests/unit/test_longstringactor.js
@@ -1,19 +1,16 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function run_test()
 {
   Cu.import("resource://gre/modules/jsdebugger.jsm");
   addDebuggerToGlobal(this);
-  let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
-    .getService(Components.interfaces.mozIJSSubScriptLoader);
-  loader.loadSubScript("resource://gre/modules/devtools/server/actors/script.js");
 
   test_LSA_disconnect();
   test_LSA_grip();
   test_LSA_onSubstring();
 }
 
 const TEST_STRING = "This is a very long string!";
 
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -1,11 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+const Cu = Components.utils;
+const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const { ThreadActor } = devtools.require("devtools/server/actors/script");
+
 var gTestGlobals = [];
 DebuggerServer.addTestGlobal = function(aGlobal) {
   gTestGlobals.push(aGlobal);
 };
 
 // A mock tab list, for use by tests. This simply presents each global in
 // gTestGlobals as a tab, and the list is fixed: it never calls its
 // onListChanged handler.
--- a/toolkit/modules/Log.jsm
+++ b/toolkit/modules/Log.jsm
@@ -15,16 +15,26 @@ const ONE_MEGABYTE = 1024 * ONE_KILOBYTE
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
+const INTERNAL_FIELDS = new Set(["_level", "_message", "_time", "_namespace"]);
+
+
+/*
+ * Dump a message everywhere we can if we have a failure.
+ */
+function dumpError(text) {
+  dump(text + "\n");
+  Cu.reportError(text);
+}
 
 this.Log = {
   Level: {
     Fatal:  70,
     Error:  60,
     Warn:   50,
     Info:   40,
     Config: 30,
@@ -75,16 +85,17 @@ this.Log = {
   Appender: Appender,
   DumpAppender: DumpAppender,
   ConsoleAppender: ConsoleAppender,
   StorageStreamAppender: StorageStreamAppender,
 
   FileAppender: FileAppender,
   BoundedFileAppender: BoundedFileAppender,
 
+  ParameterFormatter: ParameterFormatter,
   // Logging helper:
   // let logger = Log.repository.getLogger("foo");
   // logger.info(Log.enumerateInterfaces(someObject).join(","));
   enumerateInterfaces: function Log_enumerateInterfaces(aObject) {
     let interfaces = [];
 
     for (i in Ci) {
       try {
@@ -95,60 +106,143 @@ this.Log = {
     }
 
     return interfaces;
   },
 
   // Logging helper:
   // let logger = Log.repository.getLogger("foo");
   // logger.info(Log.enumerateProperties(someObject).join(","));
-  enumerateProperties: function Log_enumerateProps(aObject,
-                                                       aExcludeComplexTypes) {
+  enumerateProperties: function (aObject, aExcludeComplexTypes) {
     let properties = [];
 
     for (p in aObject) {
       try {
         if (aExcludeComplexTypes &&
-            (typeof aObject[p] == "object" || typeof aObject[p] == "function"))
+            (typeof(aObject[p]) == "object" || typeof(aObject[p]) == "function"))
           continue;
         properties.push(p + " = " + aObject[p]);
       }
       catch(ex) {
         properties.push(p + " = " + ex);
       }
     }
 
     return properties;
+  },
+
+  _formatError: function _formatError(e) {
+    let result = e.toString();
+    if (e.fileName) {
+      result +=  " (" + e.fileName;
+      if (e.lineNumber) {
+        result += ":" + e.lineNumber;
+      }
+      if (e.columnNumber) {
+        result += ":" + e.columnNumber;
+      }
+      result += ")";
+    }
+    return result + " " + Log.stackTrace(e);
+  },
+
+  // This is for back compatibility with services/common/utils.js; we duplicate
+  // some of the logic in ParameterFormatter
+  exceptionStr: function exceptionStr(e) {
+    if (!e) {
+      return "" + e;
+    }
+    if (e instanceof Ci.nsIException) {
+      return e.toString() + " " + Log.stackTrace(e);
+    }
+    else if (isError(e)) {
+      return Log._formatError(e);
+    }
+    // else
+    let message = e.message ? e.message : e;
+    return message + " " + Log.stackTrace(e);
+  },
+
+  stackTrace: function stackTrace(e) {
+    // Wrapped nsIException
+    if (e.location) {
+      let frame = e.location;
+      let output = [];
+      while (frame) {
+        // Works on frames or exceptions, munges file:// URIs to shorten the paths
+        // FIXME: filename munging is sort of hackish, might be confusing if
+        // there are multiple extensions with similar filenames
+        let str = "<file:unknown>";
+
+        let file = frame.filename || frame.fileName;
+        if (file) {
+          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
+        }
+
+        if (frame.lineNumber) {
+          str += ":" + frame.lineNumber;
+        }
+
+        if (frame.name) {
+          str = frame.name + "()@" + str;
+        }
+
+        if (str) {
+          output.push(str);
+        }
+        frame = frame.caller;
+      }
+      return "Stack trace: " + output.join(" < ");
+    }
+    // Standard JS exception
+    if (e.stack) {
+      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
+        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
+    }
+
+    return "No traceback available";
   }
 };
 
-
 /*
  * LogMessage
  * Encapsulates a single log event's data
  */
 function LogMessage(loggerName, level, message, params) {
   this.loggerName = loggerName;
   this.level = level;
-  this.message = message;
-  this.params = params;
+  /*
+   * Special case to handle "log./level/(object)", for example logging a caught exception
+   * without providing text or params like: catch(e) { logger.warn(e) }
+   * Treating this as an empty text with the object in the 'params' field causes the
+   * object to be formatted properly by BasicFormatter.
+   */
+  if (!params && message && (typeof(message) == "object") &&
+      (typeof(message.valueOf()) != "string")) {
+    this.message = null;
+    this.params = message;
+  } else {
+    // If the message text is empty, or a string, or a String object, normal handling
+    this.message = message;
+    this.params = params;
+  }
 
   // The _structured field will correspond to whether this message is to
   // be interpreted as a structured message.
   this._structured = this.params && this.params.action;
   this.time = Date.now();
 }
 LogMessage.prototype = {
   get levelDesc() {
     if (this.level in Log.Level.Desc)
       return Log.Level.Desc[this.level];
     return "UNKNOWN";
   },
 
-  toString: function LogMsg_toString(){
+  toString: function LogMsg_toString() {
     let msg = "LogMessage [" + this.time + " " + this.level + " " +
       this.message;
     if (this.params) {
       msg += " " + JSON.stringify(this.params);
     }
     return msg + "]"
   }
 };
@@ -173,17 +267,17 @@ Logger.prototype = {
   },
 
   _level: null,
   get level() {
     if (this._level != null)
       return this._level;
     if (this.parent)
       return this.parent.level;
-    dump("Log warning: root logger configuration error: no level defined\n");
+    dumpError("Log warning: root logger configuration error: no level defined");
     return Log.Level.All;
   },
   set level(level) {
     this._level = level;
   },
 
   _parent: null,
   get parent() this._parent,
@@ -241,32 +335,38 @@ Logger.prototype = {
    *
    * @param action
    *        (string) A message action, one of a set of actions known to the
    *          log consumer.
    * @param params
    *        (object) Parameters to be included in the message.
    *          If _level is included as a key and the corresponding value
    *          is a number or known level name, the message will be logged
-   *          at the indicated level.
+   *          at the indicated level. If _message is included as a key, the
+   *          value is used as the descriptive text for the message.
    */
   logStructured: function (action, params) {
     if (!action) {
       throw "An action is required when logging a structured message.";
     }
     if (!params) {
       return this.log(this.level, undefined, {"action": action});
     }
-    if (typeof params != "object") {
+    if (typeof(params) != "object") {
       throw "The params argument is required to be an object.";
     }
 
-    let level = params._level || this.level;
-    if ((typeof level == "string") && level in Log.Level.Numbers) {
-      level = Log.Level.Numbers[level];
+    let level = params._level;
+    if (level) {
+      let ulevel = level.toUpperCase();
+      if (ulevel in Log.Level.Numbers) {
+        level = Log.Level.Numbers[ulevel];
+      }
+    } else {
+      level = this.level;
     }
 
     params.action = action;
     this.log(level, params._message, params);
   },
 
   log: function (level, string, params) {
     if (this.level > level)
@@ -423,40 +523,92 @@ LoggerRepository.prototype = {
 // Abstract formatter
 function Formatter() {}
 Formatter.prototype = {
   format: function Formatter_format(message) {}
 };
 
 // Basic formatter that doesn't do anything fancy.
 function BasicFormatter(dateFormat) {
-  if (dateFormat)
+  if (dateFormat) {
     this.dateFormat = dateFormat;
+  }
+  this.parameterFormatter = new ParameterFormatter();
 }
 BasicFormatter.prototype = {
   __proto__: Formatter.prototype,
 
+  /**
+   * Format the text of a message with optional parameters.
+   * If the text contains ${identifier}, replace that with
+   * the value of params[identifier]; if ${}, replace that with
+   * the entire params object. If no params have been substituted
+   * into the text, format the entire object and append that
+   * to the message.
+   */
+  formatText: function (message) {
+    let params = message.params;
+    if (!params) {
+      return message.message || "";
+    }
+    // Defensive handling of non-object params
+    // We could add a special case for NSRESULT values here...
+    let pIsObject = (typeof(params) == 'object' || typeof(params) == 'function');
+
+    // if we have params, try and find substitutions.
+    if (message.params && this.parameterFormatter) {
+      // have we successfully substituted any parameters into the message?
+      // in the log message
+      let subDone = false;
+      let regex = /\$\{(\S*)\}/g;
+      let textParts = [];
+      if (message.message) {
+        textParts.push(message.message.replace(regex, (_, sub) => {
+          // ${foo} means use the params['foo']
+          if (sub) {
+            if (pIsObject && sub in message.params) {
+              subDone = true;
+              return this.parameterFormatter.format(message.params[sub]);
+            }
+            return '${' + sub + '}';
+          }
+          // ${} means use the entire params object.
+          subDone = true;
+          return this.parameterFormatter.format(message.params);
+        }));
+      }
+      if (!subDone) {
+        // There were no substitutions in the text, so format the entire params object
+        let rest = this.parameterFormatter.format(message.params);
+        if (rest !== null && rest != "{}") {
+          textParts.push(rest);
+        }
+      }
+      return textParts.join(': ');
+    }
+  },
+
   format: function BF_format(message) {
     return message.time + "\t" +
       message.loggerName + "\t" +
       message.levelDesc + "\t" +
-      message.message + "\n";
+      this.formatText(message);
   }
 };
 
 /**
  * A formatter that only formats the string message component.
  */
 function MessageOnlyFormatter() {
 }
 MessageOnlyFormatter.prototype = Object.freeze({
   __proto__: Formatter.prototype,
 
   format: function (message) {
-    return message.message + "\n";
+    return message.message;
   },
 });
 
 // Structured formatter that outputs JSON based on message data.
 // This formatter will format unstructured messages by supplying
 // default values.
 function StructuredFormatter() { }
 StructuredFormatter.prototype = {
@@ -480,16 +632,77 @@ StructuredFormatter.prototype = {
     if (!output._message && logMessage.message) {
       output._message = logMessage.message;
     }
 
     return JSON.stringify(output);
   }
 }
 
+/**
+ * Test an object to see if it is a Mozilla JS Error.
+ */
+function isError(aObj) {
+  return (aObj && typeof(aObj) == 'object' && "name" in aObj && "message" in aObj &&
+          "fileName" in aObj && "lineNumber" in aObj && "stack" in aObj);
+};
+
+/*
+ * Parameter Formatters
+ * These massage an object used as a parameter for a LogMessage into
+ * a string representation of the object.
+ */
+
+function ParameterFormatter() {
+  this._name = "ParameterFormatter"
+}
+ParameterFormatter.prototype = {
+  format: function(ob) {
+    try {
+      if (ob === undefined) {
+        return "undefined";
+      }
+      if (ob === null) {
+        return "null";
+      }
+      // Pass through primitive types and objects that unbox to primitive types.
+      if ((typeof(ob) != "object" || typeof(ob.valueOf()) != "object") &&
+          typeof(ob) != "function") {
+        return ob;
+      }
+      if (ob instanceof Ci.nsIException) {
+        return ob.toString() + " " + Log.stackTrace(ob);
+      }
+      else if (isError(ob)) {
+        return Log._formatError(ob);
+      }
+      // Just JSONify it. Filter out our internal fields and those the caller has
+      // already handled.
+      return JSON.stringify(ob, (key, val) => {
+        if (INTERNAL_FIELDS.has(key)) {
+          return undefined;
+        }
+        return val;
+      });
+    }
+    catch (e) {
+      dumpError("Exception trying to format object for log message: " + Log.exceptionStr(e));
+    }
+    // Fancy formatting failed. Just toSource() it - but even this may fail!
+    try {
+      return ob.toSource();
+    } catch (_) { }
+    try {
+      return "" + ob;
+    } catch (_) {
+      return "[object]"
+    }
+  }
+}
+
 /*
  * Appenders
  * These can be attached to Loggers to log to different places
  * Simply subclass and override doAppend to implement a new one
  */
 
 function Appender(formatter) {
   this._name = "Appender";
@@ -499,58 +712,66 @@ Appender.prototype = {
   level: Log.Level.All,
 
   append: function App_append(message) {
     if (message) {
       this.doAppend(this._formatter.format(message));
     }
   },
   toString: function App_toString() {
-    return this._name + " [level=" + this._level +
+    return this._name + " [level=" + this.level +
       ", formatter=" + this._formatter + "]";
   },
-  doAppend: function App_doAppend(message) {}
+  doAppend: function App_doAppend(formatted) {}
 };
 
 /*
  * DumpAppender
  * Logs to standard out
  */
 
 function DumpAppender(formatter) {
   Appender.call(this, formatter);
   this._name = "DumpAppender";
 }
 DumpAppender.prototype = {
   __proto__: Appender.prototype,
 
-  doAppend: function DApp_doAppend(message) {
-    dump(message);
+  doAppend: function DApp_doAppend(formatted) {
+    dump(formatted + "\n");
   }
 };
 
 /*
  * ConsoleAppender
  * Logs to the javascript console
  */
 
 function ConsoleAppender(formatter) {
   Appender.call(this, formatter);
   this._name = "ConsoleAppender";
 }
 ConsoleAppender.prototype = {
   __proto__: Appender.prototype,
 
-  doAppend: function CApp_doAppend(message) {
-    if (message.level > Log.Level.Warn) {
-      Cu.reportError(message);
-      return;
+  // XXX this should be replaced with calls to the Browser Console
+  append: function App_append(message) {
+    if (message) {
+      let m = this._formatter.format(message);
+      if (message.level > Log.Level.Warn) {
+        Cu.reportError(m);
+        return;
+      }
+      this.doAppend(m);
     }
+  },
+
+  doAppend: function CApp_doAppend(formatted) {
     Cc["@mozilla.org/consoleservice;1"].
-      getService(Ci.nsIConsoleService).logStringMessage(message);
+      getService(Ci.nsIConsoleService).logStringMessage(formatted);
   }
 };
 
 /**
  * Append to an nsIStorageStream
  *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
@@ -609,29 +830,29 @@ StorageStreamAppender.prototype = {
     if (!this._outputStream) {
       return;
     }
     this.outputStream.close();
     this._outputStream = null;
     this._ss = null;
   },
 
-  doAppend: function (message) {
-    if (!message) {
+  doAppend: function (formatted) {
+    if (!formatted) {
       return;
     }
     try {
-      this.outputStream.writeString(message);
+      this.outputStream.writeString(formatted + "\n");
     } catch(ex) {
       if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
         // The underlying output stream is closed, so let's open a new one
         // and try again.
         this._outputStream = null;
       } try {
-          this.outputStream.writeString(message);
+          this.outputStream.writeString(formatted + "\n");
       } catch (ex) {
         // Ah well, we tried, but something seems to be hosed permanently.
       }
     }
   }
 };
 
 /**
@@ -677,18 +898,18 @@ FileAppender.prototype = {
 
     return this._fileReadyPromise.then(_ => {
       if (!this._file) {
         return this._openFile();
       }
     });
   },
 
-  doAppend: function (message) {
-    let array = this._encoder.encode(message);
+  doAppend: function (formatted) {
+    let array = this._encoder.encode(formatted + "\n");
     if (this._file) {
       this._lastWritePromise = this._file.write(array);
     } else {
       this._lastWritePromise = this._getFile().then(_ => {
         this._fileReadyPromise = null;
         if (this._file) {
           return this._file.write(array);
         }
@@ -704,41 +925,41 @@ FileAppender.prototype = {
     });
   }
 };
 
 /**
  * Bounded File appender
  *
  * Writes output to file using OS.File. After the total message size
- * (as defined by message.length) exceeds maxSize, existing messages
+ * (as defined by formatted.length) exceeds maxSize, existing messages
  * will be discarded, and subsequent writes will be appended to a new log file.
  */
 function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
   FileAppender.call(this, path, formatter);
   this._name = "BoundedFileAppender";
   this._size = 0;
   this._maxSize = maxSize;
   this._closeFilePromise = null;
 }
 
 BoundedFileAppender.prototype = {
   __proto__: FileAppender.prototype,
 
-  doAppend: function (message) {
+  doAppend: function (formatted) {
     if (!this._removeFilePromise) {
       if (this._size < this._maxSize) {
-        this._size += message.length;
-        return FileAppender.prototype.doAppend.call(this, message);
+        this._size += formatted.length;
+        return FileAppender.prototype.doAppend.call(this, formatted);
       }
       this._removeFilePromise = this.reset();
     }
     this._removeFilePromise.then(_ => {
       this._removeFilePromise = null;
-      this.doAppend(message);
+      this.doAppend(formatted);
     });
   },
 
   reset: function () {
     let fileClosePromise;
     if (this._fileReadyPromise) {
       // An attempt to open the file may still be in progress.
       fileClosePromise = this._fileReadyPromise.then(_ => {
--- a/toolkit/modules/tests/xpcshell/test_Log.js
+++ b/toolkit/modules/tests/xpcshell/test_Log.js
@@ -7,17 +7,17 @@ Cu.import("resource://gre/modules/NetUti
 Cu.import("resource://gre/modules/osfile.jsm");
 
 Cu.import("resource://gre/modules/Log.jsm");
 
 let testFormatter = {
   format: function format(message) {
     return message.loggerName + "\t" +
       message.levelDesc + "\t" +
-      message.message + "\n";
+      message.message;
   }
 };
 
 function MockAppender(formatter) {
   Log.Appender.call(this, formatter);
   this.messages = [];
 }
 MockAppender.prototype = {
@@ -27,35 +27,33 @@ MockAppender.prototype = {
     this.messages.push(message);
   }
 };
 
 function run_test() {
   run_next_test();
 }
 
-add_test(function test_Logger() {
+add_task(function test_Logger() {
   let log = Log.repository.getLogger("test.logger");
   let appender = new MockAppender(new Log.BasicFormatter());
 
   log.level = Log.Level.Debug;
   appender.level = Log.Level.Info;
   log.addAppender(appender);
   log.info("info test");
   log.debug("this should be logged but not appended.");
 
   do_check_eq(appender.messages.length, 1);
 
   let msgRe = /\d+\ttest.logger\t\INFO\tinfo test/;
   do_check_true(msgRe.test(appender.messages[0]));
-
-  run_next_test();
 });
 
-add_test(function test_Logger_parent() {
+add_task(function test_Logger_parent() {
   // Check whether parenting is correct
   let grandparentLog = Log.repository.getLogger("grandparent");
   let childLog = Log.repository.getLogger("grandparent.parent.child");
   do_check_eq(childLog.parent.name, "grandparent");
 
   let parentLog = Log.repository.getLogger("grandparent.parent");
   do_check_eq(childLog.parent.name, "grandparent.parent");
 
@@ -63,44 +61,44 @@ add_test(function test_Logger_parent() {
   let gpAppender = new MockAppender(new Log.BasicFormatter());
   gpAppender.level = Log.Level.Info;
   grandparentLog.addAppender(gpAppender);
   childLog.info("child info test");
   Log.repository.rootLogger.info("this shouldn't show up in gpAppender");
 
   do_check_eq(gpAppender.messages.length, 1);
   do_check_true(gpAppender.messages[0].indexOf("child info test") > 0);
-
-  run_next_test();
 });
 
 add_test(function test_LoggerWithMessagePrefix() {
   let log = Log.repository.getLogger("test.logger.prefix");
   let appender = new MockAppender(new Log.MessageOnlyFormatter());
   log.addAppender(appender);
 
   let prefixed = Log.repository.getLoggerWithMessagePrefix(
     "test.logger.prefix", "prefix: ");
 
   log.warn("no prefix");
   prefixed.warn("with prefix");
 
   Assert.equal(appender.messages.length, 2, "2 messages were logged.");
   Assert.deepEqual(appender.messages, [
-    "no prefix\n",
-    "prefix: with prefix\n",
+    "no prefix",
+    "prefix: with prefix",
   ], "Prefix logger works.");
 
   run_next_test();
 });
 
-// A utility method for checking object equivalence.
-// Fields with a reqular expression value in expected will be tested
-// against the corresponding value in actual. Otherwise objects
-// are expected to have the same keys and equal values.
+/*
+ * A utility method for checking object equivalence.
+ * Fields with a reqular expression value in expected will be tested
+ * against the corresponding value in actual. Otherwise objects
+ * are expected to have the same keys and equal values.
+ */
 function checkObjects(expected, actual) {
   do_check_true(expected instanceof Object);
   do_check_true(actual instanceof Object);
   for (let key in expected) {
     do_check_neq(actual[key], undefined);
     if (expected[key] instanceof RegExp) {
       do_check_true(expected[key].test(actual[key].toString()));
     } else {
@@ -112,17 +110,17 @@ function checkObjects(expected, actual) 
     }
   }
 
   for (let key in actual) {
     do_check_neq(expected[key], undefined);
   }
 }
 
-add_test(function test_StructuredLogCommands() {
+add_task(function test_StructuredLogCommands() {
   let appender = new MockAppender(new Log.StructuredFormatter());
   let logger = Log.repository.getLogger("test.StructuredOutput");
   logger.addAppender(appender);
   logger.level = Log.Level.Info;
 
   logger.logStructured("test_message", {_message: "message string one"});
   logger.logStructured("test_message", {_message: "message string two",
                                         _level: "ERROR",
@@ -190,21 +188,19 @@ add_test(function test_StructuredLogComm
   logger.addAppender(appender);
   logger.level = Log.Level.All;
   logger.info("message string one", {action: "test_message"});
   logger.error("message string two", {action: "test_message",
                                       source_file: "test_Log.js"});
 
   checkObjects(messageOne, JSON.parse(appender.messages[0]));
   checkObjects(messageTwo, JSON.parse(appender.messages[1]));
-
-  run_next_test();
 });
 
-add_test(function test_StorageStreamAppender() {
+add_task(function test_StorageStreamAppender() {
   let appender = new Log.StorageStreamAppender(testFormatter);
   do_check_eq(appender.getInputStream(), null);
 
   // Log to the storage stream and verify the log was written and can be
   // read back.
   let logger = Log.repository.getLogger("test.StorageStreamAppender");
   logger.addAppender(appender);
   logger.info("OHAI");
@@ -222,18 +218,16 @@ add_test(function test_StorageStreamAppe
   // Reset the appender and log some more.
   appender.reset();
   do_check_eq(appender.getInputStream(), null);
   logger.debug("wut?!?");
   inputStream = appender.getInputStream();
   data = NetUtil.readInputStreamToString(inputStream,
                                          inputStream.available());
   do_check_eq(data, "test.StorageStreamAppender\tDEBUG\twut?!?\n");
-
-  run_next_test();
 });
 
 function fileContents(path) {
   let decoder = new TextDecoder();
   return OS.File.read(path).then(array => {
     return decoder.decode(array);
   });
 }
@@ -330,8 +324,249 @@ add_task(function test_BoundedFileAppend
   yield appender._lastWritePromise;
 
   do_check_eq((yield fileContents(path)),
               "test.BoundedFileAppender\tINFO\tTHREE\n" +
               "test.BoundedFileAppender\tINFO\tFOUR\n");
 
 });
 
+/*
+ * Test parameter formatting.
+ */
+add_task(function log_message_with_params() {
+  let formatter = new Log.BasicFormatter();
+
+  function formatMessage(text, params) {
+    let full = formatter.format(new Log.LogMessage("test.logger", Log.Level.Warn, text, params));
+    return full.split("\t")[3];
+  }
+
+  // Strings are substituted directly.
+  do_check_eq(formatMessage("String is ${foo}", {foo: "bar"}),
+              "String is bar");
+
+  // Numbers are substituted.
+  do_check_eq(formatMessage("Number is ${number}", {number: 47}),
+              "Number is 47")
+
+  // The entire params object is JSON-formatted and substituted.
+  do_check_eq(formatMessage("Object is ${}", {foo: "bar"}),
+              'Object is {"foo":"bar"}');
+
+  // An object nested inside params is JSON-formatted and substituted.
+  do_check_eq(formatMessage("Sub object is ${sub}", {sub: {foo: "bar"}}),
+                'Sub object is {"foo":"bar"}');
+
+  // The substitution field is missing from params. Leave the placeholder behind
+  // to make the mistake obvious.
+  do_check_eq(formatMessage("Missing object is ${missing}", {}),
+              'Missing object is ${missing}');
+
+  // Make sure we don't treat the parameter name 'false' as a falsey value.
+  do_check_eq(formatMessage("False is ${false}", {false: true}),
+              'False is true');
+
+  // If an object has a .toJSON method, the formatter uses it.
+  let ob = function() {};
+  ob.toJSON = function() {return {sneaky: "value"}};
+  do_check_eq(formatMessage("JSON is ${sub}", {sub: ob}),
+              'JSON is {"sneaky":"value"}');
+
+  // Fall back to .toSource() if JSON.stringify() fails on an object.
+  let ob = function() {};
+  ob.toJSON = function() {throw "oh noes JSON"};
+  do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+              'Fail is (function () {})');
+
+  // Fall back to .toString if both .toJSON and .toSource fail.
+  ob.toSource = function() {throw "oh noes SOURCE"};
+  do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+              'Fail is function () {}');
+
+  // Fall back to '[object]' if .toJSON, .toSource and .toString fail.
+  ob.toString = function() {throw "oh noes STRING"};
+  do_check_eq(formatMessage("Fail is ${sub}", {sub: ob}),
+              'Fail is [object]');
+
+  // If params are passed but there are no substitution in the text
+  // we JSON format and append the entire parameters object.
+  do_check_eq(formatMessage("Text with no subs", {a: "b", c: "d"}),
+              'Text with no subs: {"a":"b","c":"d"}');
+
+  // If we substitute one parameter but not the other,
+  // we ignore any params that aren't substituted.
+  do_check_eq(formatMessage("Text with partial sub ${a}", {a: "b", c: "d"}),
+              'Text with partial sub b');
+
+  // We don't format internal fields stored in params.
+  do_check_eq(formatMessage("Params with _ ${}", {a: "b", _c: "d", _level:20, _message:"froo",
+                                                  _time:123456, _namespace:"here.there"}),
+              'Params with _ {"a":"b","_c":"d"}');
+
+  // Don't print an empty params holder if all params are internal.
+  do_check_eq(formatMessage("All params internal", {_level:20, _message:"froo",
+                                                    _time:123456, _namespace:"here.there"}),
+              'All params internal');
+
+  // Format params with null and undefined values.
+  do_check_eq(formatMessage("Null ${n} undefined ${u}", {n: null, u: undefined}),
+              'Null null undefined undefined');
+
+  // Format params with number, bool, and Object/String type.
+  do_check_eq(formatMessage("number ${n} boolean ${b} boxed Boolean ${bx} String ${s}",
+                            {n: 45, b: false, bx: new Boolean(true), s: new String("whatevs")}),
+              'number 45 boolean false boxed Boolean true String whatevs');
+
+  /*
+   * Check that errors get special formatting if they're formatted directly as
+   * a named param or they're the only param, but not if they're a field in a
+   * larger structure.
+   */
+  let err = Components.Exception("test exception", Components.results.NS_ERROR_FAILURE);
+  let str = formatMessage("Exception is ${}", err);
+  do_check_true(str.contains('Exception is [Exception... "test exception"'));
+  do_check_true(str.contains("(NS_ERROR_FAILURE)"));
+  let str = formatMessage("Exception is", err);
+  do_check_true(str.contains('Exception is: [Exception... "test exception"'));
+  let str = formatMessage("Exception is ${error}", {error: err});
+  do_check_true(str.contains('Exception is [Exception... "test exception"'));
+  let str = formatMessage("Exception is", {_error: err});
+  do_print(str);
+  // Exceptions buried inside objects are formatted badly.
+  do_check_true(str.contains('Exception is: {"_error":{}'));
+  // If the message text is null, the message contains only the formatted params object.
+  let str = formatMessage(null, err);
+  do_check_true(str.startsWith('[Exception... "test exception"'));
+  // If the text is null and 'params' is a String object, the message is exactly that string.
+  let str = formatMessage(null, new String("String in place of params"));
+  do_check_eq(str, "String in place of params");
+
+  // We use object.valueOf() internally; make sure a broken valueOf() method
+  // doesn't cause the logger to fail.
+  let vOf = {a: 1, valueOf: function() {throw "oh noes valueOf"}};
+  do_check_eq(formatMessage("Broken valueOf ${}", vOf),
+              'Broken valueOf ({a:1, valueOf:(function () {throw "oh noes valueOf"})})');
+
+  // Test edge cases of bad data to formatter:
+  // If 'params' is not an object, format it as a basic type.
+  do_check_eq(formatMessage("non-object no subst", 1),
+              'non-object no subst: 1');
+  do_check_eq(formatMessage("non-object all subst ${}", 2),
+              'non-object all subst 2');
+  // If 'params' is not an object, no named substitutions can succeed;
+  // therefore we leave the placeholder and append the formatted params.
+  do_check_eq(formatMessage("non-object named subst ${junk} space", 3),
+              'non-object named subst ${junk} space: 3');
+  // If there are no params, we leave behind the placeholders in the text.
+  do_check_eq(formatMessage("no params ${missing}", undefined),
+              'no params ${missing}');
+  // If params doesn't contain any of the tags requested in the text,
+  // we leave them all behind and append the formatted params.
+  do_check_eq(formatMessage("object missing tag ${missing} space", {mising: "not here"}),
+              'object missing tag ${missing} space: {"mising":"not here"}');
+  // If we are given null text and no params, the resulting formatted message is empty.
+  do_check_eq(formatMessage(null), '');
+});
+
+/*
+ * If we call a log function with a non-string object in place of the text
+ * argument, and no parameters, treat that the same as logging empty text
+ * with the object argument as parameters. This makes the log useful when the
+ * caller does "catch(err) {logger.error(err)}"
+ */
+add_task(function test_log_err_only() {
+  let log = Log.repository.getLogger("error.only");
+  let testFormatter = { format: msg => msg };
+  let appender = new MockAppender(testFormatter);
+  log.addAppender(appender);
+
+  /*
+   * Check that log.error(err) is treated the same as
+   * log.error(null, err) by the logMessage constructor; the formatMessage()
+   * tests above ensure that the combination of null text and an error object
+   * is formatted correctly.
+   */
+  try {
+    eval("javascript syntax error");
+  }
+  catch (e) {
+    log.error(e);
+    msg = appender.messages.pop();
+    do_check_eq(msg.message, null);
+    do_check_eq(msg.params, e);
+  }
+});
+
+/*
+ * Test logStructured() messages through basic formatter.
+ */
+add_task(function test_structured_basic() {
+  let log = Log.repository.getLogger("test.logger");
+  let appender = new MockAppender(new Log.BasicFormatter());
+
+  log.level = Log.Level.Info;
+  appender.level = Log.Level.Info;
+  log.addAppender(appender);
+
+  // A structured entry with no _message is treated the same as log./level/(null, params)
+  // except the 'action' field is added to the object.
+  log.logStructured("action", {data: "structure"});
+  do_check_eq(appender.messages.length, 1);
+  do_check_true(appender.messages[0].contains('{"data":"structure","action":"action"}'));
+
+  // A structured entry with _message and substitution is treated the same as
+  // log./level/(null, params).
+  log.logStructured("action", {_message: "Structured sub ${data}", data: "structure"});
+  do_check_eq(appender.messages.length, 2);
+  do_print(appender.messages[1]);
+  do_check_true(appender.messages[1].contains('Structured sub structure'));
+});
+
+/*
+ * Test that all the basic logger methods pass the message and params through to the appender.
+ */
+add_task(function log_message_with_params() {
+  let log = Log.repository.getLogger("error.logger");
+  let testFormatter = { format: msg => msg };
+  let appender = new MockAppender(testFormatter);
+  log.addAppender(appender);
+
+  let testParams = {a:1, b:2};
+  log.fatal("Test fatal", testParams);
+  log.error("Test error", testParams);
+  log.warn("Test warn", testParams);
+  log.info("Test info", testParams);
+  log.config("Test config", testParams);
+  log.debug("Test debug", testParams);
+  log.trace("Test trace", testParams);
+  do_check_eq(appender.messages.length, 7);
+  for (let msg of appender.messages) {
+    do_check_true(msg.params === testParams);
+    do_check_true(msg.message.startsWith("Test "));
+  }
+});
+
+/*
+ * Check that we format JS Errors reasonably.
+ */
+add_task(function format_errors() {
+  let pFormat = new Log.ParameterFormatter();
+
+  // Test that subclasses of Error are recognized as errors.
+  err = new ReferenceError("Ref Error", "ERROR_FILE", 28);
+  str = pFormat.format(err);
+  do_check_true(str.contains("ReferenceError"));
+  do_check_true(str.contains("ERROR_FILE:28"));
+  do_check_true(str.contains("Ref Error"));
+
+  // Test that JS-generated Errors are recognized and formatted.
+  try {
+    eval("javascript syntax error");
+  }
+  catch (e) {
+    str = pFormat.format(e);
+    do_check_true(str.contains("SyntaxError: missing ;"));
+    // Make sure we identified it as an Error and formatted the error location as
+    // lineNumber:columnNumber.
+    do_check_true(str.contains(":1:11)"));
+  }
+});
--- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -8,28 +8,29 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
-                                  "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
                                   "resource://gre/modules/DeferredSave.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
                                   "resource://gre/modules/addons/AddonRepository_SQLiteMigrator.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+                                  "resource://gre/modules/Task.jsm");
+
 
 this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
 
 const PREF_GETADDONS_CACHE_ENABLED       = "extensions.getAddons.cache.enabled";
 const PREF_GETADDONS_CACHE_TYPES         = "extensions.getAddons.cache.types";
 const PREF_GETADDONS_CACHE_ID_ENABLED    = "extensions.%ID%.getAddons.cache.enabled"
 const PREF_GETADDONS_BROWSEADDONS        = "extensions.getAddons.browseAddons";
 const PREF_GETADDONS_BYIDS               = "extensions.getAddons.get.url";
@@ -45,17 +46,16 @@ const XMLURI_PARSE_ERROR  = "http://www.
 const API_VERSION = "1.5";
 const DEFAULT_CACHE_TYPES = "extension,theme,locale,dictionary";
 
 const KEY_PROFILEDIR        = "ProfD";
 const FILE_DATABASE         = "addons.json";
 const DB_SCHEMA             = 5;
 const DB_MIN_JSON_SCHEMA    = 5;
 const DB_BATCH_TIMEOUT_MS   = 50;
-const DB_DATA_WRITTEN_TOPIC = "addon-repository-data-written"
 
 const BLANK_DB = function() {
   return {
     addons: new Map(),
     schema: DB_SCHEMA
   };
 }
 
@@ -491,22 +491,16 @@ this.AddonRepository = {
   },
 
   // A cache of the add-ons stored in the database
   _addons: null,
 
   // An array of callbacks pending the retrieval of add-ons from AddonDatabase
   _pendingCallbacks: null,
 
-  // Whether a migration in currently in progress
-  _migrationInProgress: false,
-
-  // A callback to be called when migration finishes
-  _postMigrationCallback: null,
-
   // Whether a search is currently in progress
   _searching: false,
 
   // XHR associated with the current request
   _request: null,
 
   /*
    * Addon search results callback object that contains two functions
@@ -548,58 +542,58 @@ this.AddonRepository = {
    * add-on is not found) is passed to the specified callback. If caching is
    * disabled, null is passed to the specified callback.
    *
    * @param  aId
    *         The id of the add-on to get
    * @param  aCallback
    *         The callback to pass the result back to
    */
-  getCachedAddonByID: function AddonRepo_getCachedAddonByID(aId, aCallback) {
+  getCachedAddonByID: Task.async(function* (aId, aCallback) {
     if (!aId || !this.cacheEnabled) {
       aCallback(null);
       return;
     }
 
     let self = this;
     function getAddon(aAddons) {
       aCallback((aId in aAddons) ? aAddons[aId] : null);
     }
 
     if (this._addons == null) {
       if (this._pendingCallbacks == null) {
         // Data has not been retrieved from the database, so retrieve it
         this._pendingCallbacks = [];
         this._pendingCallbacks.push(getAddon);
-        AddonDatabase.retrieveStoredData(function getCachedAddonByID_retrieveData(aAddons) {
-          let pendingCallbacks = self._pendingCallbacks;
 
-          // Check if cache was shutdown or deleted before callback was called
-          if (pendingCallbacks == null)
-            return;
+        let addons = yield AddonDatabase.retrieveStoredData();
+        let pendingCallbacks = self._pendingCallbacks;
 
-          // Callbacks may want to trigger a other caching operations that may
-          // affect _addons and _pendingCallbacks, so set to final values early
-          self._pendingCallbacks = null;
-          self._addons = aAddons;
+        // Check if cache was shutdown or deleted before callback was called
+        if (pendingCallbacks == null)
+          return;
 
-          pendingCallbacks.forEach(function(aCallback) aCallback(aAddons));
-        });
+        // Callbacks may want to trigger a other caching operations that may
+        // affect _addons and _pendingCallbacks, so set to final values early
+        self._pendingCallbacks = null;
+        self._addons = addons;
+
+        pendingCallbacks.forEach(function(aCallback) aCallback(addons));
 
         return;
       }
 
       // Data is being retrieved from the database, so wait
       this._pendingCallbacks.push(getAddon);
       return;
     }
 
     // Data has been retrieved, so immediately return result
     getAddon(this._addons);
-  },
+  }),
 
   /**
    * Asynchronously repopulate cache so it only contains the add-ons
    * corresponding to the specified ids. If caching is disabled,
    * the cache is completely removed.
    *
    * @param  aIds
    *         The array of add-on ids to repopulate the cache with
@@ -1425,16 +1419,17 @@ this.AddonRepository = {
     this._request.overrideMimeType("text/xml");
     if (aTimeout) {
       this._request.timeout = aTimeout;
     }
 
     this._request.addEventListener("error", aEvent => this._reportFailure(), false);
     this._request.addEventListener("timeout", aEvent => this._reportFailure(), false);
     this._request.addEventListener("load", aEvent => {
+      logger.debug("Got metadata search load event");
       let request = aEvent.target;
       let responseXML = request.responseXML;
 
       if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
           (request.status != 200 && request.status != 0)) {
         this._reportFailure();
         return;
       }
@@ -1513,129 +1508,114 @@ this.AddonRepository = {
       if (Services.vc.compare(override.minVersion, aAddonVersion) <= 0 &&
           Services.vc.compare(aAddonVersion, override.maxVersion) <= 0 &&
           Services.vc.compare(override.appMinVersion, appVersion) <= 0 &&
           Services.vc.compare(appVersion, override.appMaxVersion) <= 0) {
         return override;
       }
     }
     return null;
+  },
+
+  flush: function() {
+    return AddonDatabase.flush();
   }
-
 };
 
 var AddonDatabase = {
-  // true if the database connection has been opened
-  initialized: false,
-  // false if there was an unrecoverable error openning the database
+  // false if there was an unrecoverable error opening the database
   databaseOk: true,
 
+  connectionPromise: null,
   // the in-memory database
   DB: BLANK_DB(),
 
   /**
-   * A getter to retrieve an nsIFile pointer to the DB
+   * A getter to retrieve the path to the DB
    */
   get jsonFile() {
-    delete this.jsonFile;
-    return this.jsonFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
-  },
+    return OS.Path.join(OS.Constants.Path.profileDir, FILE_DATABASE);
+ },
 
   /**
-   * Synchronously opens a new connection to the database file.
+   * Asynchronously opens a new connection to the database file.
+   *
+   * @return {Promise} a promise that resolves to the database.
    */
   openConnection: function() {
-    this.DB = BLANK_DB();
-    this.initialized = true;
-    delete this.connection;
+    if (!this.connectionPromise) {
+     this.connectionPromise = Task.spawn(function*() {
+       this.DB = BLANK_DB();
 
-    let inputDB, fstream, cstream, schema;
+       let inputDB, schema;
 
-    try {
-     let data = "";
-     fstream = Cc["@mozilla.org/network/file-input-stream;1"]
-                 .createInstance(Ci.nsIFileInputStream);
-     cstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
-                 .createInstance(Ci.nsIConverterInputStream);
+       try {
+         let data = yield OS.File.read(this.jsonFile, { encoding: "utf-8"})
+         inputDB = JSON.parse(data);
+
+         if (!inputDB.hasOwnProperty("addons") ||
+             !Array.isArray(inputDB.addons)) {
+           throw new Error("No addons array.");
+         }
 
-     fstream.init(this.jsonFile, -1, 0, 0);
-     cstream.init(fstream, "UTF-8", 0, 0);
-     let (str = {}) {
-       let read = 0;
-       do {
-         read = cstream.readString(0xffffffff, str); // read as much as we can and put it in str.value
-         data += str.value;
-       } while (read != 0);
-     }
+         if (!inputDB.hasOwnProperty("schema")) {
+           throw new Error("No schema specified.");
+         }
+
+         schema = parseInt(inputDB.schema, 10);
 
-     inputDB = JSON.parse(data);
+         if (!Number.isInteger(schema) ||
+             schema < DB_MIN_JSON_SCHEMA) {
+           throw new Error("Invalid schema value.");
+         }
+       } catch (e if e instanceof OS.File.Error && e.becauseNoSuchFile) {
+         logger.debug("No " + FILE_DATABASE + " found.");
 
-     if (!inputDB.hasOwnProperty("addons") ||
-         !Array.isArray(inputDB.addons)) {
-       throw new Error("No addons array.");
-     }
-
-     if (!inputDB.hasOwnProperty("schema")) {
-       throw new Error("No schema specified.");
-     }
-
-     schema = parseInt(inputDB.schema, 10);
+         // Create a blank addons.json file
+         this._saveDBToDisk();
 
-     if (!Number.isInteger(schema) ||
-         schema < DB_MIN_JSON_SCHEMA) {
-       throw new Error("Invalid schema value.");
-     }
-
-    } catch (e if e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
-      logger.debug("No " + FILE_DATABASE + " found.");
+         let dbSchema = 0;
+         try {
+           dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
+         } catch (e) {}
 
-      // Create a blank addons.json file
-      this._saveDBToDisk();
+         if (dbSchema < DB_MIN_JSON_SCHEMA) {
+           let results = yield new Promise((resolve, reject) => {
+             AddonRepository_SQLiteMigrator.migrate(resolve);
+           });
 
-      let dbSchema = 0;
-      try {
-        dbSchema = Services.prefs.getIntPref(PREF_GETADDONS_DB_SCHEMA);
-      } catch (e) {}
+           if (results.length) {
+             yield this._insertAddons(results);
+           }
 
-      if (dbSchema < DB_MIN_JSON_SCHEMA) {
-        this._migrationInProgress = AddonRepository_SQLiteMigrator.migrate((results) => {
-          if (results.length)
-            this.insertAddons(results);
+           Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
+         }
 
-          if (this._postMigrationCallback) {
-            this._postMigrationCallback();
-            this._postMigrationCallback = null;
-          }
+         return this.DB;
+       } catch (e) {
+         logger.error("Malformed " + FILE_DATABASE + ": " + e);
+         this.databaseOk = false;
 
-          this._migrationInProgress = false;
-        });
+         return this.DB;
+       }
 
-        Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
-      }
+       Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
 
-      return;
+       // We use _insertAddon manually instead of calling
+       // insertAddons to avoid the write to disk which would
+       // be a waste since this is the data that was just read.
+       for (let addon of inputDB.addons) {
+         this._insertAddon(addon);
+       }
 
-    } catch (e) {
-      logger.error("Malformed " + FILE_DATABASE + ": " + e);
-      this.databaseOk = false;
-      return;
-
-    } finally {
-     cstream.close();
-     fstream.close();
+       return this.DB;
+     }.bind(this));
     }
 
-    Services.prefs.setIntPref(PREF_GETADDONS_DB_SCHEMA, DB_SCHEMA);
-
-    // We use _insertAddon manually instead of calling
-    // insertAddons to avoid the write to disk which would
-    // be a waste since this is the data that was just read.
-    for (let addon of inputDB.addons) {
-      this._insertAddon(addon);
-    }
+    return this.connectionPromise;
   },
 
   /**
    * A lazy getter for the database connection.
    */
   get connection() {
     return this.openConnection();
   },
@@ -1648,50 +1628,47 @@ var AddonDatabase = {
    *         An optional callback to call once complete
    * @param  aSkipFlush
    *         An optional boolean to skip flushing data to disk. Useful
    *         when the database is going to be deleted afterwards.
    */
   shutdown: function AD_shutdown(aSkipFlush) {
     this.databaseOk = true;
 
-    if (!this.initialized) {
-      return Promise.resolve(0);
+    if (!this.connectionPromise) {
+      return Promise.resolve();
     }
 
-    this.initialized = false;
-
-    this.__defineGetter__("connection", function shutdown_connectionGetter() {
-      return this.openConnection();
-    });
+    this.connectionPromise = null;
 
     if (aSkipFlush) {
-      return Promise.resolve(0);
+      return Promise.resolve();
     } else {
       return this.Writer.flush();
     }
   },
 
   /**
    * Asynchronously deletes the database, shutting down the connection
    * first if initialized
    *
    * @param  aCallback
    *         An optional callback to call once complete
    */
   delete: function AD_delete(aCallback) {
     this.DB = BLANK_DB();
 
-    this.Writer.flush()
+    this._deleting = this.Writer.flush()
       .then(null, () => {})
       // shutdown(true) never rejects
       .then(() => this.shutdown(true))
-      .then(() => OS.File.remove(this.jsonFile.path, {}))
+      .then(() => OS.File.remove(this.jsonFile, {}))
       .then(null, error => logger.error("Unable to delete Addon Repository file " +
-                                 this.jsonFile.path, error))
+                                 this.jsonFile, error))
+      .then(() => this._deleting = null)
       .then(aCallback);
   },
 
   toJSON: function AD_toJSON() {
     let json = {
       schema: this.DB.schema,
       addons: []
     }
@@ -1705,47 +1682,53 @@ var AddonDatabase = {
   /*
    * This is a deferred task writer that is used
    * to batch operations done within 50ms of each
    * other and thus generating only one write to disk
    */
   get Writer() {
     delete this.Writer;
     this.Writer = new DeferredSave(
-      this.jsonFile.path,
+      this.jsonFile,
       () => { return JSON.stringify(this); },
       DB_BATCH_TIMEOUT_MS
     );
     return this.Writer;
   },
 
   /**
+   * Flush any pending I/O on the addons.json file
+   * @return: Promise{null}
+   *          Resolves when the pending I/O (writing out or deleting
+   *          addons.json) completes
+   */
+  flush: function() {
+    if (this._deleting) {
+      return this._deleting;
+    }
+    return this.Writer.flush();
+  },
+
+  /**
    * Asynchronously retrieve all add-ons from the database, and pass it
    * to the specified callback
    *
    * @param  aCallback
    *         The callback to pass the add-ons back to
    */
-  retrieveStoredData: function AD_retrieveStoredData(aCallback) {
-    if (!this.initialized)
-      this.openConnection();
-
-    let gatherResults = () => {
-      let result = {};
-      for (let [key, value] of this.DB.addons)
-        result[key] = value;
+  retrieveStoredData: Task.async(function* (){
+    let db = yield this.openConnection();
+    let result = {};
 
-      executeSoon(function() aCallback(result));
-    };
+    for (let [key, value] of db.addons) {
+      result[key] = value;
+    }
 
-    if (this._migrationInProgress)
-      this._postMigrationCallback = gatherResults;
-    else
-      gatherResults();
-  },
+    return result;
+  }),
 
   /**
    * Asynchronously repopulates the database so it only contains the
    * specified add-ons
    *
    * @param  aAddons
    *         The array of add-ons to repopulate the database with
    * @param  aCallback
@@ -1759,29 +1742,29 @@ var AddonDatabase = {
   /**
    * Asynchronously inserts an array of add-ons into the database
    *
    * @param  aAddons
    *         The array of add-ons to insert
    * @param  aCallback
    *         An optional callback to call once complete
    */
-  insertAddons: function AD_insertAddons(aAddons, aCallback) {
-    if (!this.initialized)
-      this.openConnection();
+  insertAddons: Task.async(function* (aAddons, aCallback) {
+    yield this.openConnection();
+    yield this._insertAddons(aAddons, aCallback);
+  }),
 
+  _insertAddons: Task.async(function* (aAddons, aCallback) {
     for (let addon of aAddons) {
       this._insertAddon(addon);
     }
 
-    this._saveDBToDisk();
-
-    if (aCallback)
-      executeSoon(aCallback);
-  },
+    yield this._saveDBToDisk();
+    aCallback && aCallback();
+  }),
 
   /**
    * Inserts an individual add-on into the database. If the add-on already
    * exists in the database (by id), then the specified add-on will not be
    * inserted.
    *
    * @param  aAddon
    *         The add-on to insert into the database
@@ -1914,18 +1897,18 @@ var AddonDatabase = {
    * Write the in-memory DB to disk, after waiting for
    * the DB_BATCH_TIMEOUT_MS timeout.
    *
    * @return Promise A promise that resolves after the
    *                 write to disk has completed.
    */
   _saveDBToDisk: function() {
     return this.Writer.saveChanges().then(
-      function() Services.obs.notifyObservers(null, DB_DATA_WRITTEN_TOPIC, null),
-      logger.error);
+      null,
+      e => logger.error("SaveDBToDisk failed", e));
   },
 
   /**
    * Make a developer object from a vanilla
    * JS object from the JSON database
    *
    * @param  aObj
    *         The JS object to use
@@ -1975,12 +1958,8 @@ var AddonDatabase = {
     return new AddonManagerPrivate.AddonCompatibilityOverride(type,
                                                               minVersion,
                                                               maxVersion,
                                                               appID,
                                                               appMinVersion,
                                                               appMaxVersion);
   },
 };
-
-function executeSoon(aCallback) {
-  Services.tm.mainThread.dispatch(aCallback, Ci.nsIThread.DISPATCH_NORMAL);
-}
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -394,69 +394,95 @@ function startupManager(aAppChanged) {
 
   gInternalManager.observe(null, "addons-startup", null);
 
   // Load the add-ons list as it was after extension registration
   loadAddonsList();
 }
 
 /**
+ * Helper to spin the event loop until a promise resolves or rejects
+ */
+function loopUntilPromise(aPromise) {
+  let done = false;
+  aPromise.then(
+    () => done = true,
+    err => {
+      do_report_unexpected_exception(err);
+      done = true;
+    });
+
+  let thr = Services.tm.mainThread;
+
+  while (!done) {
+    thr.processNextEvent(true);
+  }
+}
+
+/**
  * Restarts the add-on manager as if the host application was restarted.
  *
  * @param  aNewVersion
  *         An optional new version to use for the application. Passing this
  *         will change nsIXULAppInfo.version and make the startup appear as if
  *         the application version has changed.
  */
 function restartManager(aNewVersion) {
-  shutdownManager();
-  if (aNewVersion) {
-    gAppInfo.version = aNewVersion;
-    startupManager(true);
-  }
-  else {
-    startupManager(false);
-  }
+  loopUntilPromise(promiseRestartManager(aNewVersion));
+}
+
+function promiseRestartManager(aNewVersion) {
+  return promiseShutdownManager()
+    .then(null, err => do_report_unexpected_exception(err))
+    .then(() => {
+      if (aNewVersion) {
+        gAppInfo.version = aNewVersion;
+        startupManager(true);
+      }
+      else {
+        startupManager(false);
+      }
+    });
 }
 
 function shutdownManager() {
-  if (!gInternalManager)
-    return;
-
-  let shutdownDone = false;
+  loopUntilPromise(promiseShutdownManager());
+}
 
-  Services.obs.notifyObservers(null, "quit-application-granted", null);
-  MockAsyncShutdown.hook().then(
-    () => shutdownDone = true,
-    err => shutdownDone = true);
-
-  let thr = Services.tm.mainThread;
-
-  // Wait until we observe the shutdown notifications
-  while (!shutdownDone) {
-    thr.processNextEvent(true);
+function promiseShutdownManager() {
+  if (!gInternalManager) {
+    return Promise.resolve(false);
   }
 
-  gInternalManager = null;
+  let hookErr = null;
+  Services.obs.notifyObservers(null, "quit-application-granted", null);
+  return MockAsyncShutdown.hook()
+    .then(null, err => hookErr = err)
+    .then( () => {
+      gInternalManager = null;
 
-  // Load the add-ons list as it was after application shutdown
-  loadAddonsList();
+      // Load the add-ons list as it was after application shutdown
+      loadAddonsList();
 
-  // Clear any crash report annotations
-  gAppInfo.annotations = {};
+      // Clear any crash report annotations
+      gAppInfo.annotations = {};
 
-  // Force the XPIProvider provider to reload to better
-  // simulate real-world usage.
-  let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
-  // This would be cleaner if I could get it as the rejection reason from
-  // the AddonManagerInternal.shutdown() promise
-  gXPISaveError = XPIscope.XPIProvider._shutdownError;
-  do_print("gXPISaveError set to: " + gXPISaveError);
-  AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
-  Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
+      // Force the XPIProvider provider to reload to better
+      // simulate real-world usage.
+      let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+      // This would be cleaner if I could get it as the rejection reason from
+      // the AddonManagerInternal.shutdown() promise
+      gXPISaveError = XPIscope.XPIProvider._shutdownError;
+      do_print("gXPISaveError set to: " + gXPISaveError);
+      AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
+      Components.utils.unload("resource://gre/modules/addons/XPIProvider.jsm");
+      if (hookErr) {
+        throw hookErr;
+      }
+    });
 }
 
 function loadAddonsList() {
   function readDirectories(aSection) {
     var dirs = [];
     var keys = parser.getKeys(aSection);
     while (keys.hasMore()) {
       let descriptor = parser.getString(aSection, keys.getNext());
--- a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js
@@ -400,41 +400,35 @@ const WITH_EXTENSION_CACHE = [{
   screenshots:            [{ get url () { return get_subfile_uri(ADDON_IDS[2], "preview.png"); } }],
   sourceURI:              NetUtil.newURI(ADDON_FILES[2]).spec
 }];
 
 
 /*
  * Trigger an AddonManager background update check
  *
- * @param  aCallback
- *         Callback to call once the background update is complete
+ * @return Promise{null}
+ *         Resolves when the background update notification is received
  */
-function trigger_background_update(aCallback) {
-  Services.obs.addObserver({
-    observe: function(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(this, "addons-background-update-complete");
-      do_execute_soon(aCallback);
-    }
-  }, "addons-background-update-complete", false);
+function trigger_background_update() {
+  return new Promise((resolve, reject) => {
+    Services.obs.addObserver({
+      observe: function(aSubject, aTopic, aData) {
+        do_print("Observed " + aTopic);
+        Services.obs.removeObserver(this, "addons-background-update-complete");
+        resolve();
+      }
+    }, "addons-background-update-complete", false);
 
-  gInternalManager.notify(null);
+    gInternalManager.notify(null);
+  });
 }
 
-/*
- * Check whether or not the add-ons database exists
- *
- * @param  aExpectedExists
- *         Whether or not the database is expected to exist
- */
-function check_database_exists(aExpectedExists) {
-  let file = gProfD.clone();
-  file.append(FILE_DATABASE);
-  do_check_eq(file.exists(), aExpectedExists);
-}
+let gDBFile = gProfD.clone();
+gDBFile.append(FILE_DATABASE);
 
 /*
  * Check the actual add-on results against the expected add-on results
  *
  * @param  aActualAddons
  *         The array of actual add-ons to check
  * @param  aExpectedAddons
  *         The array of expected add-ons to check against
@@ -466,307 +460,276 @@ function check_results(aActualAddons, aE
  *
  * @param  aExpectedToFind
  *         An array of booleans representing which REPOSITORY_ADDONS are
  *         expected to be found in the cache
  * @param  aExpectedImmediately
  *         A boolean representing if results from the cache are expected
  *         immediately. Results are not immediate if the cache has not been
  *         initialized yet.
- * @param  aCallback
- *         A callback to call once the checks are complete
+ * @return Promise{null}
+ *         Resolves once the checks are complete
  */
-function check_cache(aExpectedToFind, aExpectedImmediately, aCallback) {
+function check_cache(aExpectedToFind, aExpectedImmediately) {
   do_check_eq(aExpectedToFind.length, REPOSITORY_ADDONS.length);
 
-  let pendingAddons = REPOSITORY_ADDONS.length;
-  let immediatelyFound = true;
-
-  for (let i = 0; i < REPOSITORY_ADDONS.length; i++) {
-    let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null;
-    AddonRepository.getCachedAddonByID(REPOSITORY_ADDONS[i].id, function(aAddon) {
-      do_check_eq(immediatelyFound, aExpectedImmediately);
+  let lookups = [];
 
-      if (expected == null)
-        do_check_eq(aAddon, null);
-      else
-        check_results([aAddon], [expected], true);
-
-      if (--pendingAddons == 0)
-        do_execute_soon(aCallback);
-    });
+  for (let i = 0 ; i < REPOSITORY_ADDONS.length ; i++) {
+    lookups.push(new Promise((resolve, reject) => {
+      let immediatelyFound = true;
+      let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null;
+      // can't Promise-wrap this because we're also testing whether the callback is
+      // sync or async
+      AddonRepository.getCachedAddonByID(REPOSITORY_ADDONS[i].id, function(aAddon) {
+        do_check_eq(immediatelyFound, aExpectedImmediately);
+        if (expected == null)
+          do_check_eq(aAddon, null);
+        else
+          check_results([aAddon], [expected], true);
+        resolve();
+      });
+      immediatelyFound = false;
+    }));
   }
-
-  immediatelyFound = false;
+  return Promise.all(lookups);
 }
 
 /*
- * Check an initialized cache by checking the cache, then restarting the
+ * Task to check an initialized cache by checking the cache, then restarting the
  * manager, and checking the cache. This checks that the cache is consistent
  * across manager restarts.
  *
  * @param  aExpectedToFind
  *         An array of booleans representing which REPOSITORY_ADDONS are
  *         expected to be found in the cache
- * @param  aCallback
- *         A callback to call once the checks are complete
  */
-function check_initialized_cache(aExpectedToFind, aCallback) {
-  check_cache(aExpectedToFind, true, function restart_initialized_cache() {
-    restartManager();
+function* check_initialized_cache(aExpectedToFind) {
+  yield check_cache(aExpectedToFind, true);
+  yield promiseRestartManager();
 
-    // If cache is disabled, then expect results immediately
-    let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED);
-    check_cache(aExpectedToFind, !cacheEnabled, aCallback);
-  });
-}
-
-// Waits for the data to be written from the in-memory DB to the addons.json
-// file that is done asynchronously through OS.File
-function waitForFlushedData(aCallback) {
-  Services.obs.addObserver({
-    observe: function(aSubject, aTopic, aData) {
-      Services.obs.removeObserver(this, "addon-repository-data-written");
-      aCallback(aData == "true");
-    }
-  }, "addon-repository-data-written", false);
+  // If cache is disabled, then expect results immediately
+  let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED);
+  yield check_cache(aExpectedToFind, !cacheEnabled);
 }
 
 function run_test() {
+  run_next_test();
+}
+
+add_task(function* setup() {
   // Setup for test
-  do_test_pending("test_AddonRepository_cache");
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
 
   startupManager();
 
   // Install XPI add-ons
-  installAllFiles(ADDON_FILES, function first_installs() {
-    restartManager();
-
-    gServer = new HttpServer();
-    gServer.registerDirectory("/data/", do_get_file("data"));
-    gServer.start(PORT);
+  yield promiseInstallAllFiles(ADDON_FILES);
+  yield promiseRestartManager();
 
-    do_execute_soon(run_test_1);
-  });
-}
-
-function end_test() {
-  gServer.stop(function() {do_test_finished("test_AddonRepository_cache");});
-}
+  gServer = new HttpServer();
+  gServer.registerDirectory("/data/", do_get_file("data"));
+  gServer.start(PORT);
+});
 
 // Tests AddonRepository.cacheEnabled
-function run_test_1() {
+add_task(function* run_test_1() {
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
   do_check_false(AddonRepository.cacheEnabled);
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
   do_check_true(AddonRepository.cacheEnabled);
-
-  do_execute_soon(run_test_2);
-}
+});
 
 // Tests that the cache and database begin as empty
-function run_test_2() {
-  check_database_exists(false);
-  check_cache([false, false, false], false, function(){});
-  waitForFlushedData(run_test_3);
-}
+add_task(function* run_test_2() {
+  do_check_false(gDBFile.exists());
+  yield check_cache([false, false, false], false);
+  yield AddonRepository.flush();
+});
 
 // Tests repopulateCache when the search fails
-function run_test_3() {
-  check_database_exists(true);
+add_task(function* run_test_3() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
 
-  AddonRepository.repopulateCache(ADDON_IDS, function test_3_repopulated() {
-    check_initialized_cache([false, false, false], run_test_4);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.repopulateCache(ADDON_IDS, resolve));
+  yield check_initialized_cache([false, false, false]);
+});
 
 // Tests repopulateCache when search returns no results
-function run_test_4() {
+add_task(function* run_test_4() {
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
 
-  AddonRepository.repopulateCache(ADDON_IDS, function() {
-    check_initialized_cache([false, false, false], run_test_5);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.repopulateCache(ADDON_IDS, resolve));
+  yield check_initialized_cache([false, false, false]);
+});
 
 // Tests repopulateCache when search returns results
-function run_test_5() {
+add_task(function* run_test_5() {
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
 
-  AddonRepository.repopulateCache(ADDON_IDS, function() {
-    check_initialized_cache([true, true, true], run_test_5_1);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.repopulateCache(ADDON_IDS, resolve));
+  yield check_initialized_cache([true, true, true]);
+});
 
 // Tests repopulateCache when caching is disabled for a single add-on
-function run_test_5_1() {
+add_task(function* run_test_5_1() {
   Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, false);
 
-  AddonRepository.repopulateCache(ADDON_IDS, function() {
-    // Reset pref for next test
-    Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true);
-    check_initialized_cache([false, true, true], run_test_6);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.repopulateCache(ADDON_IDS, resolve));
+
+  // Reset pref for next test
+  Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true);
+
+  yield check_initialized_cache([false, true, true]);
+});
 
 // Tests repopulateCache when caching is disabled
-function run_test_6() {
-  check_database_exists(true);
+add_task(function* run_test_6() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
 
-  AddonRepository.repopulateCache(ADDON_IDS, function() {
-    // Database should have been deleted
-    check_database_exists(false);
+  yield new Promise((resolve, reject) =>
+    AddonRepository.repopulateCache(ADDON_IDS, resolve));
+  // Database should have been deleted
+  do_check_false(gDBFile.exists());
 
-    Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
-    check_cache([false, false, false], false, function() {});
-
-    waitForFlushedData(run_test_7);
-  });
-}
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  yield check_cache([false, false, false], false);
+  yield AddonRepository.flush();
+});
 
 // Tests cacheAddons when the search fails
-function run_test_7() {
-  check_database_exists(true);
+add_task(function* run_test_7() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
 
-  AddonRepository.cacheAddons(ADDON_IDS, function() {
-    check_initialized_cache([false, false, false], run_test_8);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons(ADDON_IDS, resolve));
+  yield check_initialized_cache([false, false, false]);
+});
 
 // Tests cacheAddons when the search returns no results
-function run_test_8() {
+add_task(function* run_test_8() {
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
 
-  AddonRepository.cacheAddons(ADDON_IDS, function() {
-    check_initialized_cache([false, false, false], run_test_9);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons(ADDON_IDS, resolve));
+  yield check_initialized_cache([false, false, false]);
+});
 
 // Tests cacheAddons for a single add-on when search returns results
-function run_test_9() {
+add_task(function* run_test_9() {
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
 
-  AddonRepository.cacheAddons([ADDON_IDS[0]], function() {
-    check_initialized_cache([true, false, false], run_test_9_1);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons([ADDON_IDS[0]], resolve));
+  yield check_initialized_cache([true, false, false]);
+});
 
 // Tests cacheAddons when caching is disabled for a single add-on
-function run_test_9_1() {
+add_task(function* run_test_9_1() {
   Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, false);
 
-  AddonRepository.cacheAddons(ADDON_IDS, function() {
-    // Reset pref for next test
-    Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true);
-    check_initialized_cache([true, false, true], run_test_10);
-  });
-}
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons(ADDON_IDS, resolve));
+
+  // Reset pref for next test
+  Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true);
+
+  yield check_initialized_cache([true, false, true]);
+});
 
 // Tests cacheAddons for multiple add-ons, some already in the cache,
-function run_test_10() {
-  AddonRepository.cacheAddons(ADDON_IDS, function() {
-    check_initialized_cache([true, true, true], run_test_11);
-  });
-}
+add_task(function* run_test_10() {
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons(ADDON_IDS, resolve));
+  yield check_initialized_cache([true, true, true]);
+});
 
 // Tests cacheAddons when caching is disabled
-function run_test_11() {
-  check_database_exists(true);
+add_task(function* run_test_11() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
 
-  AddonRepository.cacheAddons(ADDON_IDS, function() {
-    // Database deleted for repopulateCache, not cacheAddons
-    check_database_exists(true);
+  yield new Promise((resolve, reject) =>
+    AddonRepository.cacheAddons(ADDON_IDS, resolve));
+  do_check_true(gDBFile.exists());
 
-    Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
-    check_initialized_cache([true, true, true], run_test_12);
-  });
-}
+  Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+  yield check_initialized_cache([true, true, true]);
+});
 
 // Tests that XPI add-ons do not use any of the repository properties if
 // caching is disabled, even if there are repository properties available
-function run_test_12() {
-  check_database_exists(true);
+add_task(function* run_test_12() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
 
-  AddonManager.getAddonsByIDs(ADDON_IDS, function test_12_check(aAddons) {
-    check_results(aAddons, WITHOUT_CACHE);
-    do_execute_soon(run_test_13);
-  });
-}
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITHOUT_CACHE);
+});
 
 // Tests that a background update with caching disabled deletes the add-ons
 // database, and that XPI add-ons still do not use any of repository properties
-function run_test_13() {
-  check_database_exists(true);
+add_task(function* run_test_13() {
+  do_check_true(gDBFile.exists());
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_EMPTY);
 
-  trigger_background_update(function() {
-    // Database should have been deleted
-    check_database_exists(false);
+  yield trigger_background_update();
+  // Database should have been deleted
+  do_check_false(gDBFile.exists());
 
-    AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
-      check_results(aAddons, WITHOUT_CACHE);
-      do_execute_soon(run_test_14);
-    });
-  });
-}
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITHOUT_CACHE);
+});
 
 // Tests that the XPI add-ons have the correct properties if caching is
 // enabled but has no information
-function run_test_14() {
+add_task(function* run_test_14() {
   Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
 
-  waitForFlushedData(function() {
-    check_database_exists(true);
+  yield trigger_background_update();
+  yield AddonRepository.flush();
+  do_check_true(gDBFile.exists());
 
-    AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
-      check_results(aAddons, WITHOUT_CACHE);
-      do_execute_soon(run_test_15);
-    });
-  });
-
-  trigger_background_update();
-}
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITHOUT_CACHE);
+});
 
 // Tests that the XPI add-ons correctly use the repository properties when
 // caching is enabled and the repository information is available
-function run_test_15() {
+add_task(function* run_test_15() {
   Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_RESULTS);
 
-  trigger_background_update(function() {
-    AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
-      check_results(aAddons, WITH_CACHE);
-      do_execute_soon(run_test_16);
-    });
-  });
-}
+  yield trigger_background_update();
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITH_CACHE);
+});
 
 // Tests that restarting the manager does not change the checked properties
 // on the XPI add-ons (repository properties still exist and are still properly
 // used)
-function run_test_16() {
-  restartManager();
+add_task(function* run_test_16() {
+  yield promiseRestartManager();
 
-  AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
-    check_results(aAddons, WITH_CACHE);
-    do_execute_soon(run_test_17);
-  });
-}
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITH_CACHE);
+});
 
 // Tests that setting a list of types to cache works
-function run_test_17() {
+add_task(function* run_test_17() {
   Services.prefs.setCharPref(PREF_GETADDONS_CACHE_TYPES, "foo,bar,extension,baz");
 
-  trigger_background_update(function() {
-    AddonManager.getAddonsByIDs(ADDON_IDS, function(aAddons) {
-      check_results(aAddons, WITH_EXTENSION_CACHE);
-      end_test();
-    });
-  });
-}
+  yield trigger_background_update();
+  let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+  check_results(aAddons, WITH_EXTENSION_CACHE);
+});
 
+add_task(function* end_test() {
+  yield new Promise((resolve, reject) => gServer.stop(resolve));
+});