Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 17 May 2016 14:15:06 -0700
changeset 297761 f3f2fa1d7eed5a8262f6401ef18ff8117a3ce43e
parent 297760 991f249a6ffa07e1ccb87ab657d71c4522429058 (current diff)
parent 297758 6e2add233b54766d44568f81aa207c624bbe4437 (diff)
child 297762 8082afb19f450b0c5cb1c259ec2ee654d2449bc2
child 297923 67eb2faeb2ab2d00f70cda1148baa46a190ccd88
push id76883
push userkwierso@gmail.com
push dateTue, 17 May 2016 21:17:34 +0000
treeherdermozilla-inbound@8082afb19f45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone49.0a1
first release with
nightly linux32
f3f2fa1d7eed / 49.0a1 / 20160518030234 / files
nightly linux64
f3f2fa1d7eed / 49.0a1 / 20160518030234 / files
nightly mac
f3f2fa1d7eed / 49.0a1 / 20160518030234 / files
nightly win32
f3f2fa1d7eed / 49.0a1 / 20160518030234 / files
nightly win64
f3f2fa1d7eed / 49.0a1 / 20160518030234 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
browser/locales/en-US/chrome/browser/browser.properties
mobile/android/base/java/org/mozilla/gecko/firstrun/WelcomePanel.java
mobile/android/base/java/org/mozilla/gecko/home/ReadingListPanel.java
mobile/android/base/resources/drawable-hdpi/reading_list_migration.png
mobile/android/base/resources/drawable-large-hdpi-v11/firstrun_background_coffee.png
mobile/android/base/resources/drawable-large-xhdpi-v11/firstrun_background_coffee.png
mobile/android/base/resources/drawable-large-xxhdpi-v11/firstrun_background_coffee.png
mobile/android/base/resources/drawable-nodpi/firstrun_background_coffee.png
mobile/android/base/resources/drawable-xhdpi/reading_list_migration.png
mobile/android/base/resources/drawable-xxhdpi/reading_list_migration.png
mobile/android/base/resources/layout/firstrun_basepanel_fragment.xml
mobile/android/base/resources/layout/firstrun_welcome_fragment.xml
mobile/android/base/resources/layout/readinglistpanel_gone_fragment.xml
testing/eslint-plugin-mozilla/LICENSE
testing/eslint-plugin-mozilla/docs/balanced-listeners.rst
testing/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
testing/eslint-plugin-mozilla/docs/import-globals.rst
testing/eslint-plugin-mozilla/docs/import-headjs-globals.rst
testing/eslint-plugin-mozilla/docs/index.rst
testing/eslint-plugin-mozilla/docs/mark-test-function-used.rst
testing/eslint-plugin-mozilla/docs/no-aArgs.rst
testing/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
testing/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
testing/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
testing/eslint-plugin-mozilla/lib/globals.js
testing/eslint-plugin-mozilla/lib/helpers.js
testing/eslint-plugin-mozilla/lib/index.js
testing/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
testing/eslint-plugin-mozilla/lib/rules/.eslintrc
testing/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
testing/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
testing/eslint-plugin-mozilla/lib/rules/import-globals.js
testing/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
testing/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
testing/eslint-plugin-mozilla/lib/rules/no-aArgs.js
testing/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
testing/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
testing/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
testing/eslint-plugin-mozilla/moz.build
testing/eslint-plugin-mozilla/package.json
testing/taskcluster/tasks/branches/base_jobs.yml
--- a/.gitignore
+++ b/.gitignore
@@ -95,18 +95,18 @@ embedding/ios/GeckoEmbed/GeckoEmbed.xcod
 
 # Ignore mozharness execution files
 testing/mozharness/.tox/
 testing/mozharness/build/
 testing/mozharness/logs/
 testing/mozharness/.coverage
 testing/mozharness/nosetests.xml
 
-# Ignore node_modules from eslint-plugin-mozilla
-testing/eslint-plugin-mozilla/node_modules/
+# Ignore node_modules
+testing/eslint/node_modules/
 
 # Ignore talos virtualenv and tp5n files.
 # The tp5n set is supposed to be decompressed at
 # testing/talos/talos/page_load_test/tp5n in order to run tests like tps
 # locally. Similarly, running talos requires a Python package virtual
 # environment. Both the virtual environment and tp5n files end up littering
 # the status command, so we ignore them.
 testing/talos/.Python
--- a/.hgignore
+++ b/.hgignore
@@ -111,18 +111,18 @@ GPATH
 ^testing/mozharness/build/
 ^testing/mozharness/logs/
 ^testing/mozharness/.coverage
 ^testing/mozharness/nosetests.xml
 
 # Ignore tox generated dir
 .tox/
 
-# Ignore node_modules from eslint-plugin-mozilla
-^testing/eslint-plugin-mozilla/node_modules/
+# Ignore node_modules
+^testing/eslint/node_modules/
 
 # Ignore talos virtualenv and tp5n files.
 # The tp5n set is supposed to be decompressed at
 # testing/talos/talos/page_load_test/tp5n in order to run tests like tps
 # locally. Similarly, running talos requires a Python package virtual
 # environment. Both the virtual environment and tp5n files end up littering
 # the status command, so we ignore them.
 ^testing/talos/.Python
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -241,16 +241,18 @@ var AboutPrivateBrowsingListener = {
   },
 };
 AboutPrivateBrowsingListener.init(this);
 
 var AboutReaderListener = {
 
   _articlePromise: null,
 
+  _isLeavingReaderMode: false,
+
   init: function() {
     addEventListener("AboutReaderContentLoaded", this, false, true);
     addEventListener("DOMContentLoaded", this, false);
     addEventListener("pageshow", this, false);
     addEventListener("pagehide", this, false);
     addMessageListener("Reader:ToggleReaderMode", this);
     addMessageListener("Reader:PushState", this);
   },
@@ -258,16 +260,17 @@ var AboutReaderListener = {
   receiveMessage: function(message) {
     switch (message.name) {
       case "Reader:ToggleReaderMode":
         let url = content.document.location.href;
         if (!this.isAboutReader) {
           this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError);
           ReaderMode.enterReaderMode(docShell, content);
         } else {
+          this._isLeavingReaderMode = true;
           ReaderMode.leaveReaderMode(docShell, content);
         }
         break;
 
       case "Reader:PushState":
         this.updateReaderButton(!!(message.data && message.data.isArticle));
         break;
     }
@@ -296,17 +299,23 @@ var AboutReaderListener = {
           sendAsyncMessage("Reader:UpdateReaderButton");
           new AboutReader(global, content, this._articlePromise);
           this._articlePromise = null;
         }
         break;
 
       case "pagehide":
         this.cancelPotentialPendingReadabilityCheck();
-        sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false });
+        // this._isLeavingReaderMode is used here to keep the Reader Mode icon
+        // visible in the location bar when transitioning from reader-mode page
+        // back to the source page.
+        sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderMode });
+        if (this._isLeavingReaderMode) {
+          this._isLeavingReaderMode = false;
+        }
         break;
 
       case "pageshow":
         // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded"
         // event, so we need to rely on "pageshow" in this case.
         if (aEvent.persisted) {
           this.updateReaderButton();
         }
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -500,16 +500,17 @@ tags = psm
 [browser_mcb_redirect.js]
 tags = mcb
 [browser_windowactivation.js]
 [browser_contextmenu_childprocess.js]
 [browser_bug963945.js]
 [browser_readerMode.js]
 support-files =
   readerModeArticle.html
+  readerModeArticleHiddenNodes.html
 [browser_readerMode_hidden_nodes.js]
 support-files =
   readerModeArticleHiddenNodes.html
 [browser_bug1124271_readerModePinnedTab.js]
 support-files =
   readerModeArticle.html
 [browser_domFullscreen_fullscreenMode.js]
 tags = fullscreen
--- a/browser/base/content/test/general/browser_readerMode.js
+++ b/browser/base/content/test/general/browser_readerMode.js
@@ -96,8 +96,120 @@ add_task(function* test_getOriginalUrl()
   is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(url)), url, "Found original URL from encoded URL");
   is(ReaderMode.getOriginalUrl("about:reader?foobar"), null, "Did not find original URL from malformed reader URL");
   is(ReaderMode.getOriginalUrl(url), null, "Did not find original URL from non-reader URL");
 
   let badUrl = "http://foo.com/?;$%^^";
   is(ReaderMode.getOriginalUrl("about:reader?url=" + encodeURIComponent(badUrl)), badUrl, "Found original URL from encoded malformed URL");
   is(ReaderMode.getOriginalUrl("about:reader?url=" + badUrl), badUrl, "Found original URL from non-encoded malformed URL");
 });
+
+add_task(function* test_reader_view_element_attribute_transform() {
+  registerCleanupFunction(function() {
+    while (gBrowser.tabs.length > 1) {
+      gBrowser.removeCurrentTab();
+    }
+  });
+
+  function observeAttribute(element, attribute, triggerFn, checkFn) {
+    let initValue = element.getAttribute(attribute);
+    return new Promise(resolve => {
+      let observer = new MutationObserver((mutations) => {
+        mutations.forEach( mu => {
+          let muValue = element.getAttribute(attribute);
+          if(element.getAttribute(attribute) !== mu.oldValue) {
+            checkFn();
+            resolve();
+            observer.disconnect();
+          }
+        });
+      });
+
+      observer.observe(element, {
+        attributes: true,
+        attributeOldValue: true,
+        attributeFilter: [attribute]
+      });
+
+      triggerFn();
+    });
+  };
+
+  let command = document.getElementById("View:ReaderView");
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser);
+  is(command.hidden, true, "Command element should have the hidden attribute");
+
+  info("Navigate a reader-able page");
+  let waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(command, "hidden",
+    () => {
+      let url = TEST_PATH + "readerModeArticle.html";
+      tab.linkedBrowser.loadURI(url);
+    },
+    () => {
+      is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
+    }
+  );
+  yield waitForPageshow;
+
+  info("Navigate a non-reader-able page");
+  waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(command, "hidden",
+    () => {
+      let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+      tab.linkedBrowser.loadURI(url);
+    },
+    () => {
+      is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
+    }
+  );
+  yield waitForPageshow;
+
+  info("Navigate a reader-able page");
+  waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(command, "hidden",
+    () => {
+      let url = TEST_PATH + "readerModeArticle.html";
+      tab.linkedBrowser.loadURI(url);
+    },
+    () => {
+      is(command.hidden, false, "Command's hidden attribute should be false on a reader-able page");
+    }
+  );
+  yield waitForPageshow;
+
+  info("Enter Reader Mode");
+  waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(readerButton, "readeractive",
+    () => {
+      readerButton.click();
+    },
+    () => {
+      is(readerButton.getAttribute("readeractive"), "true", "readerButton's readeractive attribute should be true when entering reader mode");
+    }
+  );
+  yield waitForPageshow;
+
+  info("Exit Reader Mode");
+  waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(readerButton, "readeractive",
+    () => {
+      readerButton.click();
+    },
+    () => {
+      is(readerButton.getAttribute("readeractive"), "", "readerButton's readeractive attribute should be empty when reader mode is exited");
+    }
+  );
+  yield waitForPageshow;
+
+  info("Navigate a non-reader-able page");
+  waitForPageshow = BrowserTestUtils.waitForContentEvent(tab.linkedBrowser, "pageshow");
+  yield observeAttribute(command, "hidden",
+    () => {
+      let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
+      tab.linkedBrowser.loadURI(url);
+    },
+    () => {
+      is(command.hidden, true, "Command's hidden attribute should be true on a non-reader-able page");
+    }
+  );
+  yield waitForPageshow;
+});
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -8,16 +8,19 @@ support-files =
   file_popup_api_injection_a.html
   file_popup_api_injection_b.html
   file_iframe_document.html
   file_iframe_document.sjs
   file_bypass_cache.sjs
   file_language_fr_en.html
   file_language_ja.html
   file_dummy.html
+  searchSuggestionEngine.xml
+  searchSuggestionEngine.sjs
+
 
 [browser_ext_browserAction_context.js]
 [browser_ext_browserAction_disabled.js]
 [browser_ext_browserAction_pageAction_icon.js]
 [browser_ext_browserAction_popup.js]
 [browser_ext_browserAction_simple.js]
 [browser_ext_commands_execute_page_action.js]
 [browser_ext_commands_getAll.js]
@@ -60,16 +63,17 @@ support-files =
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_zoom.js]
 [browser_ext_tabs_update_url.js]
 [browser_ext_topwindowid.js]
 [browser_ext_webNavigation_getFrames.js]
+[browser_ext_webNavigation_urlbar_transitions.js]
 [browser_ext_windows.js]
 [browser_ext_windows_create.js]
 tags = fullscreen
 [browser_ext_windows_create_tabId.js]
 [browser_ext_windows_events.js]
 [browser_ext_windows_size.js]
 skip-if = os == 'mac' # Fails when windows are randomly opened in fullscreen mode
 [browser_ext_windows_update.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -0,0 +1,261 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+                                  "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+                                  "resource://testing-common/PlacesTestUtils.jsm");
+
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+function* addBookmark(bookmark) {
+  if (bookmark.keyword) {
+    yield PlacesUtils.keywords.insert({
+      keyword: bookmark.keyword,
+      url: bookmark.url,
+    });
+  }
+
+  yield PlacesUtils.bookmarks.insert({
+    parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+    url: bookmark.url,
+    title: bookmark.title,
+  });
+
+  registerCleanupFunction(function* () {
+    yield PlacesUtils.bookmarks.eraseEverything();
+  });
+}
+
+function addSearchEngine(basename) {
+  return new Promise((resolve, reject) => {
+    info("Waiting for engine to be added: " + basename);
+    let url = getRootDirectory(gTestPath) + basename;
+    Services.search.addEngine(url, null, "", false, {
+      onSuccess: (engine) => {
+        info(`Search engine added: ${basename}`);
+        registerCleanupFunction(() => Services.search.removeEngine(engine));
+        resolve(engine);
+      },
+      onError: (errCode) => {
+        ok(false, `addEngine failed with error code ${errCode}`);
+        reject();
+      },
+    });
+  });
+}
+
+function* prepareSearchEngine() {
+  let oldCurrentEngine = Services.search.currentEngine;
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+  let engine = yield addSearchEngine(TEST_ENGINE_BASENAME);
+  Services.search.currentEngine = engine;
+
+  registerCleanupFunction(function* () {
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.search.currentEngine = oldCurrentEngine;
+
+    // Make sure the popup is closed for the next test.
+    gURLBar.blur();
+    gURLBar.popup.selectedIndex = -1;
+    gURLBar.popup.hidePopup();
+    ok(!gURLBar.popup.popupOpen, "popup should be closed");
+
+    // Clicking suggestions causes visits to search results pages, so clear that
+    // history now.
+    yield PlacesTestUtils.clearHistory();
+  });
+}
+
+add_task(function* test_webnavigation_urlbar_typed_transitions() {
+  function backgroundScript() {
+    browser.webNavigation.onCommitted.addListener((msg) => {
+      browser.test.assertEq("http://example.com/?q=typed", msg.url,
+                            "Got the expected url");
+      // assert from_address_bar transition qualifier
+      browser.test.assertTrue(msg.transitionQualifiers &&
+                          msg.transitionQualifiers.includes("from_address_bar"),
+                              "Got the expected from_address_bar transitionQualifier");
+      browser.test.assertEq("typed", msg.transitionType,
+                            "Got the expected transitionType");
+      browser.test.notifyPass("webNavigation.from_address_bar.typed");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: backgroundScript,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitMessage("ready");
+
+  gURLBar.focus();
+  gURLBar.textValue = "http://example.com/?q=typed";
+
+  EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
+
+  yield extension.awaitFinish("webNavigation.from_address_bar.typed");
+
+  yield extension.unload();
+  info("extension unloaded");
+});
+
+add_task(function* test_webnavigation_urlbar_bookmark_transitions() {
+  function backgroundScript() {
+    browser.webNavigation.onCommitted.addListener((msg) => {
+      browser.test.assertEq("http://example.com/?q=bookmark", msg.url,
+                            "Got the expected url");
+
+      // assert from_address_bar transition qualifier
+      browser.test.assertTrue(msg.transitionQualifiers &&
+                          msg.transitionQualifiers.includes("from_address_bar"),
+                              "Got the expected from_address_bar transitionQualifier");
+      browser.test.assertEq("auto_bookmark", msg.transitionType,
+                            "Got the expected transitionType");
+      browser.test.notifyPass("webNavigation.from_address_bar.auto_bookmark");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: backgroundScript,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield addBookmark({
+    title: "Bookmark To Click",
+    url: "http://example.com/?q=bookmark",
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitMessage("ready");
+
+  gURLBar.focus();
+  gURLBar.value = "Bookmark To Click";
+  gURLBar.controller.startSearch("Bookmark To Click");
+
+  let item;
+
+  yield BrowserTestUtils.waitForCondition(() => {
+    item = gURLBar.popup.richlistbox.getItemAtIndex(1);
+    return item;
+  });
+
+  item.click();
+  yield extension.awaitFinish("webNavigation.from_address_bar.auto_bookmark");
+
+  yield extension.unload();
+  info("extension unloaded");
+});
+
+add_task(function* test_webnavigation_urlbar_keyword_transition() {
+  function backgroundScript() {
+    browser.webNavigation.onCommitted.addListener((msg) => {
+      browser.test.assertEq(`http://example.com/?q=search`, msg.url,
+                            "Got the expected url");
+
+      // assert from_address_bar transition qualifier
+      browser.test.assertTrue(msg.transitionQualifiers &&
+                          msg.transitionQualifiers.includes("from_address_bar"),
+                              "Got the expected from_address_bar transitionQualifier");
+      browser.test.assertEq("keyword", msg.transitionType,
+                            "Got the expected transitionType");
+      browser.test.notifyPass("webNavigation.from_address_bar.keyword");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: backgroundScript,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield addBookmark({
+    title: "Test Keyword",
+    url: "http://example.com/?q=%s",
+    keyword: "testkw",
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitMessage("ready");
+
+  gURLBar.focus();
+  gURLBar.value = "testkw search";
+  gURLBar.controller.startSearch("testkw search");
+
+  yield BrowserTestUtils.waitForCondition(() => {
+    return gURLBar.popup.input.controller.matchCount;
+  });
+
+  let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
+  item.click();
+
+  yield extension.awaitFinish("webNavigation.from_address_bar.keyword");
+
+  yield extension.unload();
+  info("extension unloaded");
+});
+
+add_task(function* test_webnavigation_urlbar_search_transitions() {
+  function backgroundScript() {
+    browser.webNavigation.onCommitted.addListener((msg) => {
+      browser.test.assertEq("http://mochi.test:8888/", msg.url,
+                            "Got the expected url");
+
+      // assert from_address_bar transition qualifier
+      browser.test.assertTrue(msg.transitionQualifiers &&
+                          msg.transitionQualifiers.includes("from_address_bar"),
+                              "Got the expected from_address_bar transitionQualifier");
+      browser.test.assertEq("generated", msg.transitionType,
+                            "Got the expected 'generated' transitionType");
+      browser.test.notifyPass("webNavigation.from_address_bar.generated");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: backgroundScript,
+    manifest: {
+      permissions: ["webNavigation"],
+    },
+  });
+
+  yield extension.startup();
+
+  yield extension.awaitMessage("ready");
+
+  yield prepareSearchEngine();
+
+  gURLBar.focus();
+  gURLBar.value = "foo";
+  gURLBar.controller.startSearch("foo");
+
+  yield BrowserTestUtils.waitForCondition(() => {
+    return gURLBar.popup.input.controller.matchCount;
+  });
+
+  let item = gURLBar.popup.richlistbox.getItemAtIndex(0);
+  item.click();
+
+  yield extension.awaitFinish("webNavigation.from_address_bar.generated");
+
+  yield extension.unload();
+  info("extension unloaded");
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+  let suffixes = ["foo", "bar"];
+  let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+  resp.setHeader("Content-Type", "application/json", false);
+  resp.write(JSON.stringify(data));
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/searchSuggestionEngine.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/extensions/test/browser/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://mochi.test:8888/" rel="searchform"/>
+</SearchPlugin>
--- a/browser/experiments/test/xpcshell/test_conditions.js
+++ b/browser/experiments/test/xpcshell/test_conditions.js
@@ -1,17 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 
 Cu.import("resource:///modules/experiments/Experiments.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
-Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 
 const FILE_MANIFEST            = "experiments.manifest";
 const SEC_IN_ONE_DAY = 24 * 60 * 60;
 const MS_IN_ONE_DAY  = SEC_IN_ONE_DAY * 1000;
 
 var gProfileDir = null;
 var gHttpServer = null;
 var gHttpRoot   = null;
@@ -48,18 +47,17 @@ function applicableFromManifestData(data
 function run_test() {
   run_next_test();
 }
 
 add_task(function* test_setup() {
   createAppInfo();
   gProfileDir = do_get_profile();
   startAddonManagerOnly();
-  yield TelemetryController.setup();
-  yield TelemetrySession.setup();
+  yield TelemetryController.testSetup();
   gPolicy = new Experiments.Policy();
 
   patchPolicy(gPolicy, {
     updatechannel: () => "nightly",
     locale: () => "en-US",
     random: () => 0.5,
   });
 
@@ -324,10 +322,10 @@ add_task(function* test_times() {
       + i + ": " + JSON.stringify([entry[2], entry[3]]));
     if (!applicable && entry[1]) {
       Assert.equal(reason, entry[1], "Experiment rejection reason should match for test " + i);
     }
   }
 });
 
 add_task(function* test_shutdown() {
-  yield TelemetrySession.shutdown(false);
+  yield TelemetryController.testShutdown();
 });
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -395,17 +395,17 @@ pointerLock.allow2.accesskey=H
 pointerLock.alwaysAllow=Always allow hiding
 pointerLock.alwaysAllow.accesskey=A
 pointerLock.neverAllow=Never allow hiding
 pointerLock.neverAllow.accesskey=N
 pointerLock.title3=Would you like to allow the pointer to be hidden on this site?
 pointerLock.autoLock.title3=This site will hide the pointer.
 
 # Phishing/Malware Notification Bar.
-# LOCALIZATION NOTE (notAForgery, notAnAttack)
+# LOCALIZATION NOTE (notADeceptiveSite, notAnAttack)
 # The two button strings will never be shown at the same time, so
 # it's okay for them to have the same access key
 safebrowsing.getMeOutOfHereButton.label=Get me out of here!
 safebrowsing.getMeOutOfHereButton.accessKey=G
 safebrowsing.deceptiveSite=Deceptive Site!
 safebrowsing.notADeceptiveSiteButton.label=This isn’t a deceptive site…
 safebrowsing.notADeceptiveSiteButton.accessKey=D
 safebrowsing.reportedAttackSite=Reported Attack Site!
--- a/devtools/client/aboutdebugging/test/browser_addons_reload.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -17,30 +17,75 @@ function promiseAddonEvent(event) {
         resolve(args);
       }
     };
 
     AddonManager.addAddonListener(listener);
   });
 }
 
-add_task(function* () {
+function getReloadButton(document, addonName) {
+  const names = [...document.querySelectorAll("#addons .target-name")];
+  const name = names.filter(element => element.textContent === addonName)[0];
+  ok(name, `Found ${addonName} add-on in the list`);
+  const targetElement = name.parentNode.parentNode;
+  const reloadButton = targetElement.querySelector(".reload-button");
+  info(`Found reload button for ${addonName}`);
+  return reloadButton;
+}
+
+/**
+ * Creates a web extension from scratch in a temporary location.
+ * The object must be removed when you're finished working with it.
+ */
+class TempWebExt {
+  constructor(addonId) {
+    this.addonId = addonId;
+    this.tmpDir = FileUtils.getDir("TmpD", ["browser_addons_reload"]);
+    if (!this.tmpDir.exists()) {
+      this.tmpDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    }
+    this.sourceDir = this.tmpDir.clone();
+    this.sourceDir.append(this.addonId);
+    if (!this.sourceDir.exists()) {
+      this.sourceDir.create(Ci.nsIFile.DIRECTORY_TYPE,
+                           FileUtils.PERMS_DIRECTORY);
+    }
+  }
+
+  writeManifest(manifestData) {
+    const manifest = this.sourceDir.clone();
+    manifest.append("manifest.json");
+    if (manifest.exists()) {
+      manifest.remove(true);
+    }
+    const fos = Cc["@mozilla.org/network/file-output-stream;1"]
+                              .createInstance(Ci.nsIFileOutputStream);
+    fos.init(manifest,
+             FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+             FileUtils.MODE_TRUNCATE,
+             FileUtils.PERMS_FILE, 0);
+
+    const manifestString = JSON.stringify(manifestData);
+    fos.write(manifestString, manifestString.length);
+    fos.close();
+  }
+
+  remove() {
+    return this.tmpDir.remove(true);
+  }
+}
+
+add_task(function* reloadButtonReloadsAddon() {
   const { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
   yield installAddon(document, "addons/unpacked/install.rdf",
                      ADDON_NAME, ADDON_NAME);
 
-  // Retrieve the Reload button.
-  const names = [...document.querySelectorAll("#addons .target-name")];
-  const name = names.filter(element => element.textContent === ADDON_NAME)[0];
-  ok(name, `Found ${ADDON_NAME} add-on in the list`);
-  const targetElement = name.parentNode.parentNode;
-  const reloadButton = targetElement.querySelector(".reload-button");
-  ok(reloadButton, "Found its reload button");
-
+  const reloadButton = getReloadButton(document, ADDON_NAME);
   const onInstalled = promiseAddonEvent("onInstalled");
 
   const onBootstrapInstallCalled = new Promise(done => {
     Services.obs.addObserver(function listener() {
       Services.obs.removeObserver(listener, ADDON_NAME, false);
       info("Add-on was re-installed: " + ADDON_NAME);
       done();
     }, ADDON_NAME, false);
@@ -58,8 +103,60 @@ add_task(function* () {
   const onUninstalled = promiseAddonEvent("onUninstalled");
   reloadedAddon.uninstall();
   const [uninstalledAddon] = yield onUninstalled;
   is(uninstalledAddon.id, ADDON_ID,
      "Add-on was uninstalled: " + uninstalledAddon.id);
 
   yield closeAboutDebugging(tab);
 });
+
+add_task(function* reloadButtonRefreshesMetadata() {
+  const { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+
+  const manifestBase = {
+    "manifest_version": 2,
+    "name": "Temporary web extension",
+    "version": "1.0",
+    "applications": {
+      "gecko": {
+        "id": ADDON_ID
+      }
+    }
+  };
+
+  const tempExt = new TempWebExt(ADDON_ID);
+  tempExt.writeManifest(manifestBase);
+
+  const onAddonListUpdated = waitForMutation(getAddonList(document),
+                                             { childList: true });
+  const onInstalled = promiseAddonEvent("onInstalled");
+  yield AddonManager.installTemporaryAddon(tempExt.sourceDir);
+  const [addon] = yield onInstalled;
+  info(`addon installed: ${addon.id}`);
+  yield onAddonListUpdated;
+
+  const newName = "Temporary web extension (updated)";
+  tempExt.writeManifest(Object.assign({}, manifestBase, {name: newName}));
+
+  // Wait for the add-on list to be updated with the reloaded name.
+  const onReInstall = promiseAddonEvent("onInstalled");
+  const onAddonReloaded = waitForMutation(getAddonList(document),
+                                          { childList: true, subtree: true });
+  const reloadButton = getReloadButton(document, manifestBase.name);
+  reloadButton.click();
+
+  yield onAddonReloaded;
+  const [reloadedAddon] = yield onReInstall;
+  // Make sure the name was updated correctly.
+  const allAddons = [...document.querySelectorAll("#addons .target-name")]
+    .map(element => element.textContent);
+  const nameWasUpdated = allAddons.some(name => name === newName);
+  ok(nameWasUpdated, `New name appeared in reloaded add-ons: ${allAddons}`);
+
+  const onUninstalled = promiseAddonEvent("onUninstalled");
+  reloadedAddon.uninstall();
+  yield onUninstalled;
+
+  tempExt.remove();
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/themes/dark-theme.css
+++ b/devtools/client/themes/dark-theme.css
@@ -167,17 +167,17 @@ body {
 
 .theme-toolbar,
 .devtools-toolbar,
 .devtools-sidebar-tabs tabs,
 .devtools-sidebar-alltabs,
 .cm-s-mozilla .CodeMirror-dialog { /* General toolbar styling */
   color: var(--theme-body-color-alt);
   background-color: var(--theme-toolbar-background);
-  border-color: hsla(210,8%,5%,.6);
+  border-color: var(--theme-splitter-color);
 }
 
 .theme-fg-contrast { /* To be used for text on theme-bg-contrast */
   color: black;
 }
 
 .ruleview-swatch,
 .computedview-colorswatch {
--- a/devtools/client/themes/images/tool-debugger-paused.svg
+++ b/devtools/client/themes/images/tool-debugger-paused.svg
@@ -1,7 +1,7 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#2E761A">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#5FC749">
   <path d="M8.2 1.7C4.8 1.7 2 4.4 2 7.9s2.7 6.3 6.3 6.3c3.5 0 6.3-2.7 6.3-6.3-.1-3.4-2.9-6.2-6.4-6.2zm0 11.4C5.3 13.1 3 10.8 3 7.9s2.3-5.2 5.2-5.2 5.2 2.3 5.2 5.2c0 3-2.2 5.2-5.2 5.2z"/>
   <path d="M11.4 7.5L10 6.2c-.1-.1-.2-.1-.4-.1H5.7c-.1 0-.2.1-.2.2v3.2c0 .1.1.4.2.4h4c.1 0 .2-.1.2-.1l1.5-1.7c.2-.1.2-.4 0-.6z"/>
 </svg>
--- a/devtools/client/themes/images/tool-memory-active.svg
+++ b/devtools/client/themes/images/tool-memory-active.svg
@@ -1,7 +1,7 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#2E761A">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#5FC749">
   <path opacity="0.2" d="M5.2 2.9l5.7.1v9.4H5.2V2.9z"/>
   <path d="M10.5 2.2h-5c-.4 0-.8.4-.8.9v9.2c0 .4.3.9.8.9h5c.4 0 .8-.4.8-.9V3.1s-.6-.9-.8-.9zm-.2 10H5.7v-9h4.6v9zM15.5 6.6c-.1 0-.3-.1-.4-.2l-1.5-1.7-.9.9c-.2.2-.5.2-.7 0-.2-.2-.2-.5 0-.7l1.3-1.3c.1-.1.2-.1.4-.1.1 0 .3.1.4.2l1.9 2c.2.2.2.5 0 .7-.3.2-.4.2-.5.2zM15.5 9.7c-.1 0-.3-.1-.4-.2l-1.5-1.7-.9.9c-.2.3-.5.3-.7.1-.2-.2-.2-.5 0-.7l1.3-1.2c.1-.1.2-.1.4-.1.1 0 .3.1.4.2L16 9c.2.2.2.5 0 .7h-.5zM15.5 12.7c-.1 0-.3-.1-.4-.2l-1.5-1.7-.9.9c-.2.2-.5.2-.7 0-.2-.2-.2-.5 0-.7l1.3-1.2c.1-.1.2-.1.4-.1.1 0 .3.1.4.2l1.9 2c.2.2.2.5 0 .7-.3.1-.4.1-.5.1zM.5 6.6c.1 0 .3-.1.4-.2l1.5-1.7.9.9c.2.2.5.2.7 0 .2-.2.2-.5 0-.7L2.7 3.7c-.1-.1-.2-.1-.3-.1-.2 0-.3 0-.4.1l-1.9 2c-.2.3-.1.6.1.8.1.1.2.1.3.1zM.5 9.7c.1 0 .3-.1.4-.2l1.5-1.7.9.9c.2.3.5.3.7.1.2-.2.2-.6 0-.8L2.7 6.8c-.1-.1-.2-.1-.3-.1-.2 0-.3 0-.4.1l-1.9 2c-.2.2-.2.5 0 .7.2.2.3.2.4.2zM.5 12.7c.1 0 .3-.1.4-.2l1.5-1.7.9.9c.2.3.5.3.7.1.2-.2.2-.5 0-.7L2.7 9.8c-.1-.1-.2-.1-.3-.1-.2 0-.3 0-.4.1l-1.9 2c-.2.2-.2.5 0 .7.2.2.3.2.4.2z"/>
 </svg>
--- a/devtools/client/themes/images/tool-profiler-active.svg
+++ b/devtools/client/themes/images/tool-profiler-active.svg
@@ -1,10 +1,10 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#2E761A">
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="#5FC749">
   <path d="M6.9 7.8c-.3.1-.6.4-.6.7-.1.5.2 1 .7 1.1.3 0 .7-.1.9-.3l2.5-2.9-3.5 1.4z"/>
   <path opacity="0.5" d="M4.7 10.6c.7 1.1 1.9 1.9 3.3 1.9s2.6-.8 3.3-1.9H4.7z"/>
   <path d="M-12.7-2.5c-3.8 0-6.7 3-6.7 6.7s3 6.7 6.7 6.7c3.8 0 6.7-3 6.7-6.7s-2.9-6.7-6.7-6.7zM-12.8 9c-2.5 0-4.6-2.1-4.8-4.5v-.2h.6c.6 0 1-.4 1-1s-.4-1-1-1h-.4c.4-.9.8-1.4 1.5-1.9l.2.4c.3.5.8.7 1.3.4.5-.2.7-.7.4-1.2l-.2-.4c.4-.1.9-.2 1.4-.2.6 0 1.1.1 1.6.3l-.2.4c-.3.5-.1 1 .4 1.3.5.3 1 .1 1.3-.4l.2-.6c.6.6 1.2 1.5 1.4 1.8h-.4c-.6 0-1 .4-1 1s.4 1 1 1h.6v.2C-8.2 6.9-10.3 9-12.8 9zM-12.8 12.7c-3.4 0-6.2 2.7-6.2 6.2s2.7 6.3 6.3 6.3c3.5 0 6.3-2.7 6.3-6.3-.1-3.4-2.9-6.2-6.4-6.2zm0 11.4c-2.9 0-5.2-2.3-5.2-5.2 0-2.9 2.3-5.2 5.2-5.2s5.2 2.3 5.2 5.2c0 3-2.2 5.2-5.2 5.2z"/>
   <path d="M-14.5 16.3c-.2 0-.4.2-.4.4v4.5c0 .2.3.4.5.4s.5-.2.5-.4v-4.5c0-.2-.3-.4-.5-.4"/>
   <path d="M8 2.3C4.5 2.3 1.8 5 1.8 8.5s2.7 6.3 6.3 6.3c3.5 0 6.3-2.7 6.3-6.3-.2-3.4-2.9-6.2-6.4-6.2zm0 11.4c-2.9 0-5.2-2.3-5.2-5.2v-.3h1.7c.2 0 .4-.3.4-.5s-.2-.6-.4-.6H2.9C3.3 5.9 4 4.8 5 4.1l1.4 1.3c.1.1.5.1.6-.1.1-.1.2-.5.1-.6L6 3.6c.6-.2 1.3-.4 2-.4.8 0 1.5.2 2.2.5L9 4.8c-.1.1 0 .5.1.6s.5.2.6.1L11 4.3c1 .7 1.7 1.7 2 2.8h-1.7c-.2 0-.4.3-.4.5s.2.5.4.5h1.9v.3c0 3.1-2.2 5.3-5.2 5.3z"/>
 </svg>
--- a/devtools/client/themes/toolbars.css
+++ b/devtools/client/themes/toolbars.css
@@ -854,20 +854,16 @@
   min-height: 24px;
   border: 0px solid;
   border-bottom-width: 1px;
   padding: 0;
   background: var(--theme-tab-toolbar-background);
   border-bottom-color: var(--theme-splitter-color);
 }
 
-.theme-dark .devtools-tabbar {
-  box-shadow: 0 -0.5px 0 var(--theme-splitter-color) inset;
-}
-
 #toolbox-tabs {
   margin: 0;
 }
 
 .toolbox-panel {
   display: -moz-box;
   -moz-box-flex: 1;
   visibility: collapse;
@@ -920,22 +916,18 @@
 .theme-dark .devtools-tab:hover:active {
   color: var(--theme-selection-color);
 }
 
 .devtools-tab:hover:active {
   background-color: var(--toolbar-tab-hover-active);
 }
 
-.devtools-tab:not([selected])[highlighted] {
-  box-shadow: 0 2px 0 var(--theme-highlight-green) inset;
-}
-
 .theme-dark .devtools-tab:not([selected])[highlighted] {
-  background-color: hsla(99,100%,14%,.2);
+  background-color: hsla(99, 100%, 14%, .3);
 }
 
 .theme-light .devtools-tab:not([selected])[highlighted] {
   background-color: rgba(44, 187, 15, .2);
 }
 
 /* Display execution pointer in the Debugger tab to indicate
    that the debugger is paused. */
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -2320,16 +2320,21 @@ Object.defineProperty(BrowserAddonList.p
       AddonManager.addAddonListener(this);
     } else {
       AddonManager.removeAddonListener(this);
     }
   }
 });
 
 BrowserAddonList.prototype.onInstalled = function (addon) {
+  if (this._actorByAddonId.get(addon.id)) {
+    // When an add-on gets upgraded or reloaded, it will not be uninstalled
+    // so this step is necessary to clear the cache.
+    this._actorByAddonId.delete(addon.id);
+  }
   this._onListChanged();
 };
 
 BrowserAddonList.prototype.onUninstalled = function (addon) {
   this._actorByAddonId.delete(addon.id);
   this._onListChanged();
 };
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import android.Manifest;
 import android.app.DownloadManager;
 import android.os.Environment;
 import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import org.json.JSONArray;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
@@ -73,16 +74,17 @@ import org.mozilla.gecko.search.SearchEn
 import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
+import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.telemetry.TelemetryDispatcher;
 import org.mozilla.gecko.telemetry.UploadTelemetryCorePingCallback;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
@@ -1949,16 +1951,18 @@ public class BrowserApp extends GeckoApp
 
                 // Display notification for Mozilla data reporting, if data should be collected.
                 if (AppConstants.MOZ_DATA_REPORTING && Restrictions.isAllowed(this, Restrictable.DATA_CHOICES)) {
                     DataReportingNotification.checkAndNotifyPolicy(GeckoAppShell.getContext());
                 }
 
             } else if (event.equals("Search:Keyword")) {
                 storeSearchQuery(message.getString("query"));
+                recordSearch(GeckoSharedPrefs.forProfile(this), message.getString("identifier"),
+                        TelemetryContract.Method.ACTIONBAR);
             } else if (event.equals("LightweightTheme:Update")) {
                 ThreadUtils.postToUiThread(new Runnable() {
                     @Override
                     public void run() {
                         mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
                     }
                 });
             } else if (event.equals("Prompt:ShowTop")) {
@@ -2373,16 +2377,17 @@ public class BrowserApp extends GeckoApp
         // If the URL doesn't look like a search query, just load it.
         if (!StringUtils.isSearchQuery(url, true)) {
             Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
             return;
         }
 
         // Otherwise, check for a bookmark keyword.
+        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this);
         final BrowserDB db = getProfile().getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 final String keyword;
                 final String keywordSearch;
 
                 final int index = url.indexOf(" ");
@@ -2399,52 +2404,41 @@ public class BrowserApp extends GeckoApp
                 // If there isn't a bookmark keyword, load the url. This may result in a query
                 // using the default search engine.
                 if (TextUtils.isEmpty(keywordUrl)) {
                     Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
                     Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
                     return;
                 }
 
-                recordSearch(null, "barkeyword");
-
                 // Otherwise, construct a search query from the bookmark keyword.
                 // Replace lower case bookmark keywords with URLencoded search query or
                 // replace upper case bookmark keywords with un-encoded search query.
                 // This makes it match the same behaviour as on Firefox for the desktop.
                 final String searchUrl = keywordUrl.replace("%s", URLEncoder.encode(keywordSearch)).replace("%S", keywordSearch);
 
                 Tabs.getInstance().loadUrl(searchUrl, Tabs.LOADURL_USER_ENTERED);
                 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL,
                                       TelemetryContract.Method.ACTIONBAR,
                                       "keyword");
             }
         });
     }
 
     /**
-     * Record in Health Report that a search has occurred.
+     * Records in telemetry that a search has occurred.
      *
-     * @param engine
-     *        a search engine instance. Can be null.
-     * @param where
-     *        where the search was initialized; one of the values in
-     *        {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
+     * @param where where the search was started from
      */
-    private static void recordSearch(SearchEngine engine, String where) {
-        //try {
-        //    String identifier = (engine == null) ? "other" : engine.getEngineIdentifier();
-        //    JSONObject message = new JSONObject();
-        //    message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
-        //    message.put("location", where);
-        //    message.put("identifier", identifier);
-        //    EventDispatcher.getInstance().dispatchEvent(message, null);
-        //} catch (Exception e) {
-        //    Log.e(LOGTAG, "Error recording search.", e);
-        //}
+    private static void recordSearch(@NonNull final SharedPreferences prefs, @NonNull final String engineIdentifier,
+            @NonNull final TelemetryContract.Method where) {
+        // We could include the engine identifier as an extra but we'll
+        // just capture that with core ping telemetry (bug 1253319).
+        Telemetry.sendUIEvent(TelemetryContract.Event.SEARCH, where);
+        SearchCountMeasurements.incrementSearch(prefs, engineIdentifier, where.name());
     }
 
     /**
      * Store search query in SearchHistoryProvider.
      *
      * @param query
      *        a search query to store. We won't store empty queries.
      */
@@ -2599,16 +2593,23 @@ public class BrowserApp extends GeckoApp
                     delegate.onActivityResult(this, requestCode, resultCode, data);
                 }
 
                 super.onActivityResult(requestCode, resultCode, data);
         }
     }
 
     private void showFirstrunPager() {
+        if (Experiments.isInExperimentLocal(getContext(), Experiments.ONBOARDING3_A)) {
+            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A);
+            GeckoSharedPrefs.forProfile(getContext()).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_A).apply();
+            Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_A);
+            return;
+        }
+
         if (mFirstrunAnimationContainer == null) {
             final ViewStub firstrunPagerStub = (ViewStub) findViewById(R.id.firstrun_pager_stub);
             mFirstrunAnimationContainer = (FirstrunAnimationContainer) firstrunPagerStub.inflate();
             mFirstrunAnimationContainer.load(getApplicationContext(), getSupportFragmentManager());
             mFirstrunAnimationContainer.registerOnFinishListener(new FirstrunAnimationContainer.OnFinishListener() {
                 @Override
                 public void onFinish() {
                     if (mFirstrunAnimationContainer.showBrowserHint()) {
@@ -3883,24 +3884,24 @@ public class BrowserApp extends GeckoApp
                 getResources().getString(R.string.new_tab_opened);
         final String buttonMessage = getResources().getString(R.string.switch_button_message);
 
         SnackbarHelper.showSnackbarWithAction(this, message, Snackbar.LENGTH_LONG, buttonMessage, callback);
     }
 
     // BrowserSearch.OnSearchListener
     @Override
-    public void onSearch(SearchEngine engine, String text) {
+    public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
         // Don't store searches that happen in private tabs. This assumes the user can only
         // perform a search inside the currently selected tab, which is true for searches
         // that come from SearchEngineRow.
         if (!Tabs.getInstance().getSelectedTab().isPrivate()) {
             storeSearchQuery(text);
         }
-        recordSearch(engine, "barsuggest");
+        recordSearch(GeckoSharedPrefs.forProfile(this), engine.getEngineIdentifier(), method);
         openUrlAndStopEditing(text, engine.name);
     }
 
     // BrowserSearch.OnEditSuggestionListener
     @Override
     public void onEditSuggestion(String suggestion) {
         mBrowserToolbar.onEditSuggestion(suggestion);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunAnimationContainer.java
@@ -59,19 +59,18 @@ public class FirstrunAnimationContainer 
     public void hide() {
         visible = false;
         if (onFinishListener != null) {
             onFinishListener.onFinish();
         }
         animateHide();
 
         // Stop all versions of firstrun A/B sessions.
-        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_A);
-        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_B);
-        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_C);
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B);
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C);
     }
 
     private void animateHide() {
         final Animator alphaAnimator = ObjectAnimator.ofFloat(this, "alpha", 0);
         alphaAnimator.setDuration(150);
         alphaAnimator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPager.java
@@ -30,34 +30,31 @@ import java.util.List;
  *
  * @see FirstrunPanel for the first run pages that are used in this pager.
  */
 public class FirstrunPager extends ViewPager {
 
     private Context context;
     protected FirstrunPanel.PagerNavigation pagerNavigation;
     private Decor mDecor;
-    private View mTabStrip;
 
     public FirstrunPager(Context context) {
         this(context, null);
     }
 
     public FirstrunPager(Context context, AttributeSet attrs) {
         super(context, attrs);
         this.context = context;
     }
 
     @Override
     public void addView(View child, int index, ViewGroup.LayoutParams params) {
         if (child instanceof Decor) {
             ((ViewPager.LayoutParams) params).isDecor = true;
             mDecor = (Decor) child;
-            mTabStrip = child;
-
             mDecor.setOnTitleClickListener(new TabMenuStrip.OnTitleClickListener() {
                 @Override
                 public void onTitleClicked(int index) {
                     setCurrentItem(index, true);
                 }
             });
         }
 
@@ -66,19 +63,16 @@ public class FirstrunPager extends ViewP
 
     public void load(Context appContext, FragmentManager fm, final FirstrunAnimationContainer.OnFinishListener onFinishListener) {
         final List<FirstrunPagerConfig.FirstrunPanelConfig> panels;
 
         if (Restrictions.isUserRestricted(context)) {
             panels = FirstrunPagerConfig.getRestricted();
         } else {
             panels = FirstrunPagerConfig.getDefault(appContext);
-            if (panels.size() == 1) {
-                mTabStrip.setVisibility(GONE);
-            }
         }
 
         setAdapter(new ViewPagerAdapter(fm, panels));
         this.pagerNavigation = new FirstrunPanel.PagerNavigation() {
             @Override
             public void next() {
                 final int currentPage = FirstrunPager.this.getCurrentItem();
                 if (currentPage < FirstrunPager.this.getAdapter().getCount() - 1) {
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPagerConfig.java
@@ -22,44 +22,38 @@ public class FirstrunPagerConfig {
 
     public static final String KEY_IMAGE = "imageRes";
     public static final String KEY_TEXT = "textRes";
     public static final String KEY_SUBTEXT = "subtextRes";
 
    public static List<FirstrunPanelConfig> getDefault(Context context) {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
 
-        if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_A)) {
-            panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
-            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_A);
-            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_A).apply();
-        } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_B)) {
-            panels.add(SimplePanelConfigs.urlbarPanelConfig);
-            panels.add(SimplePanelConfigs.bookmarksPanelConfig);
-            panels.add(SimplePanelConfigs.syncPanelConfig);
-            panels.add(new FirstrunPanelConfig(SyncPanel.class.getName(), SyncPanel.TITLE_RES));
-            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_B);
-            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_B).apply();
-        } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING2_C)) {
+        if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_B)) {
             panels.add(SimplePanelConfigs.urlbarPanelConfig);
             panels.add(SimplePanelConfigs.bookmarksPanelConfig);
             panels.add(SimplePanelConfigs.dataPanelConfig);
             panels.add(SimplePanelConfigs.syncPanelConfig);
-            panels.add(new FirstrunPanelConfig(SyncPanel.class.getName(), SyncPanel.TITLE_RES));
-            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING2_C);
-            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING2_C).apply();
+            panels.add(SimplePanelConfigs.signInPanelConfig);
+            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_B);
+            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_B).apply();
+        } else if (Experiments.isInExperimentLocal(context, Experiments.ONBOARDING3_C)) {
+            panels.add(SimplePanelConfigs.tabqueuePanelConfig);
+            panels.add(SimplePanelConfigs.notificationsPanelConfig);
+            panels.add(SimplePanelConfigs.readerviewPanelConfig);
+            panels.add(SimplePanelConfigs.accountPanelConfig);
+            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.ONBOARDING3_C);
+            GeckoSharedPrefs.forProfile(context).edit().putString(Experiments.PREF_ONBOARDING_VERSION, Experiments.ONBOARDING3_C).apply();
         } else {
-            Log.d(LOGTAG, "Not in an experiment!");
-            panels.add(new FirstrunPanelConfig(WelcomePanel.class.getName(), WelcomePanel.TITLE_RES));
+            Log.e(LOGTAG, "Not in an experiment!");
+            panels.add(SimplePanelConfigs.signInPanelConfig);
         }
-
         return panels;
     }
 
-
     public static List<FirstrunPanelConfig> getRestricted() {
         final List<FirstrunPanelConfig> panels = new LinkedList<>();
         panels.add(new FirstrunPanelConfig(RestrictedWelcomePanel.class.getName(), RestrictedWelcomePanel.TITLE_RES));
         return panels;
     }
 
     public static class FirstrunPanelConfig {
 
@@ -95,15 +89,21 @@ public class FirstrunPagerConfig {
             return this.titleRes;
         }
 
         public Bundle getArgs() {
             return args;
         }
     }
 
-    protected static class SimplePanelConfigs {
+    private static class SimplePanelConfigs {
         public static final FirstrunPanelConfig urlbarPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_panel_title_welcome, R.drawable.firstrun_urlbar, R.string.firstrun_urlbar_message, R.string.firstrun_urlbar_subtext);
         public static final FirstrunPanelConfig bookmarksPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_bookmarks_title, R.drawable.firstrun_bookmarks, R.string.firstrun_bookmarks_message, R.string.firstrun_bookmarks_subtext);
+        public static final FirstrunPanelConfig dataPanelConfig = new FirstrunPanelConfig(DataPanel.class.getName(), R.string.firstrun_data_title, R.drawable.firstrun_data_off, R.string.firstrun_data_message, R.string.firstrun_data_subtext);
         public static final FirstrunPanelConfig syncPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_sync_title, R.drawable.firstrun_sync, R.string.firstrun_sync_message, R.string.firstrun_sync_subtext);
-        public static final FirstrunPanelConfig dataPanelConfig = new FirstrunPanelConfig(DataPanel.class.getName(), R.string.firstrun_data_title, R.drawable.firstrun_data_off, R.string.firstrun_data_message, R.string.firstrun_data_subtext);
+        public static final FirstrunPanelConfig signInPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.pref_sync, R.drawable.firstrun_signin, R.string.firstrun_signin_message, R.string.firstrun_welcome_button_browser);
+
+        public static final FirstrunPanelConfig tabqueuePanelConfig = new FirstrunPanelConfig(TabQueuePanel.class.getName(), R.string.firstrun_tabqueue_title, R.drawable.firstrun_tabqueue_off, R.string.firstrun_tabqueue_message_off, R.string.firstrun_tabqueue_subtext_off);
+        public static final FirstrunPanelConfig notificationsPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_notifications_title, R.drawable.firstrun_notifications, R.string.firstrun_notifications_message, R.string.firstrun_notifications_subtext);
+        public static final FirstrunPanelConfig readerviewPanelConfig = new FirstrunPanelConfig(FirstrunPanel.class.getName(), R.string.firstrun_readerview_title, R.drawable.firstrun_readerview, R.string.firstrun_readerview_message, R.string.firstrun_readerview_subtext);
+        public static final FirstrunPanelConfig accountPanelConfig = new FirstrunPanelConfig(SyncPanel.class.getName(), R.string.firstrun_account_title, R.drawable.firstrun_account, R.string.firstrun_account_message, R.string.firstrun_button_notnow);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/FirstrunPanel.java
@@ -24,17 +24,17 @@ import org.mozilla.gecko.TelemetryContra
  */
 public class FirstrunPanel extends Fragment {
 
     public static final int TITLE_RES = -1;
     protected boolean showBrowserHint = true;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
-        final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_fragment, container, false);
+        final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_basepanel_checkable_fragment, container, false);
         Bundle args = getArguments();
         if (args != null) {
             final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
             final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
             final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
 
             ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
             ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/SyncPanel.java
@@ -5,30 +5,39 @@
 
 package org.mozilla.gecko.firstrun;
 
 import android.content.Intent;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
 
 public class SyncPanel extends FirstrunPanel {
-    // XXX: To simplify localization, this uses the pref_sync string. If this is used in the final product, add a new string to Nightly.
-    public static final int TITLE_RES = R.string.pref_sync;
-
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
         final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_sync_fragment, container, false);
-        // TODO: Update id names.
+        final Bundle args = getArguments();
+        if (args != null) {
+            final int imageRes = args.getInt(FirstrunPagerConfig.KEY_IMAGE);
+            final int textRes = args.getInt(FirstrunPagerConfig.KEY_TEXT);
+            final int subtextRes = args.getInt(FirstrunPagerConfig.KEY_SUBTEXT);
+
+            ((ImageView) root.findViewById(R.id.firstrun_image)).setImageResource(imageRes);
+            ((TextView) root.findViewById(R.id.firstrun_text)).setText(textRes);
+            ((TextView) root.findViewById(R.id.welcome_browse)).setText(subtextRes);
+        }
+
         root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
                 showBrowserHint = false;
 
                 final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
                 intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_FIRSTRUN);
copy from mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java
copy to mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/DataPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/firstrun/TabQueuePanel.java
@@ -1,47 +1,92 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.firstrun;
 
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
 import android.os.Bundle;
-import android.util.Log;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.widget.SwitchCompat;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
+import android.widget.CompoundButton;
 import android.widget.ImageView;
+import android.widget.TextView;
 import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.tabqueue.TabQueueHelper;
+import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 
-public class DataPanel extends FirstrunPanel {
-    private boolean isEnabled = false;
+public class TabQueuePanel extends FirstrunPanel {
+    private static final int REQUEST_CODE_TAB_QUEUE = 1;
+    private SwitchCompat toggleSwitch;
+    private ImageView imageView;
+    private TextView messageTextView;
+    private TextView subtextTextView;
+    private Context context;
 
     @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
+    public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstance) {
+        context = getContext();
         final View root = super.onCreateView(inflater, container, savedInstance);
-        final ImageView clickableImage = (ImageView) root.findViewById(R.id.firstrun_image);
-        clickableImage.setOnClickListener(new View.OnClickListener() {
+
+        imageView = (ImageView) root.findViewById(R.id.firstrun_image);
+        messageTextView = (TextView) root.findViewById(R.id.firstrun_text);
+        subtextTextView = (TextView) root.findViewById(R.id.firstrun_subtext);
+
+        toggleSwitch = (SwitchCompat) root.findViewById(R.id.firstrun_switch);
+        toggleSwitch.setVisibility(View.VISIBLE);
+        toggleSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
             @Override
-            public void onClick(View view) {
-                // Set new state.
-                isEnabled = !isEnabled;
-                int newResource = isEnabled ? R.drawable.firstrun_data_on : R.drawable.firstrun_data_off;
-                ((ImageView) view).setImageResource(newResource);
-                if (isEnabled) {
-                    // Always block images.
-                    PrefsHelper.setPref("browser.image_blocking", 0);
-                } else {
-                    // Default: always load images.
-                    PrefsHelper.setPref("browser.image_blocking", 1);
+            public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions");
+                if (b && !TabQueueHelper.canDrawOverlays(context)) {
+                    Intent promptIntent = new Intent(context, TabQueuePrompt.class);
+                    startActivityForResult(promptIntent, REQUEST_CODE_TAB_QUEUE);
+                    return;
                 }
-                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-datasaving-" + isEnabled);
+
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-" + b);
+
+                final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
+                final SharedPreferences.Editor editor = prefs.edit();
+                editor.putBoolean(GeckoPreferences.PREFS_TAB_QUEUE, b).apply();
+
+                // Set image, text, and typeface changes.
+                imageView.setImageResource(b ? R.drawable.firstrun_tabqueue_on : R.drawable.firstrun_tabqueue_off);
+                messageTextView.setText(b ? R.string.firstrun_tabqueue_message_on : R.string.firstrun_tabqueue_message_off);
+                messageTextView.setTypeface(b ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT);
+                subtextTextView.setText(b ? R.string.firstrun_tabqueue_subtext_on : R.string.firstrun_tabqueue_subtext_off);
+                subtextTextView.setTypeface(b ? Typeface.defaultFromStyle(Typeface.ITALIC) : Typeface.DEFAULT);
+                subtextTextView.setTextColor(b ? ContextCompat.getColor(context, R.color.fennec_ui_orange) : ContextCompat.getColor(context, R.color.placeholder_grey));
             }
         });
 
         return root;
     }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        switch (requestCode) {
+            case REQUEST_CODE_TAB_QUEUE:
+                final boolean accepted = TabQueueHelper.processTabQueuePromptResponse(resultCode, context);
+                if (accepted) {
+                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-yes");
+                    toggleSwitch.setChecked(true);
+                    Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-tabqueue-true");
+                }
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.DIALOG, "firstrun_tabqueue-permissions-" + (accepted ? "accepted" : "rejected"));
+                break;
+        }
+    }
+
 }
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/firstrun/WelcomePanel.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.firstrun;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.fxa.FxAccountConstants;
-import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
-
-public class WelcomePanel extends FirstrunPanel {
-    public static final int TITLE_RES = R.string.firstrun_panel_title_welcome;
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
-        final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.firstrun_welcome_fragment, container, false);
-        root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-sync");
-                showBrowserHint = false;
-
-                final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
-                intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_FIRSTRUN);
-                intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
-                startActivity(intent);
-
-                close();
-            }
-        });
-
-        root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "firstrun-browser");
-                close();
-            }
-        });
-
-        return root;
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/BrowserSearch.java
@@ -181,17 +181,17 @@ public class BrowserSearch extends HomeF
 
     // Whether the suggestions will fade in when shown
     private boolean mAnimateSuggestions;
 
     // Opt-in prompt view for search suggestions
     private View mSuggestionsOptInPrompt;
 
     public interface OnSearchListener {
-        public void onSearch(SearchEngine engine, String text);
+        void onSearch(SearchEngine engine, String text, TelemetryContract.Method method);
     }
 
     public interface OnEditSuggestionListener {
         public void onEditSuggestion(String suggestion);
     }
 
     public static BrowserSearch newInstance() {
         BrowserSearch browserSearch = new BrowserSearch();
@@ -724,20 +724,19 @@ public class BrowserSearch extends HomeF
             mSearchEngineBar.setVisibility(View.VISIBLE);
         } else {
             mSearchEngineBar.setVisibility(View.GONE);
         }
     }
 
     @Override
     public void onSearchBarClickListener(final SearchEngine searchEngine) {
-        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM,
-                "searchenginebar");
-
-        mSearchListener.onSearch(searchEngine, mSearchTerm);
+        final TelemetryContract.Method method = TelemetryContract.Method.LIST_ITEM;
+        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, method, "searchenginebar");
+        mSearchListener.onSearch(searchEngine, mSearchTerm, method);
     }
 
     private void ensureSuggestClientIsSet(final String suggestTemplate) {
         // Don't update the suggestClient if we already have a client with the correct template
         if (mSuggestClient != null && suggestTemplate.equals(mSuggestClient.getSuggestTemplate())) {
             return;
         }
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
@@ -38,23 +38,23 @@ public final class HomeConfig {
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
      */
     @RobocopTarget
     public static enum PanelType implements Parcelable {
         TOP_SITES("top_sites", TopSitesPanel.class),
         BOOKMARKS("bookmarks", BookmarksPanel.class),
         COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
-        READING_LIST("reading_list", ReadingListPanel.class),
         RECENT_TABS("recent_tabs", RecentTabsPanel.class),
         DYNAMIC("dynamic", DynamicPanel.class),
         // Deprecated panels that should no longer exist but are kept around for
         // migration code. Class references have been replaced with new version of the panel.
         DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class),
-        DEPRECATED_HISTORY("history", CombinedHistoryPanel.class);
+        DEPRECATED_HISTORY("history", CombinedHistoryPanel.class),
+        DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class);
 
         private final String mId;
         private final Class<?> mPanelClass;
 
         PanelType(String id, Class<?> panelClass) {
             mId = id;
             mPanelClass = panelClass;
         }
@@ -1593,21 +1593,21 @@ public final class HomeConfig {
     // UUIDs used to create PanelConfigs for default built-in panels. These are
     // public because they can be used in "about:home?panel=UUID" query strings
     // to open specific panels without querying the active Home Panel
     // configuration. Because they don't consider the active configuration, it
     // is only sensible to do this for built-in panels (and not for dynamic
     // panels).
     private static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
     private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
-    private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
     private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
     private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6";
     private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
     private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0";
+    private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
 
     private final HomeConfigBackend mBackend;
 
     public HomeConfig(HomeConfigBackend backend) {
         mBackend = backend;
     }
 
     public State load() {
@@ -1634,26 +1634,24 @@ public final class HomeConfig {
     }
 
     public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) {
         switch (panelType) {
         case TOP_SITES:
             return R.string.home_top_sites_title;
 
         case BOOKMARKS:
+        case DEPRECATED_READING_LIST:
             return R.string.bookmarks_title;
 
         case DEPRECATED_HISTORY:
         case DEPRECATED_REMOTE_TABS:
         case COMBINED_HISTORY:
             return R.string.home_history_title;
 
-        case READING_LIST:
-            return R.string.reading_list_title;
-
         case RECENT_TABS:
             return R.string.recent_tabs_title;
 
         default:
             throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
         }
     }
 
@@ -1669,18 +1667,18 @@ public final class HomeConfig {
             return HISTORY_PANEL_ID;
 
         case COMBINED_HISTORY:
             return COMBINED_HISTORY_PANEL_ID;
 
         case DEPRECATED_REMOTE_TABS:
             return REMOTE_TABS_PANEL_ID;
 
-        case READING_LIST:
-            return READING_LIST_PANEL_ID;
+        case DEPRECATED_READING_LIST:
+            return DEPRECATED_READING_LIST_PANEL_ID;
 
         case RECENT_TABS:
             return RECENT_TABS_PANEL_ID;
 
         default:
             throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
         }
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -30,17 +30,17 @@ import android.content.SharedPreferences
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class HomeConfigPrefsBackend implements HomeConfigBackend {
     private static final String LOGTAG = "GeckoHomeConfigBackend";
 
     // Increment this to trigger a migration.
-    private static final int VERSION = 5;
+    private static final int VERSION = 6;
 
     // This key was originally used to store only an array of panel configs.
     public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
 
     // This key is now used to store a version number with the array of panel configs.
     public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
 
     // Keys used with JSON object stored in prefs.
@@ -71,17 +71,16 @@ public class HomeConfigPrefsBackend impl
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
                                                   EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
 
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS));
-        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.READING_LIST));
 
         return new State(panelConfigs, true);
     }
 
     /**
      * Iterate through the panels to check if they are all disabled.
      */
     private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
@@ -229,29 +228,71 @@ public class HomeConfigPrefsBackend impl
         }
 
         // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
         final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
         jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
     }
 
     /**
+     * Remove the reading list panel.
+     * If the reading list panel used to be the default panel, we make bookmarks the new default.
+     */
+    private static JSONArray removeReadingListPanel(Context context, JSONArray jsonPanels) throws JSONException {
+        boolean wasDefault = false;
+        int bookmarksIndex = -1;
+
+        // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
+        // the items we don't want deleted into a new array.
+        final JSONArray newJSONPanels = new JSONArray();
+
+        for (int i = 0; i < jsonPanels.length(); i++) {
+            final JSONObject panelJSON = jsonPanels.getJSONObject(i);
+            final PanelConfig panelConfig = new PanelConfig(panelJSON);
+
+            if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
+                // If this panel was the default we'll need to assign a new default:
+                wasDefault = panelConfig.isDefault();
+            } else {
+                if (panelConfig.getType() == PanelType.BOOKMARKS) {
+                    bookmarksIndex = newJSONPanels.length();
+                }
+
+                newJSONPanels.put(panelJSON);
+            }
+        }
+
+        if (wasDefault) {
+            // This will make the bookmarks panel visible if it was previously hidden - this is desired
+            // since this will make the new equivalent of the reading list visible by default.
+            final JSONObject bookmarksPanelConfig = createBuiltinPanelConfig(context, PanelType.BOOKMARKS, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
+            if (bookmarksIndex != -1) {
+                newJSONPanels.put(bookmarksIndex, bookmarksPanelConfig);
+            } else {
+                newJSONPanels.put(bookmarksPanelConfig);
+            }
+        }
+
+        return newJSONPanels;
+    }
+
+    /**
      * Checks to see if the reading list panel already exists.
      *
      * @param jsonPanels JSONArray array representing the curent set of panel configs.
      *
      * @return boolean Whether or not the reading list panel exists.
      */
     private static boolean readingListPanelExists(JSONArray jsonPanels) {
         final int count = jsonPanels.length();
         for (int i = 0; i < count; i++) {
             try {
                 final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
                 final PanelConfig panelConfig = new PanelConfig(jsonPanelConfig);
-                if (panelConfig.getType() == PanelType.READING_LIST) {
+                if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
                     return true;
                 }
             } catch (Exception e) {
                 // It's okay to ignore this exception, since an invalid reading list
                 // panel config is equivalent to no reading list panel.
                 Log.e(LOGTAG, "Exception loading PanelConfig from JSON", e);
             }
         }
@@ -318,34 +359,38 @@ public class HomeConfigPrefsBackend impl
                     break;
 
                 case 3:
                     // Add the "Reading List" panel if it does not exist. At one time,
                     // the Reading List panel was shown only to devices that were not
                     // considered "low memory". Now, we expose the panel to all devices.
                     // This migration should only occur for "low memory" devices.
                     // Note: This will not agree with the default configuration, which
-                    // has DEPRECATED_REMOTE_TABS after READING_LIST on some devices.
+                    // has DEPRECATED_REMOTE_TABS after DEPRECATED_READING_LIST on some devices.
                     if (!readingListPanelExists(jsonPanels)) {
                         addBuiltinPanelConfig(context, jsonPanels,
-                                PanelType.READING_LIST, Position.BACK, Position.BACK);
+                                PanelType.DEPRECATED_READING_LIST, Position.BACK, Position.BACK);
                     }
                     break;
 
                 case 4:
                     // Combine the History and Sync panels. In order to minimize an unexpected reordering
                     // of panels, we try to replace the History panel if it's visible, and fall back to
                     // the Sync panel if that's visible.
                     jsonPanels = combineHistoryAndSyncPanels(context, jsonPanels);
                     break;
 
                 case 5:
                     // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
                     ensureDefaultPanelForV5(context, jsonPanels);
                     break;
+
+                case 6:
+                    jsonPanels = removeReadingListPanel(context, jsonPanels);
+                    break;
             }
         }
 
         // Save the new panel config and the new version number.
         final JSONObject newJson = new JSONObject();
         newJson.put(JSON_KEY_PANELS, jsonPanels);
         newJson.put(JSON_KEY_VERSION, VERSION);
 
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/home/ReadingListPanel.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/* -*- 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.home;
-
-import android.os.Bundle;
-
-import java.util.EnumSet;
-import java.util.Locale;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.GeckoSharedPrefs;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SnackbarHelper;
-
-import android.support.design.widget.Snackbar;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * Fragment that used to display reading list contents in a ListView, and now directs
- * users to Bookmarks to view their former reading-list content.
- */
-public class ReadingListPanel extends HomeFragment {
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance) {
-        final ViewGroup root = (ViewGroup) inflater.inflate(R.layout.readinglistpanel_gone_fragment, container, false);
-
-        // We could update the ID names - however this panel is only intended to be live for one
-        // release, hence there's little utility in optimising this code.
-        root.findViewById(R.id.welcome_account).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                boolean bookmarksEnabled = GeckoSharedPrefs.forProfile(getContext()).getBoolean(HomeConfig.PREF_KEY_BOOKMARKS_PANEL_ENABLED, true);
-
-                if (bookmarksEnabled) {
-                    mUrlOpenListener.onUrlOpen("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS),
-                            EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
-                } else {
-                    SnackbarHelper.showSnackbar(getActivity(),
-                            getResources().getString(R.string.reading_list_migration_bookmarks_hidden),
-                            Snackbar.LENGTH_LONG);
-                }
-            }
-        });
-
-        root.findViewById(R.id.welcome_browse).setOnClickListener(new View.OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final String link = getString(R.string.migrated_reading_list_url,
-                        AppConstants.MOZ_APP_VERSION,
-                        AppConstants.OS_TARGET,
-                        Locales.getLanguageTag(Locale.getDefault()));
-
-                mUrlOpenListener.onUrlOpen(link,
-                        EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
-            }
-        });
-
-        return root;
-    }
-
-    @Override
-    protected void load() {
-        // Must be overriden, but we're not doing any loading hence no real implementation...
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngine.java
@@ -1,15 +1,16 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
+import android.support.annotation.NonNull;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.R;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -58,16 +59,17 @@ public class SearchEngine {
             return null;
         }
         return data.getString(key);
     }
 
     /**
      * @return a non-null string suitable for use by FHR.
      */
+    @NonNull
     public String getEngineIdentifier() {
         if (this.identifier != null) {
             return this.identifier;
         }
         if (this.name != null) {
             return "other-" + this.name;
         }
         return "other";
--- a/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/SearchEngineRow.java
@@ -111,17 +111,17 @@ class SearchEngineRow extends AnimatedHe
                         mUrlOpenListener.onUrlOpen(suggestion, EnumSet.noneOf(OnUrlOpenListener.Flags.class));
                     }
                 } else if (mSearchListener != null) {
                     if (v == mUserEnteredView) {
                         Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
                     } else {
                         Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, (String) v.getTag());
                     }
-                    mSearchListener.onSearch(mSearchEngine, suggestion);
+                    mSearchListener.onSearch(mSearchEngine, suggestion, TelemetryContract.Method.SUGGESTION);
                 }
             }
         };
 
         mLongClickListener = new OnLongClickListener() {
             @Override
             public boolean onLongClick(View v) {
                 if (mEditSuggestionListener != null) {
@@ -236,17 +236,17 @@ class SearchEngineRow extends AnimatedHe
 
     /**
      * Perform a search for the user-entered term.
      */
     public void performUserEnteredSearch() {
         String searchTerm = getSuggestionTextFromView(mUserEnteredView);
         if (mSearchListener != null) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.SUGGESTION, "user");
-            mSearchListener.onSearch(mSearchEngine, searchTerm);
+            mSearchListener.onSearch(mSearchEngine, searchTerm, TelemetryContract.Method.SUGGESTION);
         }
     }
 
     public void setSearchTerm(String searchTerm) {
         mUserEnteredTextView.setText(searchTerm);
 
         // mSearchEngine is not set in the first call to this method; the content description
         // is instead initially set in updateSuggestions().
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/UploadTelemetryCorePingCallback.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/UploadTelemetryCorePingCallback.java
@@ -10,16 +10,18 @@ import android.content.SharedPreferences
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.util.Log;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.search.SearchEngineManager;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 
 /**
@@ -72,18 +74,27 @@ public class UploadTelemetryCorePingCall
 
                 // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
                 final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(activity, profile.getName());
                 final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
                         .setClientID(clientID)
                         .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
                         .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
                         .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs));
-                final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
-                if (distributionId != null) {
-                    pingBuilder.setOptDistributionID(distributionId);
-                }
+                maybeSetOptionalMeasurements(sharedPrefs, pingBuilder);
 
                 activity.getTelemetryDispatcher().queuePingForUpload(activity, pingBuilder);
             }
         });
     }
+
+    private static void maybeSetOptionalMeasurements(final SharedPreferences sharedPrefs, final TelemetryCorePingBuilder pingBuilder) {
+        final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
+        if (distributionId != null) {
+            pingBuilder.setOptDistributionID(distributionId);
+        }
+
+        final ExtendedJSONObject searchCounts = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+        if (searchCounts.size() > 0) {
+            pingBuilder.setOptSearchCounts(searchCounts);
+        }
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SearchCountMeasurements.java
@@ -0,0 +1,100 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.measurements;
+
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A place to store and retrieve the number of times a user has searched with a specific engine from a
+ * specific location. This is designed for use as a telemetry core ping measurement.
+ *
+ * The implementation works by storing a preference for each engine-location pair and incrementing them
+ * each time {@link #incrementSearch(SharedPreferences, String, String)} is called. In order to
+ * retrieve the full set of keys later, we store all the available key names in another preference.
+ *
+ * When we retrieve the keys in {@link #getAndZeroSearch(SharedPreferences)} (using the set of keys
+ * preference), the values saved to the preferences are returned and the preferences are removed
+ * (i.e. zeroed) from Shared Preferences. The reason we remove the preferences (rather than actually
+ * zeroing them) is to avoid bloating shared preferences if 1) the set of engines ever changes or
+ * 2) we remove this feature.
+ *
+ * Since we increment a value on each successive search, which doesn't take up more space, we don't
+ * have to worry about using excess disk space if the measurements are never zeroed (e.g. telemetry
+ * upload is disabled). In the worst case, we overflow the integer and may return negative values.
+ *
+ * This class is thread-safe by locking access to its public methods. When this class was written, incrementing &
+ * retrieval were called from multiple threads so rather than enforcing the callers keep their threads straight, it
+ * was simpler to lock all access.
+ */
+public class SearchCountMeasurements {
+    /** The set of "engine + where" keys we've stored; used for retrieving stored engines. */
+    @VisibleForTesting static final String PREF_SEARCH_KEYSET = "measurements-search-count-keyset";
+    private static final String PREF_SEARCH_PREFIX = "measurements-search-count-engine-"; // + "engine.where"
+
+    private SearchCountMeasurements() {}
+
+    public static synchronized void incrementSearch(@NonNull final SharedPreferences prefs,
+            @NonNull final String engineIdentifier, @NonNull final String where) {
+        final String engineWhereStr = engineIdentifier + "." + where;
+        final String key = getEngineSearchCountKey(engineWhereStr);
+
+        final int count = prefs.getInt(key, 0);
+        prefs.edit().putInt(key, count + 1).apply();
+
+        unionKeyToSearchKeyset(prefs, engineWhereStr);
+    }
+
+    /**
+     * @param key Engine of the form, "engine.where"
+     */
+    private static void unionKeyToSearchKeyset(@NonNull final SharedPreferences prefs, @NonNull final String key) {
+        final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet());
+        if (keysFromPrefs.contains(key)) {
+            return;
+        }
+
+        // String set returned by shared prefs cannot be modified so we copy.
+        final Set<String> keysToSave = new HashSet<>(keysFromPrefs);
+        keysToSave.add(key);
+        prefs.edit().putStringSet(PREF_SEARCH_KEYSET, keysToSave).apply();
+    }
+
+    /**
+     * Gets and zeroes search counts.
+     *
+     * We return ExtendedJSONObject for now because that's the format needed by the core telemetry ping.
+     */
+    public static synchronized ExtendedJSONObject getAndZeroSearch(@NonNull final SharedPreferences prefs) {
+        final ExtendedJSONObject out = new ExtendedJSONObject();
+        final SharedPreferences.Editor editor = prefs.edit();
+
+        final Set<String> keysFromPrefs = prefs.getStringSet(PREF_SEARCH_KEYSET, Collections.<String>emptySet());
+        for (final String engineWhereStr : keysFromPrefs) {
+            final String key = getEngineSearchCountKey(engineWhereStr);
+            out.put(engineWhereStr, prefs.getInt(key, 0));
+            editor.remove(key);
+        }
+        editor.remove(PREF_SEARCH_KEYSET)
+                .apply();
+        return out;
+    }
+
+    /**
+     * @param engineWhereStr string of the form "engine.where"
+     * @return the key for the engines' search counts in shared preferences
+     */
+    @VisibleForTesting static String getEngineSearchCountKey(final String engineWhereStr) {
+        return PREF_SEARCH_PREFIX + engineWhereStr;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
@@ -13,16 +13,17 @@ import android.support.annotation.NonNul
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
 import android.text.TextUtils;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.search.SearchEngine;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.telemetry.TelemetryConstants;
 import org.mozilla.gecko.telemetry.TelemetryPing;
 import org.mozilla.gecko.util.DateUtil;
 import org.mozilla.gecko.util.Experiments;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -34,30 +35,31 @@ import java.util.concurrent.TimeUnit;
  * Builds a {@link TelemetryPing} representing a core ping.
  *
  * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
  * for details on the core ping.
  */
 public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
 
     private static final String NAME = "core";
-    private static final int VERSION_VALUE = 5; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
+    private static final int VERSION_VALUE = 6; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
     private static final String OS_VALUE = "Android";
 
     private static final String ARCHITECTURE = "arch";
     private static final String CLIENT_ID = "clientId";
     private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
     private static final String DEVICE = "device";
     private static final String DISTRIBUTION_ID = "distributionId";
     private static final String EXPERIMENTS = "experiments";
     private static final String LOCALE = "locale";
     private static final String OS_ATTR = "os";
     private static final String OS_VERSION = "osversion";
     private static final String PING_CREATION_DATE = "created";
     private static final String PROFILE_CREATION_DATE = "profileDate";
+    private static final String SEARCH_COUNTS = "searches";
     private static final String SEQ = "seq";
     private static final String TIMEZONE_OFFSET = "tz";
     private static final String VERSION_ATTR = "v";
 
     public TelemetryCorePingBuilder(final Context context) {
         initPayloadConstants(context);
     }
 
@@ -129,16 +131,30 @@ public class TelemetryCorePingBuilder ex
         if (distributionID == null) {
             throw new IllegalArgumentException("Expected non-null distribution ID");
         }
         payload.put(DISTRIBUTION_ID, distributionID);
         return this;
     }
 
     /**
+     * @param searchCounts non-empty JSON with {"engine.where": <int-count>}
+     */
+    public TelemetryCorePingBuilder setOptSearchCounts(@NonNull final ExtendedJSONObject searchCounts) {
+        if (searchCounts == null) {
+            throw new IllegalStateException("Expected non-null search counts");
+        } else if (searchCounts.size() == 0) {
+            throw new IllegalStateException("Expected non-empty search counts");
+        }
+
+        payload.put(SEARCH_COUNTS, searchCounts);
+        return this;
+    }
+
+    /**
      * @param date The profile creation date in days to the unix epoch (not millis!), or null if there is an error.
      */
     public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) {
         if (date != null && date < 0) {
             throw new IllegalArgumentException("Expect positive date value. Received: " + date);
         }
         payload.put(PROFILE_CREATION_DATE, date);
         return this;
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/stores/TelemetryJSONFilePingStore.java
@@ -145,16 +145,22 @@ public class TelemetryJSONFilePingStore 
     }
 
     /**
      * Logs if there is an error.
      *
      * @return the JSON object from the given file or null if there is an error.
      */
     private JSONObject lockAndReadJSONFromFile(final File file) {
+        // lockAndReadFileAndCloseStream doesn't handle file size of 0.
+        if (file.length() == 0) {
+            Log.w(LOGTAG, "Unexpected empty file: " + file.getName() + ". Ignoring");
+            return null;
+        }
+
         final FileInputStream inputStream;
         try {
             inputStream = new FileInputStream(file);
         } catch (final FileNotFoundException e) {
             throw new IllegalStateException("Expected file to exist");
         }
 
         final JSONObject obj;
--- a/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
@@ -32,19 +32,19 @@ public class Experiments {
 
     // Subscribe to known, bookmarked sites and show a notification if new content is available.
     public static final String CONTENT_NOTIFICATIONS_12HRS = "content-notifications-12hrs";
     public static final String CONTENT_NOTIFICATIONS_8AM = "content-notifications-8am";
     public static final String CONTENT_NOTIFICATIONS_5PM = "content-notifications-5pm";
 
     // Onboarding: "Features and Story". These experiments are determined
     // on the client, they are not part of the server config.
-    public static final String ONBOARDING2_A = "onboarding2-a"; // Control: Single (blue) welcome screen
-    public static final String ONBOARDING2_B = "onboarding2-b"; // 4 static Feature slides
-    public static final String ONBOARDING2_C = "onboarding2-c"; // 4 static + 1 clickable (Data saving) Feature slides
+    public static final String ONBOARDING3_A = "onboarding3-a"; // Control: No first run
+    public static final String ONBOARDING3_B = "onboarding3-b"; // 4 static Feature + 1 dynamic slides
+    public static final String ONBOARDING3_C = "onboarding3-c"; // Differentiating features slides
 
     // Synchronizing the catalog of downloadable content from Kinto
     public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
 
     // Promotion for "Add to homescreen"
     public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
 
     public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
@@ -90,22 +90,22 @@ public class Experiments {
     }
 
     /**
      * Returns if a user is in certain local experiment.
      * @param experiment Name of experiment to look up
      * @return returns value for experiment or false if experiment does not exist.
      */
     public static boolean isInExperimentLocal(Context context, String experiment) {
-        if (SwitchBoard.isInBucket(context, 0, 33)) {
-            return Experiments.ONBOARDING2_A.equals(experiment);
-        } else if (SwitchBoard.isInBucket(context, 33, 66)) {
-            return Experiments.ONBOARDING2_B.equals(experiment);
-        } else if (SwitchBoard.isInBucket(context, 66, 100)) {
-            return Experiments.ONBOARDING2_C.equals(experiment);
+        if (SwitchBoard.isInBucket(context, 0, 20)) {
+            return Experiments.ONBOARDING3_A.equals(experiment);
+        } else if (SwitchBoard.isInBucket(context, 20, 60)) {
+            return Experiments.ONBOARDING3_B.equals(experiment);
+        } else if (SwitchBoard.isInBucket(context, 60, 100)) {
+            return Experiments.ONBOARDING3_C.equals(experiment);
         } else {
             return false;
         }
     }
 
     /**
      * Returns list of all active experiments, remote and local.
      * @return List of experiment names Strings
--- a/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/UnusedResourcesUtil.java
@@ -8,16 +8,20 @@ import org.mozilla.gecko.R;
  */
 @SuppressWarnings("unused")
 final class UnusedResourcesUtil {
     public static final int[] CONSTANTS = {
             R.dimen.match_parent,
             R.dimen.wrap_content,
     };
 
+    public static final int[] USED_IN_BRANDING = {
+            R.drawable.large_icon
+    };
+
     public static final int[] USED_IN_COLOR_PALETTE = {
             R.color.private_browsing_purple, // This will be used eventually, then this item removed.
     };
 
     public static final int[] USED_IN_CRASH_REPORTER = {
             R.string.crash_allow_contact2,
             R.string.crash_close_label,
             R.string.crash_comment,
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -1,44 +1,58 @@
 <!-- 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/. -->
 
 <!ENTITY firstrun_panel_title_welcome "Welcome">
-<!ENTITY  onboard_start_message3 "Browse with &brandShortName;">
-<!ENTITY  onboard_start_subtext3 "Make your mobile Web browsing experience truly your own.">
 
 <!ENTITY firstrun_urlbar_message "Welcome to &brandShortName;">
 <!ENTITY firstrun_urlbar_subtext "Find things faster with helpful search suggestion shortcuts.">
 <!ENTITY firstrun_bookmarks_title "History">
 <!ENTITY firstrun_bookmarks_message "Your faves, front and center">
 <!ENTITY firstrun_bookmarks_subtext "Get results from your bookmarks and history when you search.">
 <!ENTITY firstrun_data_title "Data">
 <!ENTITY firstrun_data_message "Less data, more savings">
 <!ENTITY firstrun_data_subtext2 "Turn off images to spend less data on every site you visit.">
 <!ENTITY firstrun_sync_title "Sync">
 <!ENTITY firstrun_sync_message "&brandShortName;, always by your side">
 <!ENTITY firstrun_sync_subtext "Sync your tabs, passwords, and more everywhere you use it.">
 <!ENTITY firstrun_signin_message "Get connected, get started">
 <!ENTITY firstrun_signin_button "Sign in to Sync">
 <!ENTITY  onboard_start_button_browser "Start Browsing">
+<!ENTITY firstrun_button_notnow "Not right now">
 <!ENTITY firstrun_button_next "Next">
 
+<!ENTITY firstrun_tabqueue_title "Links">
+<!-- Localization note (firstrun_tabqueue_message): 'Tab queue' is a feature that allows users to queue up or save links from outside of Firefox (without switching apps) - these links will be loaded in Firefox the next time Firefox is opened. -->
+<!ENTITY firstrun_tabqueue_message_off "Turn on Tab queue">
+<!ENTITY firstrun_tabqueue_subtext_off "Save links for later in &brandShortName; when tapping them in other apps.">
+
+<!ENTITY firstrun_tabqueue_message_on "Success!">
+<!ENTITY firstrun_tabqueue_subtext_on "You can always turn this off in &settings; under &pref_category_general;.">
+
+<!ENTITY firstrun_notifications_title "Blogs">
+<!ENTITY firstrun_notifications_message "Stay informed">
+<!ENTITY firstrun_notifications_subtext "Get notified when blogs you have bookmarked post an update.">
+
+<!ENTITY firstrun_readerview_title "Articles">
+<!-- Localization note (firstrun_readerview_message): This is a casual way of describing getting rid of unnecessary things, and is referring to simplifying websites so only the article text and images are visible, removing unnecessary headers or ads. -->
+<!ENTITY firstrun_readerview_message "Lose the clutter">
+<!ENTITY firstrun_readerview_subtext "Use Reader View to make articles nicer to read \u2014 even offline.">
+
+<!-- Localization note (firstrun_devices_title): This is a casual way of addressing the user, somewhat referring to their online identity (which would include other devices, Firefox usage, accounts, etc). -->
+<!ENTITY firstrun_account_title "You">
+<!ENTITY firstrun_account_message "Have &brandShortName; on another device?">
+
 <!ENTITY  onboard_start_restricted1 "Stay safe and in control with this simplified version of &brandShortName;.">
 
-<!ENTITY  reading_list_migration_title "Reading List connected">
-<!ENTITY  reading_list_migration_subtext "Your Reading List items will now be added to your Bookmarks">
-<!ENTITY  reading_list_migration_goto_bookmarks "Go to Bookmarks">
-<!ENTITY  reading_list_migration_bookmarks_hidden "Your Bookmarks panel is hidden">
-
 <!-- Localization note: These are used as the titles of different pages on the home screen.
      They are automatically converted to all caps by the Android platform. -->
 <!ENTITY  bookmarks_title "Bookmarks">
 <!ENTITY  history_title "History">
-<!ENTITY  reading_list_title "Reading List">
 <!ENTITY  recent_tabs_title "Recent Tabs">
 
 <!ENTITY  switch_to_tab "Switch to tab">
 
 <!ENTITY  crash_reporter_title "&brandShortName; Crash Reporter">
 <!ENTITY  crash_message2 "&brandShortName; had a problem and crashed. Your tabs should be listed on the &brandShortName; Start page when you restart.">
 <!ENTITY  crash_send_report_message3 "Tell &vendorShortName; about this crash so they can fix it">
 <!ENTITY  crash_include_url2 "Include the address of the page I was on">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -308,17 +308,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'FindInPageBar.java',
     'firstrun/DataPanel.java',
     'firstrun/FirstrunAnimationContainer.java',
     'firstrun/FirstrunPager.java',
     'firstrun/FirstrunPagerConfig.java',
     'firstrun/FirstrunPanel.java',
     'firstrun/RestrictedWelcomePanel.java',
     'firstrun/SyncPanel.java',
-    'firstrun/WelcomePanel.java',
+    'firstrun/TabQueuePanel.java',
     'FormAssistPopup.java',
     'GeckoAccessibility.java',
     'GeckoActivity.java',
     'GeckoActivityStatus.java',
     'GeckoApp.java',
     'GeckoApplication.java',
     'GeckoAppShell.java',
     'GeckoBatteryManager.java',
@@ -421,17 +421,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'home/PanelLayout.java',
     'home/PanelListView.java',
     'home/PanelRecyclerView.java',
     'home/PanelRecyclerViewAdapter.java',
     'home/PanelRefreshLayout.java',
     'home/PanelViewAdapter.java',
     'home/PanelViewItemHandler.java',
     'home/PinSiteDialog.java',
-    'home/ReadingListPanel.java',
     'home/RecentTabsPanel.java',
     'home/RemoteTabsExpandableListState.java',
     'home/SearchEngine.java',
     'home/SearchEngineAdapter.java',
     'home/SearchEngineBar.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
@@ -571,16 +570,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/TabPanelBackButton.java',
     'tabs/TabsGridLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'Telemetry.java',
+    'telemetry/measurements/SearchCountMeasurements.java',
     'telemetry/pingbuilders/TelemetryCorePingBuilder.java',
     'telemetry/pingbuilders/TelemetryPingBuilder.java',
     'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java',
     'telemetry/schedulers/TelemetryUploadScheduler.java',
     'telemetry/stores/TelemetryJSONFilePingStore.java',
     'telemetry/stores/TelemetryPingStore.java',
     'telemetry/TelemetryConstants.java',
     'telemetry/TelemetryDispatcher.java',
deleted file mode 100644
index 69af116f2e23546f55ff9704d19268c7ea124f1d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index e25e7a962e0fd373a6544689ba66b1bf8d4832e7..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 48b2fc7abe80e7e8464f3813874563821a8e5b08..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index eb7701079520bb6edeec5cc654b87f899441505e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..38c77afaafbcc0636b569f7fd96adb5b0238d202
GIT binary patch
literal 8733
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%0A;8ZV|K5tuHyJiVHkbdq5E<vyp=Iin2P8*
zd7fxbmYE2_DuMC(UF}=n$7YX^cT;&S&D&0W9|>ZEJG#vGrfD7tgUKttS`9876*NW4
zn+TKMh@OO30OmFA)+{%GnDrV6g(IeKQ#`sEf_gAz`5l8Fl>MzYBR8B*DOYT1ASE~I
zUS@a&^H21Y`J9U7C+GWQ@G7g>9s{FLWd>1Jy{VUx!!$wrFy-p(c*Kl#Cps>cw_5Zd
zhP|<tfJO$dyn1C~?Mu;^CjjYI!y%UdcIb=Iz(dN@fg7aF`Ml<-mbdi}!=ieoW^ynS
z<0bosu>kBe(Ym*VZ(^Qxnr*>)s1d!-#-;eme=XK9mzq>gI{`y|EozGsMkNm^P;HLD
zP<iX8n5Z%>3e=26;`{j409oY^h%75FhoTHRXH|R9r<$GLLx8`gTj9$e$a(<vLD&F1
zw!d0k4IKPTq0^kEMakSZ=wr2jZKhoqFAC6uKicRXkD?oLfpD3ZZsC5*87VH^=y4B#
zo`^J(GZNl?Ox@3S1g=N}nrUs8aiqOvzc{=5-UCfcCLaKLf;UNe+B$AVeRKNq-S|9@
zQ~vo4Xp?Yy<!Q124SC*+6;<yrBN}J>%K<6bJtT4g^iRdBKK5w9gkbJT9N@rBJoG&V
zMC6P5!~}AZpX`0xc}rH+Y>l~B&2_Zmml;N8%C_>c!L}OHaj@mpr?o&Xer2J=Un{82
zDPMBO(olt@X(+wB?$6xsDEBW(D%R(0tkV`kNjuW=NcjA~psHtDw)WSD+Z^B|9)LN#
zUKt&x!YaeNYVX^oSpGF6+emsTmZ4<3r9BM{zvkZ*S2S6uF^Ac9N)}AkY*qZtzk{wt
z><Ez~&RlqieEVl>hE~EmZTkkB;0*;TZ7nqr@!+@^<!jwY|GIW`ZpZz5UZmx45ZRx%
z-*Hs*dN?wt=0m<dV-fJ1>xig6?97#l&dNyPXh3H_Ko{@X2VPK)eIF+&k=R*9?A!4-
z9hD^}!FRSXT&ENTe-z9SKKc)Mt<*j-FOwC*w3WJUCU!0A>nCCH2>kCpu}*9iuUP4=
zKu>3tM4sBcTjWJGfhe)-Mz^1dI^R)p(4b8!Laot4iT7fVVyww$v8JrVCH<>%N=n=@
zt=WCfN*F`4B$Bt1<<8xOyNR|U#;LiIS)QB&GHd>}QYuXG5<RkqprNa6c&A9~>AU{}
DuR6FO
deleted file mode 100644
index 770eeae967efcb4c022de61f84364d90c4594751..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f599d52af0eca4c0f26d2a2b9f797b7ca933f759
GIT binary patch
literal 10870
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
Xm!7dX30)p!q^14X;8D5y%Xj|=<P-Z#
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c611f08c9b22120cf5bc8aeecb759d0e634a182d
GIT binary patch
literal 9895
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
aGQ_Md2(_Q#(s=$au@@>YpH?VYg!~_X==G@p
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9e986685ad9370254e35a791edcdf296cce84db8
GIT binary patch
literal 8779
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
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5439f7a5c8d84f6d48d3b81e1508c9cf0d803acb
GIT binary patch
literal 9444
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
deleted file mode 100644
index cab7f2a3602653ecf593bec66fdbb4f562b75e6f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index f81bbb0ab76370cc3383397285b64c7ad5f6ab1e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
rename from mobile/android/base/resources/layout/firstrun_basepanel_fragment.xml
rename to mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml
--- a/mobile/android/base/resources/layout/firstrun_basepanel_fragment.xml
+++ b/mobile/android/base/resources/layout/firstrun_basepanel_checkable_fragment.xml
@@ -35,19 +35,22 @@
         <TextView android:id="@+id/firstrun_subtext"
                   android:layout_width="@dimen/firstrun_content_width"
                   android:layout_height="wrap_content"
                   android:paddingTop="20dp"
                   android:paddingBottom="30dp"
                   android:gravity="center"
                   android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"/>
 
-        <View android:layout_weight="1"
-              android:layout_height="0dp"
-              android:layout_width="match_parent"/>
+        <android.support.v7.widget.SwitchCompat
+                  android:id="@+id/firstrun_switch"
+                  android:layout_width="wrap_content"
+                  android:layout_height="0dp"
+                  android:layout_weight="1"
+                  android:visibility="invisible"/>
 
         <TextView android:id="@+id/firstrun_link"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:padding="30dp"
                   android:gravity="center"
                   android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
                   android:text="@string/firstrun_button_next"/>
--- a/mobile/android/base/resources/layout/firstrun_sync_fragment.xml
+++ b/mobile/android/base/resources/layout/firstrun_sync_fragment.xml
@@ -13,43 +13,42 @@
     <LinearLayout android:layout_width="match_parent"
                   android:layout_height="wrap_content"
                   android:minHeight="@dimen/firstrun_min_height"
                   android:background="@color/about_page_header_grey"
                   android:gravity="center_horizontal"
                   android:orientation="vertical">
 
 
-        <ImageView android:layout_width="wrap_content"
+        <ImageView android:id="@+id/firstrun_image"
+                   android:layout_width="wrap_content"
                    android:layout_height="@dimen/firstrun_background_height"
                    android:layout_marginTop="40dp"
                    android:layout_marginBottom="40dp"
                    android:scaleType="fitCenter"
                    android:layout_gravity="center"
-                   android:adjustViewBounds="true"
-                   android:src="@drawable/firstrun_signin"/>
+                   android:adjustViewBounds="true"/>
 
-        <TextView android:layout_width="@dimen/firstrun_content_width"
+        <TextView android:id="@+id/firstrun_text"
+                  android:layout_width="@dimen/firstrun_content_width"
                   android:layout_height="wrap_content"
                   android:gravity="center"
                   android:paddingBottom="40dp"
-                  android:textAppearance="@style/TextAppearance.FirstrunLight.Main"
-                  android:text="@string/firstrun_signin_message"/>
+                  android:textAppearance="@style/TextAppearance.FirstrunLight.Main"/>
 
         <Button android:id="@+id/welcome_account"
                 style="@style/Widget.Firstrun.Button"
                 android:background="@drawable/button_background_action_orange_round"
                 android:layout_gravity="center"
                 android:text="@string/firstrun_signin_button"/>
 
         <View android:layout_weight="1"
               android:layout_height="0dp"
               android:layout_width="match_parent"/>
 
         <TextView android:id="@+id/welcome_browse"
                   android:layout_width="wrap_content"
                   android:layout_height="wrap_content"
                   android:padding="30dp"
                   android:gravity="center"
-                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
-                  android:text="@string/firstrun_welcome_button_browser"/>
+                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"/>
     </LinearLayout>
 </ScrollView>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/firstrun_welcome_fragment.xml
+++ /dev/null
@@ -1,66 +0,0 @@
-<?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/. -->
-
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:orientation="vertical"
-            android:fillViewport="true">
-
-    <LinearLayout android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:minHeight="248dp"
-                  android:background="@color/android:white"
-                  android:gravity="center_horizontal"
-                  android:orientation="vertical">
-
-        <FrameLayout android:layout_width="match_parent"
-                     android:layout_height="wrap_content"
-                     android:background="@color/link_blue">
-
-            <ImageView android:layout_width="wrap_content"
-                       android:layout_height="248dp"
-                       android:scaleType="fitCenter"
-                       android:layout_gravity="center"
-                       android:adjustViewBounds="true"
-                       android:src="@drawable/firstrun_background_coffee"/>
-
-            <ImageView android:layout_width="@dimen/firstrun_brand_size"
-                       android:layout_height="@dimen/firstrun_brand_size"
-                       android:src="@drawable/large_icon"
-                       android:layout_gravity="center"/>
-        </FrameLayout>
-
-        <TextView android:layout_width="@dimen/firstrun_content_width"
-                  android:layout_height="wrap_content"
-                  android:gravity="center"
-                  android:paddingTop="30dp"
-                  android:textAppearance="@style/TextAppearance.FirstrunLight.Main"
-                  android:text="@string/firstrun_welcome_message"/>
-
-        <TextView android:layout_width="@dimen/firstrun_content_width"
-                  android:layout_height="wrap_content"
-                  android:paddingTop="20dp"
-                  android:paddingBottom="30dp"
-                  android:gravity="center"
-                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"
-                  android:text="@string/firstrun_welcome_subtext"/>
-
-        <Button android:id="@+id/welcome_account"
-                style="@style/Widget.Firstrun.Button"
-                android:background="@drawable/button_background_action_orange_round"
-                android:layout_gravity="center"
-                android:text="@string/firstrun_signin_button"/>
-
-        <TextView android:id="@+id/welcome_browse"
-                  android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:padding="20dp"
-                  android:gravity="center"
-                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
-                  android:text="@string/firstrun_welcome_button_browser"/>
-    </LinearLayout>
-</ScrollView>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/readinglistpanel_gone_fragment.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-<?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/. -->
-
-
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-            android:layout_height="wrap_content"
-            android:layout_width="match_parent"
-            android:orientation="vertical"
-            android:fillViewport="true">
-
-    <LinearLayout android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:minHeight="@dimen/firstrun_min_height"
-                  android:gravity="center_horizontal"
-                  android:orientation="vertical">
-
-        <ImageView android:layout_width="wrap_content"
-                   android:layout_height="wrap_content"
-                   android:layout_marginTop="40dp"
-                   android:layout_marginBottom="40dp"
-                   android:scaleType="fitCenter"
-                   android:layout_gravity="center"
-                   android:adjustViewBounds="true"
-                   android:src="@drawable/reading_list_migration"/>
-
-        <TextView android:layout_width="@dimen/firstrun_content_width"
-                  android:layout_height="wrap_content"
-                  android:gravity="center"
-                  android:textAppearance="@style/TextAppearance.FirstrunLight.Main"
-                  android:text="@string/reading_list_migration_title"/>
-
-        <TextView android:id="@+id/firstrun_subtext"
-                  android:layout_width="@dimen/firstrun_content_width"
-                  android:layout_height="wrap_content"
-                  android:paddingTop="20dp"
-                  android:paddingBottom="30dp"
-                  android:gravity="center"
-                  android:text="@string/reading_list_migration_subtext"
-                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Body"
-                  android:singleLine="false"/>
-
-        <Button android:id="@+id/welcome_account"
-                style="@style/Widget.Firstrun.Button"
-                android:background="@drawable/button_background_action_orange_round"
-                android:layout_gravity="center"
-                android:text="@string/reading_list_migration_goto_bookmarks"/>
-
-        <TextView android:id="@+id/welcome_browse"
-                  android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:padding="16dp"
-                  android:gravity="center"
-                  android:textAppearance="@style/TextAppearance.FirstrunRegular.Link"
-                  android:text="@string/pref_learn_more"/>
-    </LinearLayout>
-</ScrollView>
--- a/mobile/android/base/resources/values-large-v11/dimens.xml
+++ b/mobile/android/base/resources/values-large-v11/dimens.xml
@@ -19,17 +19,16 @@
 
     <dimen name="browser_toolbar_site_security_height">60dp</dimen>
     <dimen name="browser_toolbar_site_security_width">34dp</dimen>
     <dimen name="browser_toolbar_site_security_margin_right">1dp</dimen>
     <!-- We primarily use padding (instead of margins) to increase the hit area. -->
     <dimen name="browser_toolbar_site_security_padding_vertical">21dp</dimen>
     <dimen name="browser_toolbar_site_security_padding_horizontal">8dp</dimen>
 
-    <dimen name="firstrun_brand_size">72dp</dimen>
     <dimen name="firstrun_background_height">300dp</dimen>
 
     <dimen name="tabs_panel_indicator_width">72dp</dimen>
     <dimen name="tabs_panel_button_width">60dp</dimen>
     <dimen name="panel_grid_view_column_width">200dp</dimen>
 
     <dimen name="overlay_prompt_container_width">360dp</dimen>
 
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -56,17 +56,16 @@
          redesign sometime after this is written) you should increase this value to the largest
          commonly-used size of favicon and, performance permitting, fetch the remainder from the
          database. The largest available size is always stored in the database, regardless of this
          value.-->
     <dimen name="favicon_largest_interesting_size">32dp</dimen>
 
     <dimen name="firstrun_content_width">300dp</dimen>
     <dimen name="firstrun_min_height">180dp</dimen>
-    <dimen name="firstrun_brand_size">48dp</dimen>
     <dimen name="firstrun_background_height">180dp</dimen>
 
     <dimen name="overlay_prompt_content_width">260dp</dimen>
     <dimen name="overlay_prompt_button_width">148dp</dimen>
     <dimen name="overlay_prompt_container_width">@dimen/match_parent</dimen>
 
     <!-- Site security icon -->
     <dimen name="browser_toolbar_site_security_height">32dp</dimen>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -25,45 +25,57 @@
   <string name="moz_android_shared_fxaccount_type">@ANDROID_PACKAGE_NAME@_fxaccount</string>
   <string name="android_package_name_for_ui">@ANDROID_PACKAGE_NAME@</string>
 
 #include ../search/strings/search_strings.xml.in
 
 #include ../services/strings.xml.in
 
   <string name="firstrun_panel_title_welcome">&firstrun_panel_title_welcome;</string>
-  <string name="firstrun_welcome_message">&onboard_start_message3;</string>
-  <string name="firstrun_welcome_subtext">&onboard_start_subtext3;</string>
 
   <string name="firstrun_urlbar_message">&firstrun_urlbar_message;</string>
   <string name="firstrun_urlbar_subtext">&firstrun_urlbar_subtext;</string>
   <string name="firstrun_bookmarks_title">&firstrun_bookmarks_title;</string>
   <string name="firstrun_bookmarks_message">&firstrun_bookmarks_message;</string>
   <string name="firstrun_bookmarks_subtext">&firstrun_bookmarks_subtext;</string>
   <string name="firstrun_data_title">&firstrun_data_title;</string>
   <string name="firstrun_data_message">&firstrun_data_message;</string>
   <string name="firstrun_data_subtext">&firstrun_data_subtext2;</string>
   <string name="firstrun_sync_title">&firstrun_sync_title;</string>
   <string name="firstrun_sync_message">&firstrun_sync_message;</string>
   <string name="firstrun_sync_subtext">&firstrun_sync_subtext;</string>
   <string name="firstrun_signin_message">&firstrun_signin_message;</string>
   <string name="firstrun_signin_button">&firstrun_signin_button;</string>
   <string name="firstrun_welcome_button_browser">&onboard_start_button_browser;</string>
+  <string name="firstrun_button_notnow">&firstrun_button_notnow;</string>
   <string name="firstrun_button_next">&firstrun_button_next;</string>
 
+  <string name="firstrun_tabqueue_title">&firstrun_tabqueue_title;</string>
+  <string name="firstrun_tabqueue_message_off">&firstrun_tabqueue_message_off;</string>
+  <string name="firstrun_tabqueue_subtext_off">&firstrun_tabqueue_subtext_off;</string>
+  <string name="firstrun_tabqueue_message_on">&firstrun_tabqueue_message_on;</string>
+  <string name="firstrun_tabqueue_subtext_on">&firstrun_tabqueue_subtext_on;</string>
+
+  <string name="firstrun_notifications_title">&firstrun_notifications_title;</string>
+  <string name="firstrun_notifications_message">&firstrun_notifications_message;</string>
+  <string name="firstrun_notifications_subtext">&firstrun_notifications_subtext;</string>
+
+  <string name="firstrun_readerview_title">&firstrun_readerview_title;</string>
+  <string name="firstrun_readerview_message">&firstrun_readerview_message;</string>
+  <string name="firstrun_readerview_subtext">&firstrun_readerview_subtext;</string>
+
+  <string name="firstrun_account_title">&firstrun_account_title;</string>
+  <string name="firstrun_account_message">&firstrun_account_message;</string>
+
   <string name="firstrun_welcome_restricted">&onboard_start_restricted1;</string>
 
   <string name="bookmarks_title">&bookmarks_title;</string>
   <string name="history_title">&history_title;</string>
-  <string name="reading_list_title">&reading_list_title;</string>
   <string name="recent_tabs_title">&recent_tabs_title;</string>
 
-  <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/reading-list -->
-  <string name="migrated_reading_list_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/reading-list</string>
-
   <string name="switch_to_tab">&switch_to_tab;</string>
 
   <string name="crash_reporter_title">&crash_reporter_title;</string>
   <string name="crash_message2">&crash_message2;</string>
   <string name="crash_send_report_message3">&crash_send_report_message3;</string>
   <string name="crash_include_url2">&crash_include_url2;</string>
   <string name="crash_sorry">&crash_sorry;</string>
   <string name="crash_comment">&crash_comment;</string>
@@ -370,21 +382,16 @@
   <string name="tabs_normal">&tabs_normal;</string>
   <string name="tabs_private">&tabs_private;</string>
   <string name="edit_mode_cancel">&edit_mode_cancel;</string>
 
   <string name="site_settings_title">&site_settings_title3;</string>
   <string name="site_settings_cancel">&site_settings_cancel;</string>
   <string name="site_settings_clear">&site_settings_clear;</string>
 
-  <string name="reading_list_migration_title">&reading_list_migration_title;</string>
-  <string name="reading_list_migration_subtext">&reading_list_migration_subtext;</string>
-  <string name="reading_list_migration_goto_bookmarks">&reading_list_migration_goto_bookmarks;</string>
-  <string name="reading_list_migration_bookmarks_hidden">&reading_list_migration_bookmarks_hidden;</string>
-
   <string name="page_action_dropmarker_description">&page_action_dropmarker_description;</string>
 
   <string name="contextmenu_open_new_tab">&contextmenu_open_new_tab;</string>
   <string name="contextmenu_open_private_tab">&contextmenu_open_private_tab;</string>
   <string name="contextmenu_remove">&contextmenu_remove;</string>
   <string name="contextmenu_add_to_launcher">&contextmenu_add_to_launcher;</string>
   <string name="contextmenu_share">&contextmenu_share;</string>
   <string name="contextmenu_pasteandgo">&contextmenu_pasteandgo;</string>
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -1,29 +1,43 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
+                                  "resource://devtools/shared/event-emitter.js");
+
 // Import the android PageActions module.
 XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
                                   "resource://gre/modules/PageActions.jsm");
 
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+
+var {
+  SingletonEventManager,
+} = ExtensionUtils;
+
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
 function PageAction(options, extension) {
   this.id = null;
 
   let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
 
   this.options = {
     title: options.default_title || extension.name,
     icon: DEFAULT_ICON,
     id: extension.id,
+    clickCallback: () => {
+      this.emit("click");
+    },
   };
+
+  EventEmitter.decorate(this);
 }
 
 PageAction.prototype = {
   show(tabId) {
     // TODO: Only show the PageAction for the tab with the provided tabId.
     if (!this.id) {
       this.id = PageActions.add(this.options);
     }
@@ -49,14 +63,24 @@ extensions.on("shutdown", (type, extensi
     pageActionMap.delete(extension);
   }
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
   return {
     pageAction: {
+      onClicked: new SingletonEventManager(context, "pageAction.onClicked", fire => {
+        let listener = (event) => {
+          fire();
+        };
+        pageActionMap.get(extension).on("click", listener);
+        return () => {
+          pageActionMap.get(extension).off("click", listener);
+        };
+      }).api(),
+
       show(tabId) {
         pageActionMap.get(extension).show(tabId);
       },
     },
   };
 });
--- a/mobile/android/components/extensions/schemas/page_action.json
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -207,17 +207,16 @@
             ]
           }
         ]
       }
     ],
     "events": [
       {
         "name": "onClicked",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a page action icon is clicked.  This event will not fire if the page action has a popup.",
         "parameters": [
           {
             "name": "tab",
             "$ref": "tabs.Tab"
           }
         ]
--- a/mobile/android/components/extensions/test/mochitest/.eslintrc
+++ b/mobile/android/components/extensions/test/mochitest/.eslintrc
@@ -1,7 +1,8 @@
 {
   "extends": "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc",
 
   "globals": {
     "isPageActionShown": true,
+    "clickPageAction": true,
   },
 }
--- a/mobile/android/components/extensions/test/mochitest/head.js
+++ b/mobile/android/components/extensions/test/mochitest/head.js
@@ -1,11 +1,15 @@
 "use strict";
 
-/* exported isPageActionShown */
+/* exported isPageActionShown clickPageAction */
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/PageActions.jsm");
 
 function isPageActionShown(extensionId) {
   return PageActions.isShown(extensionId);
 }
+
+function clickPageAction(extensionId) {
+  PageActions.synthesizeClick(extensionId);
+}
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -18,17 +18,20 @@ function backgroundScript() {
   browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
 
   // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
   let tabId = 1;
 
   browser.pageAction.show(tabId);
   browser.test.sendMessage("page-action-shown");
 
-  browser.test.notifyPass("page-action");
+  browser.pageAction.onClicked.addListener(tab => {
+    // TODO: Make sure we get the correct tab once the tabs API is supported.
+    browser.test.notifyPass("pageAction-clicked");
+  });
 }
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension({
     background: "(" + backgroundScript.toString() + ")()",
     manifest: {
       "name": "PageAction Extension",
       "page_action": {
@@ -37,17 +40,19 @@ add_task(function* test_contentscript() 
     },
   });
 
   yield extension.startup();
   yield extension.awaitMessage("page-action-shown");
 
   is(isPageActionShown(extension.id), true, "The PageAction should be shown");
 
-  yield extension.awaitFinish("page-action");
+  clickPageAction(extension.id);
+
+  yield extension.awaitFinish("pageAction-clicked");
   yield extension.unload();
 
   is(isPageActionShown(extension.id), false, "The PageAction should be removed after unload");
 });
 </script>
 
 </body>
 </html>
--- a/mobile/android/modules/PageActions.jsm
+++ b/mobile/android/modules/PageActions.jsm
@@ -26,17 +26,16 @@ function resolveGeckoURI(aURI) {
     return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
   } else if (aURI.startsWith("resource://")) {
     let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
     return handler.resolveURI(Services.io.newURI(aURI, null, null));
   }
   return aURI;
 }
 
-
 var PageActions = {
   _items: { },
 
   _inited: false,
 
   _maybeInit: function() {
     if (!this._inited && Object.keys(this._items).length > 0) {
       this._inited = true;
@@ -65,16 +64,23 @@ var PageActions = {
       }
     }
   },
 
   isShown: function(id) {
     return !!this._items[id];
   },
 
+  synthesizeClick: function(id) {
+    let item = this._items[id];
+    if (item && item.clickCallback) {
+      item.clickCallback();
+    }
+  },
+
   add: function(aOptions) {
     let id = aOptions.id || uuidgen.generateUUID().toString()
 
     Messaging.sendRequest({
       type: "PageActions:Add",
       id: id,
       title: aOptions.title,
       icon: resolveGeckoURI(aOptions.icon),
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSearchCountMeasurements.java
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.telemetry.measurements;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for the class that stores search count measurements.
+ */
+@RunWith(TestRunner.class)
+public class TestSearchCountMeasurements {
+
+    private SharedPreferences sharedPrefs;
+
+    @Before
+    public void setUp() throws Exception {
+        sharedPrefs = RuntimeEnvironment.application.getSharedPreferences(
+                TestSearchCountMeasurements.class.getSimpleName(), Context.MODE_PRIVATE);
+    }
+
+    private void assertNewValueInsertedNoIncrementedValues(final int expectedKeyCount) {
+        assertEquals("Shared prefs key count has incremented", expectedKeyCount, sharedPrefs.getAll().size());
+        assertTrue("Shared prefs still contains non-incremented initial value", sharedPrefs.getAll().containsValue(1));
+        assertFalse("Shared prefs has not incremented any values", sharedPrefs.getAll().containsValue(2));
+    }
+
+    @Test
+    public void testIncrementSearchCanRecreateEngineAndWhere() throws Exception {
+        final String expectedIdentifier = "google";
+        final String expectedWhere = "suggestbar";
+
+        SearchCountMeasurements.incrementSearch(sharedPrefs, expectedIdentifier, expectedWhere);
+        assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+        assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+        boolean foundEngine = false;
+        for (final String key : sharedPrefs.getAll().keySet()) {
+            // We could try to match the exact key, but that's more fragile.
+            if (key.contains(expectedIdentifier) && key.contains(expectedWhere)) {
+                foundEngine = true;
+            }
+        }
+        assertTrue("SharedPrefs keyset contains enough info to recreate engine & where", foundEngine);
+    }
+
+    @Test
+    public void testIncrementSearchCalledMultipleTimesSameEngine() throws Exception {
+        final String engineIdentifier = "whatever";
+        final String where = "wherever";
+
+        SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where);
+        assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+        assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+        // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+        // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+        final int keyCountAfterFirst = sharedPrefs.getAll().size();
+        for (int i = 2; i <= 3; ++i) {
+            SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdentifier, where);
+            assertEquals("Shared prefs key count has not changed", keyCountAfterFirst, sharedPrefs.getAll().size());
+            assertTrue("Shared prefs incremented", sharedPrefs.getAll().containsValue(i));
+        }
+    }
+
+    @Test
+    public void testIncrementSearchCalledMultipleTimesSameEngineDifferentWhere() throws Exception {
+        final String engineIdenfitier = "whatever";
+
+        SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "one place");
+        assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+        assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+        // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+        // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+        final int keyCountAfterFirst = sharedPrefs.getAll().size();
+        for (int i = 1; i <= 2; ++i) {
+            SearchCountMeasurements.incrementSearch(sharedPrefs, engineIdenfitier, "another place " + i);
+            assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i);
+        }
+    }
+
+    @Test
+    public void testIncrementSearchCalledMultipleTimesDifferentEngines() throws Exception {
+        final String where = "wherever";
+
+        SearchCountMeasurements.incrementSearch(sharedPrefs, "steam engine", where);
+        assertFalse("Shared prefs has some values", sharedPrefs.getAll().isEmpty());
+        assertTrue("Shared prefs contains initial value", sharedPrefs.getAll().containsValue(1));
+
+        // The initial key count storage saves metadata so we can't verify the number of keys is only 1. However,
+        // we assume subsequent calls won't add additional metadata and use it to verify the key count.
+        final int keyCountAfterFirst = sharedPrefs.getAll().size();
+        for (int i = 1; i <= 2; ++i) {
+            SearchCountMeasurements.incrementSearch(sharedPrefs, "combustion engine" + i, where);
+            assertNewValueInsertedNoIncrementedValues(keyCountAfterFirst + i);
+        }
+    }
+
+    @Test // assumes the format saved in SharedPrefs to store test data
+    public void testGetAndZeroSearchDeletesPrefs() throws Exception {
+        assertTrue("Shared prefs is empty", sharedPrefs.getAll().isEmpty());
+
+        final SharedPreferences.Editor editor = sharedPrefs.edit();
+        final Set<String> engineKeys = new HashSet<>(Arrays.asList("whatever.yeah", "lol.what"));
+        editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, engineKeys);
+        for (final String key : engineKeys) {
+            editor.putInt(getEngineSearchCountKey(key), 1);
+        }
+        editor.apply();
+        assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty());
+
+        SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+        assertTrue("Shared prefs is empty after zero", sharedPrefs.getAll().isEmpty());
+    }
+
+    @Test // assumes the format saved in SharedPrefs to store test data
+    public void testGetAndZeroSearchVerifyReturnedData() throws Exception {
+        final HashMap<String, Integer> expected = new HashMap<>();
+        expected.put("steamengine.here", 1337);
+        expected.put("combustionengine.there", 10);
+
+        final SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putStringSet(SearchCountMeasurements.PREF_SEARCH_KEYSET, expected.keySet());
+        for (final String key : expected.keySet()) {
+            editor.putInt(SearchCountMeasurements.getEngineSearchCountKey(key), expected.get(key));
+        }
+        editor.apply();
+        assertFalse("Shared prefs is not empty after test data inserted", sharedPrefs.getAll().isEmpty());
+
+        final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+        assertEquals("Returned JSON contains number of items inserted", expected.size(), actual.size());
+        for (final String key : expected.keySet()) {
+            assertEquals("Returned JSON contains inserted value", expected.get(key), (Integer) actual.getIntegerSafely(key));
+        }
+    }
+
+    @Test
+    public void testGetAndZeroSearchNoData() throws Exception {
+        final ExtendedJSONObject actual = SearchCountMeasurements.getAndZeroSearch(sharedPrefs);
+        assertEquals("Returned json is empty", 0, actual.size());
+    }
+
+    private String getEngineSearchCountKey(final String engineWhereStr) {
+        return SearchCountMeasurements.getEngineSearchCountKey(engineWhereStr);
+    }
+}
\ No newline at end of file
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/stores/TestTelemetryJSONFilePingStore.java
@@ -115,16 +115,24 @@ public class TestTelemetryJSONFilePingSt
 
         final ArrayList<TelemetryPing> pings = testStore.getAllPings();
         for (final TelemetryPing ping : pings) {
             assertEquals("Expected url path value received", urlPrefix + ping.getDocID(), ping.getURLPath());
             assertIsGeneratedPayload(ping.getPayload());
         }
     }
 
+    @Test // regression test: bug 1272817
+    public void testGetAllPingsHandlesEmptyFiles() throws Exception {
+        final int expectedPingCount = 3;
+        writeTestPingsToStore(expectedPingCount, "whatever");
+        assertTrue("Empty file is created", testStore.getPingFile(getDocID()).createNewFile());
+        assertEquals("Returned pings only contains valid files", expectedPingCount, testStore.getAllPings().size());
+    }
+
     @Test
     public void testMaybePrunePingsDoesNothingIfAtMax() throws Exception {
         final int pingCount = TelemetryJSONFilePingStore.MAX_PING_COUNT;
         writeTestPingsToStore(pingCount, "whatever");
         assertStoreFileCount(pingCount);
         testStore.maybePrunePings();
         assertStoreFileCount(pingCount);
     }
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/AboutHomeTest.java
@@ -28,23 +28,21 @@ import com.robotium.solo.Condition;
  * To use any of these methods in your test make sure it extends AboutHomeTest instead of BaseTest
  */
 abstract class AboutHomeTest extends PixelTest {
     protected enum AboutHomeTabs {
         RECENT_TABS,
         HISTORY,
         TOP_SITES,
         BOOKMARKS,
-        READING_LIST
     };
 
     private final ArrayList<String> aboutHomeTabs = new ArrayList<String>() {{
                   add("TOP_SITES");
                   add("BOOKMARKS");
-                  add("READING_LIST");
               }};
 
 
     @Override
     public void setUp() throws Exception {
         super.setUp();
 
         if (aboutHomeTabs.size() < 4) {
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/StringHelper.java
@@ -112,17 +112,16 @@ public class StringHelper {
     public final String BRAND_NAME = "(Fennec|Nightly|Aurora|Firefox Beta|Firefox)";
     public final String ABOUT_LABEL = "About " + BRAND_NAME ;
     public final String LOCATION_SERVICES_LABEL = "Mozilla Location Service";
 
     // Labels for the about:home tabs
     public final String HISTORY_LABEL;
     public final String TOP_SITES_LABEL;
     public final String BOOKMARKS_LABEL;
-    public final String READING_LIST_LABEL;
     public final String TODAY_LABEL;
 
     // Desktop default bookmarks folders
     public final String BOOKMARKS_UP_TO;
     public final String BOOKMARKS_ROOT_LABEL;
     public final String DESKTOP_FOLDER_LABEL;
     public final String TOOLBAR_FOLDER_LABEL;
     public final String BOOKMARKS_MENU_FOLDER_LABEL;
@@ -285,17 +284,16 @@ public class StringHelper {
         // Settings menu strings
         PRIVACY_SECTION_LABEL = res.getString(R.string.pref_category_privacy_short);
         MOZILLA_SECTION_LABEL = res.getString(R.string.pref_category_vendor);
 
         // Labels for the about:home tabs
         HISTORY_LABEL = res.getString(R.string.home_history_title);
         TOP_SITES_LABEL = res.getString(R.string.home_top_sites_title);
         BOOKMARKS_LABEL = res.getString(R.string.bookmarks_title);
-        READING_LIST_LABEL = res.getString(R.string.reading_list_title);
         TODAY_LABEL = res.getString(R.string.history_today_section);
 
         BOOKMARKS_UP_TO = res.getString(R.string.home_move_back_to_filter);
         BOOKMARKS_ROOT_LABEL = res.getString(R.string.bookmarks_title);
         DESKTOP_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_desktop);
         TOOLBAR_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_toolbar);
         BOOKMARKS_MENU_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_menu);
         UNSORTED_FOLDER_LABEL = res.getString(R.string.bookmarks_folder_unfiled);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
@@ -30,18 +30,17 @@ import com.robotium.solo.Solo;
  */
 public class AboutHomeComponent extends BaseComponent {
     private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
 
     private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
             PanelType.TOP_SITES,
             PanelType.BOOKMARKS,
             PanelType.COMBINED_HISTORY,
-            PanelType.RECENT_TABS,
-            PanelType.READING_LIST
+            PanelType.RECENT_TABS
     );
 
     // The percentage of the panel to swipe between 0 and 1. This value was set through
     // testing: 0.55f was tested on try and fails on armv6 devices.
     private static final float SWIPE_PERCENTAGE = 0.70f;
 
     public AboutHomeComponent(final UITestContext testContext) {
         super(testContext);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
@@ -1,15 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
-import org.mozilla.gecko.home.HomeConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.GeckoHelper;
 
 /**
  * Tests functionality related to navigating between the various about:home panels.
  *
  * TODO: Update this test to account for recent tabs panel (bug 1028727).
@@ -22,19 +21,16 @@ public class testAboutHomePageNavigation
         GeckoHelper.blockForDelayedStartup();
 
         mAboutHome.assertVisible()
                   .assertCurrentPanel(PanelType.TOP_SITES);
 
         mAboutHome.swipeToPanelOnRight();
         mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
 
-        mAboutHome.swipeToPanelOnRight();
-        mAboutHome.assertCurrentPanel(PanelType.READING_LIST);
-
         // Ideally these helpers would just be their own tests. However, by keeping this within
         // one method, we're saving test setUp and tearDown resources.
         if (DeviceHelper.isTablet()) {
             helperTestTablet();
         } else {
             helperTestPhone();
         }
     }
@@ -43,34 +39,28 @@ public class testAboutHomePageNavigation
         mAboutHome.swipeToPanelOnRight();
         mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
 
         // Edge case.
         mAboutHome.swipeToPanelOnRight();
         mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
 
         mAboutHome.swipeToPanelOnLeft();
-        mAboutHome.assertCurrentPanel(PanelType.READING_LIST);
-
-        mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
 
         mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
 
         // Edge case.
         mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
     }
 
     private void helperTestPhone() {
         // Edge case.
-        mAboutHome.swipeToPanelOnRight();
-        mAboutHome.assertCurrentPanel(PanelType.READING_LIST);
-
         mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.BOOKMARKS);
 
         mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.TOP_SITES);
 
         mAboutHome.swipeToPanelOnLeft();
         mAboutHome.assertCurrentPanel(PanelType.COMBINED_HISTORY);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testShareLink.java
@@ -29,17 +29,17 @@ public class testShareLink extends About
     String urlTitle = mStringHelper.ROBOCOP_BIG_LINK_TITLE;
 
     public void testShareLink() {
         url = getAbsoluteUrl(mStringHelper.ROBOCOP_BIG_LINK_URL);
         ArrayList<String> shareOptions;
         blockForGeckoReady();
 
         // FIXME: This is a temporary hack workaround for a permissions problem.
-        openAboutHomeTab(AboutHomeTabs.READING_LIST);
+        openAboutHomeTab(AboutHomeTabs.HISTORY);
 
         inputAndLoadUrl(url);
         verifyUrlBarTitle(url); // Waiting for page title to ensure the page is loaded
 
         selectMenuItem(mStringHelper.SHARE_LABEL);
         if (Build.VERSION.SDK_INT >= 14) {
             // Check for our own sync in the submenu.
             waitForText("Sync$");
--- a/mobile/locales/en-US/searchplugins/amazondotcom.xml
+++ b/mobile/locales/en-US/searchplugins/amazondotcom.xml
@@ -1,15 +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/. -->
 
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>Amazon.com</ShortName>
 <InputEncoding>ISO-8859-1</InputEncoding>
 <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAAAXNSR0IArs4c6QAACixJREFUeAHtXQ1MldcZfuRXlIr4A1MoYAf+xKBswlKmdTJ1Q7RZdc5kM7XWsXaGWk2WTDPc0qlzOmZqnHGZoxrntuiydB1UXZSRqS1h03WTVdDa+osov/I3qAicve93ufTC/eHc673f+ejOmxzu933n/c77nuc5/+fcC/CJjKDLXArlFFopCB38igFjytgyxoz1APkM3ZVQ0KCbgwFjzZgbwmxo8M0B3rGAM+YjgunPdyhspKDFXASeInPVTMABCvHm2tbW+hCI5eaHO4cnNCRKEGhjArhd0qIIgSBFdrXZPgQ0AYqLgiZAE6AYAcXmdQ3QBChGQLF5XQM0AYoRUGxe1wBNgGIEFJvXNUAToBgBxeZ1DdAEKEZAsfkQxfZ9Nh8SEoKwsDD09PTg4cOHPqej+sVhQcC4ceOwdOlSZGRkICUlxQhJSUkIDuYNPaCpqQm3bt3CzZs3UVlZiVOnTqG8vNwgRzXAMvYdN4otdT1//nxBYIpHjx4Jb6W2tlbk5+eLqKgoS+WJCBnsj9ODwQqm38fHx4uSkhJvMXep39zcLNauXWt6HlwA7c4HaxGwZMkS0dDQ4BLMx3m4b98+dwCofm4dAhYuXOhTcyNLzObNm1WD7cq+NQiYNm2a4OYikNLZ2SmSk5NdgaDymTUIOHnyZCCx70/70KFDKsF2sm2JYyk02sHZs2dlRmyoqanB4cOHcfnyZbS2tmLq1KlYs2YN0tLSpN5vbGxEbGyspYaoTqx40YP75d2jR4/2l1BPFzt37hQjR450shkUFCR27drl6dUBcenp6U5pmJ1nB3tqmyAGr76+fgBArm4KCgqGBI0mX65edXq2YsWKIdNyACigusoX41JTUzFhwgTKr3uhzhnbtm1zr9AXc+TIkSF1WIHmGVJ6ZigpJ+DatWtG+9/d3e02v8eOHUNbW5vbeHtERUWF/dLjJy9tWEWUrwV1dHRgwYIFxrpOXFwcEhMTMWXKFCPwek9oaCi2b98uhVd7e7uUHi/kWUUs4wmvat6+fdsI58+f9wkfT7XIMUHqdxxvlV5bxxM/wEC9rVQqsnpSiT2mkmVqgEw+uDnicf/MmTMxY8YMozOdPHkyOEyaNAkTJ06UScZSOpYnIDMzEzk5OcjOzjYmW1Zqv/3BpCUJCA8Px+rVq7Fp0ybwMPXTLJYjYN68eSgsLAQtzn2ace/Pm6U64S1btuDcuXP/N+AzC5apAVu3bpUe77PjPJLh/V9enKOlDNAmjjFnWL9+PUd7FCuNgtjRgK51yKS/aNEiwkRO6urqxMaNG0VMTIyT39OnT5dKZMeOHU7vyvgZCB3lNSAiIgIHDx6kvA0tpaWlWL58ubEM7UrbfkrCVZxVnyknYOXKlcayw1AAXbhwAcuWLQPtarlVHY4EKO+E161b5xZQxwg6YuIRfNYdjnMEpQSMHj0avBs2lNy5cwdnzpwZSm1YzoSVEsCTLJmFMdllZj41JyPR0dEyaqboKCUgISFBKpOe2n3HBPj4oozwsrdVRCkBtL8rhYNMieWZ8+LFi6XSmzNnDkaM4PMI6kUpAXTmUwoB2kQ3JlmelGnDvv+wric9juMtSVmyhkrLH/HKJiVZWVlSEydW2rBhg1s/aQlDOh274qVLlwQtb7tNk4A1K840Q04Zos140dvba8fE42dXV5egFVJBTUd/OrQPIPbu3evxPU+Re/bs6U/LRMAH21RHAGeaRjieMHKKq6qqErRJL06fPi3oixlO8d4+sMAZIbUEcNOiUmjkNLhEmn2vloDIyEhx7949v3Fw8eJFo4bIJLh7926zwXZlTy0B3AzRdqN0X+AJ2OLiYkGza8GkXr161a0qnZ4QtGztCgwVz9QTwCTk5uYKBsZX2b9/v6DFuH4A6UyRqK6udkqODngJCzQ7/X5S3q1BAPvB+wJXrlxxAs3Tg7KyMsHfJXOVDzo5MeDcKa0pidmzZ7vUdXyfjg2J1DiI59IgXsmCePXLEHn0mZMKkTDOv3jxdJAd8lqSY4DrDUBvr9evenyBj56sWrXKCPSNGfCCnaPQsBXUvIAPbxUVFeHEiROO0U7XPOs9cOCAsXOWl5dnfDop9T1IiQW+/xXga3TSfWKkOy3gj+8B3/iV+3hvYnwm4P3XbL9Anf8W8Na/vTHpnS4f3OXzPnzssKWlxTgjSjXCu0QktHetAL5HKxkhEmsDnTSBH/WKRKISKj4TkDge+BNtv37uSeDvN4D8PwN/rZKwaFGVl2lVvPVj4E4Tfe+4A2im0Eb3wUTIdPqZ7Re/CLz0jM35FtoTGrvJPxnxmQA2HxEGvLEG+GaGzZmKu8AvSoHf/QPo7PKPg1ZJZewo4MHrNm/OXQO+9HP/eCZR4dwbYpC/VQhsfpP6AmoVZtEq76+fB+7uBn72dSDJ87F/9wkrjmG/f5ADHH8JGN/XF0QTAXZ59yP71eN/PlYNcDSfReeo3ngBmEJNk12YlGI6sv8b+rcFpyuBdqrSVpXYMcCqdFttznzK5iX7n7ETeO828PzTlI8Xbc+X7gdO/sc/OfEbAezOKGqSfvIcQMM2BHHKDtLVA5z9AHibCHmbnL9e7xCp6DKaBljL0wj0LwBcgIIdfL7RCLz8W+AMFRyWQmpqvz0XuEnPU34IdFN+/CF+JcDu0NNUgn65Gkjz8E2gqvs2Mt75EPgnlbC7D+xvB+6TxvCYmwzMozD3swCN9Z0KSncv8HoJ8Fox0NHXj4XSb4LcK6DmiAh79bitn/OXlwEhgJ3jDSeu0j9+FphG4+uhpLaNiLhlq+4f1gFcAm80ADXNQA+B4o3EUHOSNJ5KaozNNo9iuFA86WErmFobFF0CflQEVFQPtEYTMmPEV9NCaW79hJiBWr7dBYwAuzs8jHshkzrqbGAqAeKtcDvc9F+gjgh60GHLvL1kcjMXFgJEhtM/QKDdzbER9I9ZougZlVhZ6aH0j18AfvoX4H0axbkSHm7TLNgY+ZRfd6Xh+7OAE2B3jWsEt7PfnQ9wieJqrVLutwJ/uAjsKwU+qnfvCfcT9wuAvN9TP/COez1fY0wjwNFBHnFwx/fsLOAZao/NIqORatKb/wKOUYn/Gw0IZJZRRoYCcxKBd6mvCoQoIcAxI1HUbHx1JpBNIZ0yOmOS3HKAYxrurhvagTJqMspo3M4Alt/w3+jFnU1vnysnYLDDXOJmxQOfT7B1oHFjgTjqPGOfAMYQWWOorQ+ndp/7ho8f0XIB/VxcIwFdT4E7bW5OOHCH/kHt4NStd285AmQg4o7d25GRTLoqdIYlASqACpRNKktaVCKgCVCJPtnWBGgCFCOg2LyuAZoAxQgoNq9rgCZAMQKKzesaoAlQjIBi87oGaAIUI6DYvK4BmgDFCCg2zzWAtru1KEKgjQmoVGRcmyXsmYBCjYQyBAzseVeshALtsupgIgaMOWNvCJ0d0yQQBmYVQgafMR8gzEYuhXIKdGzJNGfMyrRqO4wpY8sY95f8/wEKvBLprcz3zwAAAABJRU5ErkJggg==</Image>
+<Url type="application/x-suggestions+json" method="GET" template="https://completion.amazon.com/search/complete?q={searchTerms}&amp;search-alias=aps&amp;mkt=1"/>
 <Url type="text/html" method="GET" template="https://www.amazon.com/gp/aw/s">
   <Param name="k" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
   <Param name="tag" value="mozilla-20"/>
 </Url>
 <SearchForm>https://www.amazon.com/</SearchForm>
 </SearchPlugin>
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -1,15 +1,17 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
+import __main__
 import argparse
+import json
 import logging
 import mozpack.path as mozpath
 import os
 import platform
 import subprocess
 import sys
 import which
 from distutils.version import LooseVersion
@@ -19,34 +21,27 @@ from mozbuild.base import (
 )
 
 from mach.decorators import (
     CommandArgument,
     CommandProvider,
     Command,
 )
 
-ESLINT_VERSION = "2.9.0"
+ESLINT_PACKAGES = [
+    "eslint@2.9.0",
+    "eslint-plugin-html@1.4.0",
+    "eslint-plugin-mozilla@0.0.3",
+    "eslint-plugin-react@4.2.3"
+]
 
 ESLINT_NOT_FOUND_MESSAGE = '''
 Could not find eslint!  We looked at the --binary option, at the ESLINT
-environment variable, and then at your path.  Install eslint and needed plugins
-with
-
-mach eslint --setup
-
-and try again.
-'''.strip()
-
-ESLINT_OUTDATED_MESSAGE = '''
-eslint in your path is outdated.
-  path: %(binary)s
-  version: %(version)s
-Expected version: %(min_version)s
-Update eslint with
+environment variable, and then at your local node_modules path. Please Install
+eslint and needed plugins with:
 
 mach eslint --setup
 
 and try again.
 '''.strip()
 
 NODE_NOT_FOUND_MESSAGE = '''
 nodejs v4.2.3 is either not installed or is installed to a non-standard path.
@@ -211,57 +206,63 @@ class MachCommands(MachCommandBase):
     @CommandArgument('-e', '--ext', default='[.js,.jsm,.jsx,.xml,.html]',
         help='Filename extensions to lint, default: "[.js,.jsm,.jsx,.xml,.html]".')
     @CommandArgument('-b', '--binary', default=None,
         help='Path to eslint binary.')
     @CommandArgument('args', nargs=argparse.REMAINDER)  # Passed through to eslint.
     def eslint(self, setup, ext=None, binary=None, args=None):
         '''Run eslint.'''
 
+        module_path = self.get_eslint_module_path()
+
         # eslint requires at least node 4.2.3
         nodePath = self.getNodeOrNpmPath("node", LooseVersion("4.2.3"))
         if not nodePath:
             return 1
 
         if setup:
             return self.eslint_setup()
 
+        npmPath = self.getNodeOrNpmPath("npm")
+        if not npmPath:
+            return 1
+
+        if self.eslintModuleHasIssues():
+            install = self._prompt_yn("\nContinuing will automatically fix "
+                                      "these issues. Would you like to "
+                                      "continue")
+            if install:
+                self.eslint_setup()
+            else:
+                return 1
+
+        # Valid binaries are:
+        #  - Any provided by the binary argument.
+        #  - Any pointed at by the ESLINT environmental variable.
+        #  - Those provided by mach eslint --setup.
+        #
+        #  eslint --setup installs some mozilla specific plugins and installs
+        #  all node modules locally. This is the preferred method of
+        #  installation.
+
         if not binary:
             binary = os.environ.get('ESLINT', None)
+
             if not binary:
-                try:
-                    binary = which.which('eslint')
-                except which.WhichError:
-                    npmPath = self.getNodeOrNpmPath("npm")
-                    if npmPath:
-                        try:
-                            output = subprocess.check_output([npmPath, "bin", "-g"],
-                                                             stderr=subprocess.STDOUT)
-                            if output:
-                                base = output.split("\n")[0].strip()
-                                binary = os.path.join(base, "eslint")
-                                if not os.path.isfile(binary):
-                                    binary = None
-                        except (subprocess.CalledProcessError, OSError):
-                            pass
+                binary = os.path.join(module_path, "node_modules", ".bin", "eslint")
+                if not os.path.isfile(binary):
+                    binary = None
 
         if not binary:
             print(ESLINT_NOT_FOUND_MESSAGE)
             return 1
 
         self.log(logging.INFO, 'eslint', {'binary': binary, 'args': args},
             'Running {binary}')
 
-        version_str = subprocess.check_output([binary, "--version"],
-                                              stderr=subprocess.STDOUT)
-        version = LooseVersion(version_str.lstrip('v'))
-        if version < LooseVersion(ESLINT_VERSION):
-            print (ESLINT_OUTDATED_MESSAGE % {"binary": binary, "version": version_str, "min_version": ESLINT_VERSION})
-            return 1
-
         args = args or ['.']
 
         cmd_args = [binary,
                     # Enable the HTML plugin.
                     # We can't currently enable this in the global config file
                     # because it has bad interactions with the SublimeText
                     # ESLint plugin (bug 1229874).
                     '--plugin', 'html',
@@ -280,67 +281,139 @@ class MachCommands(MachCommandBase):
 
     def eslint_setup(self, update_only=False):
         """Ensure eslint is optimally configured.
 
         This command will inspect your eslint configuration and
         guide you through an interactive wizard helping you configure
         eslint for optimal use on Mozilla projects.
         """
+        orig_cwd = os.getcwd()
         sys.path.append(os.path.dirname(__file__))
 
+        module_path = self.get_eslint_module_path()
+
+        # npm sometimes fails to respect cwd when it is run using check_call so
+        # we manually switch folders here instead.
+        os.chdir(module_path)
+
         npmPath = self.getNodeOrNpmPath("npm")
         if not npmPath:
             return 1
 
-        # Install eslint.
-        # Note that that's the version currently compatible with the mozilla
-        # eslint plugin.
-        success = self.callProcess("eslint",
-                                   [npmPath, "install", "eslint@%s" % ESLINT_VERSION, "-g"])
-        if not success:
-            return 1
+        # Install eslint and necessary plugins.
+        for pkg in ESLINT_PACKAGES:
+            name, version = pkg.split("@")
+            success = False
 
-        # Install eslint-plugin-mozilla.
-        success = self.callProcess("eslint-plugin-mozilla",
-                                   [npmPath, "link"],
-                                   "testing/eslint-plugin-mozilla")
-        if not success:
-            return 1
+            if self.node_package_installed(pkg, cwd=module_path):
+                success = True
+            else:
+                if pkg.startswith("eslint-plugin-mozilla"):
+                    cmd = [npmPath, "install",
+                           os.path.join(module_path, "eslint-plugin-mozilla")]
+                else:
+                    cmd = [npmPath, "install", pkg]
 
-        # Install eslint-plugin-html.
-        success = self.callProcess("eslint-plugin-html",
-                                   [npmPath, "install", "eslint-plugin-html@1.4.0", "-g"])
-        if not success:
-            return 1
+                print("Installing %s v%s using \"%s\"..."
+                      % (name, version, " ".join(cmd)))
+                success = self.callProcess(pkg, cmd)
 
-        # Install eslint-plugin-react.
-        success = self.callProcess("eslint-plugin-react",
-                                   [npmPath, "install", "eslint-plugin-react@4.2.3", "-g"])
-        if not success:
-            return 1
+            if not success:
+                return 1
+
+        eslint_path = os.path.join(module_path, "node_modules", ".bin", "eslint")
 
         print("\nESLint and approved plugins installed successfully!")
+        print("\nNOTE: Your local eslint binary is at %s\n" % eslint_path)
+
+        os.chdir(orig_cwd)
 
     def callProcess(self, name, cmd, cwd=None):
-        print("\nInstalling %s using \"%s\"..." % (name, " ".join(cmd)))
-
         try:
             with open(os.devnull, "w") as fnull:
                 subprocess.check_call(cmd, cwd=cwd, stdout=fnull)
         except subprocess.CalledProcessError:
             if cwd:
                 print("\nError installing %s in the %s folder, aborting." % (name, cwd))
             else:
                 print("\nError installing %s, aborting." % name)
 
             return False
 
         return True
 
+    def eslintModuleHasIssues(self):
+        print("Checking eslint and modules...")
+
+        has_issues = False
+        npmPath = self.getNodeOrNpmPath("npm")
+        module_path = self.get_eslint_module_path()
+
+        for pkg in ESLINT_PACKAGES:
+            name, req_version = pkg.split("@")
+
+            try:
+                with open(os.devnull, "w") as fnull:
+                    global_install = subprocess.check_output([npmPath, "ls", "--json", name, "-g"],
+                                                             stderr=fnull)
+                info = json.loads(global_install)
+                global_version = info["dependencies"][name]["version"]
+            except subprocess.CalledProcessError:
+                global_version = None
+
+            try:
+                with open(os.devnull, "w") as fnull:
+                    local_install = subprocess.check_output([npmPath, "ls", "--json", name],
+                                                            cwd=module_path, stderr=fnull)
+                info = json.loads(local_install)
+                local_version = info["dependencies"][name]["version"]
+            except subprocess.CalledProcessError:
+                local_version = None
+
+            if global_version:
+                if name == "eslint-plugin-mozilla":
+                    print("%s should never be installed globally. This global "
+                          "module will be removed." % name)
+                    has_issues = True
+                else:
+                    print("%s is installed globally. This global module will "
+                          "be ignored. We recommend uninstalling it using "
+                          "sudo %s remove %s -g" % (name, npmPath, name))
+            if local_version:
+                if local_version != req_version:
+                    print("%s v%s is installed locally but is not the "
+                          "required version (v%s). This module will be "
+                          "reinstalled so that the versions match." %
+                          (name, local_version, req_version))
+                    has_issues = True
+            else:
+                print("%s v%s is not installed locally and only local modules "
+                      "are valid. This module will be installed locally."
+                      % (name, req_version))
+                has_issues = True
+
+        return has_issues
+
+    def node_package_installed(self, package_name="", globalInstall=False, cwd=None):
+        try:
+            npmPath = self.getNodeOrNpmPath("npm")
+
+            cmd = [npmPath, "ls", "--parseable", package_name]
+
+            if globalInstall:
+                cmd.append("-g")
+
+            with open(os.devnull, "w") as fnull:
+                subprocess.check_call(cmd, stdout=fnull, stderr=fnull, cwd=cwd)
+
+            return True
+        except subprocess.CalledProcessError:
+            return False
+
     def getPossibleNodePathsWin(self):
         """
         Return possible nodejs paths on Windows.
         """
         if platform.system() != "Windows":
             return []
 
         return list({
@@ -394,8 +467,35 @@ class MachCommands(MachCommandBase):
                                                   stderr=subprocess.STDOUT)
             if minversion:
                 # nodejs prefixes its version strings with "v"
                 version = LooseVersion(version_str.lstrip('v'))
                 return version >= minversion
             return True
         except (subprocess.CalledProcessError, OSError):
             return False
+
+    def get_project_root(self):
+        fullpath = os.path.abspath(sys.modules['__main__'].__file__)
+        return os.path.dirname(fullpath)
+
+    def get_eslint_module_path(self):
+        return os.path.join(self.get_project_root(), "testing", "eslint")
+
+    def _prompt_yn(self, msg):
+        if not sys.stdin.isatty():
+            return False
+
+        print('%s? [Y/n]' % msg)
+
+        while True:
+            choice = raw_input().lower().strip()
+
+            if not choice:
+                return True
+
+            if choice in ('y', 'yes'):
+                return True
+
+            if choice in ('n', 'no'):
+                return False
+
+            print('Must reply with one of {yes, no, y, n}.')
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -160,16 +160,21 @@ TabStore.prototype = {
         let current = entries[index - 1];
 
         // We ignore the tab completely if the current entry url is
         // not acceptable (we need something accurate to open).
         if (!acceptable(current.url)) {
           continue;
         }
 
+        if (current.url.length >= (MAX_UPLOAD_BYTES - 1000)) {
+          this._log.trace("Skipping over-long URL.");
+          continue;
+        }
+
         // The element at `index` is the current page. Previous URLs were
         // previously visited URLs; subsequent URLs are in the 'forward' stack,
         // which we can't represent in Sync, so we truncate here.
         let candidates = (entries.length == index) ?
                          entries :
                          entries.slice(0, index);
 
         let urls = candidates.map((entry) => entry.url)
--- a/services/sync/tests/unit/test_tab_engine.js
+++ b/services/sync/tests/unit/test_tab_engine.js
@@ -1,11 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 function getMocks() {
   let engine = new TabEngine(Service);
@@ -18,33 +19,38 @@ function getMocks() {
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_getOpenURLs() {
   _("Test getOpenURLs.");
   let [engine, store] = getMocks();
 
-  let urls = ["http://bar.com", "http://foo.com", "http://foobar.com"];
-  function threeURLs() {
+  let superLongURL = "http://" + (new Array(MAX_UPLOAD_BYTES).join("w")) + ".com/";
+  let urls = ["http://bar.com", "http://foo.com", "http://foobar.com", superLongURL];
+  function fourURLs() {
     return urls.pop();
   }
-  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, threeURLs, 1, 3);
+  store.getWindowEnumerator = mockGetWindowEnumerator.bind(this, fourURLs, 1, 4);
 
   let matches;
 
   _("  test matching works (true)");
   let openurlsset = engine.getOpenURLs();
   matches = openurlsset.has("http://foo.com");
   ok(matches);
 
   _("  test matching works (false)");
   matches = openurlsset.has("http://barfoo.com");
   ok(!matches);
 
+  _("  test matching works (too long)");
+  matches = openurlsset.has(superLongURL);
+  ok(!matches);
+
   run_next_test();
 });
 
 add_test(function test_tab_engine_skips_incoming_local_record() {
   _("Ensure incoming records that match local client ID are never applied.");
   let [engine, store] = getMocks();
   let localID = engine.service.clientsEngine.localID;
   let apply = store.applyIncoming;
--- a/testing/docker/lint/Dockerfile
+++ b/testing/docker/lint/Dockerfile
@@ -1,19 +1,21 @@
 FROM          node:4.2
 MAINTAINER    Dave Townsend <dtownsend@oxymoronical.com>
 
 RUN useradd -d /home/worker -s /bin/bash -m worker
 WORKDIR /home/worker
 
 # install necessary npm packages
 RUN           npm install -g taskcluster-vcs@2.3.12
-RUN           npm install -g eslint@2.9.0
-RUN           npm install -g eslint-plugin-html@1.4.0
-RUN           npm install -g eslint-plugin-react@4.2.3
+
+# Install tooltool directly from github.
+RUN mkdir /build
+ADD https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py /build/tooltool.py
+RUN chmod +rx /build/tooltool.py
 
 # Set variable normally configured at login, by the shells parent process, these
 # are taken from GNU su manual
 ENV           HOME          /home/worker
 ENV           SHELL         /bin/bash
 ENV           USER          worker
 ENV           LOGNAME       worker
 ENV           HOSTNAME      taskcluster-worker
rename from testing/eslint-plugin-mozilla/LICENSE
rename to testing/eslint/eslint-plugin-mozilla/LICENSE
rename from testing/eslint-plugin-mozilla/docs/balanced-listeners.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/balanced-listeners.rst
rename from testing/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/import-browserjs-globals.rst
rename from testing/eslint-plugin-mozilla/docs/import-globals.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/import-globals.rst
rename from testing/eslint-plugin-mozilla/docs/import-headjs-globals.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/import-headjs-globals.rst
rename from testing/eslint-plugin-mozilla/docs/index.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/index.rst
rename from testing/eslint-plugin-mozilla/docs/mark-test-function-used.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/mark-test-function-used.rst
rename from testing/eslint-plugin-mozilla/docs/no-aArgs.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/no-aArgs.rst
rename from testing/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/no-cpows-in-tests.rst
rename from testing/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/reject-importGlobalProperties.rst
rename from testing/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
rename to testing/eslint/eslint-plugin-mozilla/docs/var-only-at-top-level.rst
rename from testing/eslint-plugin-mozilla/lib/globals.js
rename to testing/eslint/eslint-plugin-mozilla/lib/globals.js
rename from testing/eslint-plugin-mozilla/lib/helpers.js
rename to testing/eslint/eslint-plugin-mozilla/lib/helpers.js
rename from testing/eslint-plugin-mozilla/lib/index.js
rename to testing/eslint/eslint-plugin-mozilla/lib/index.js
rename from testing/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
rename to testing/eslint/eslint-plugin-mozilla/lib/processors/xbl-bindings.js
rename from testing/eslint-plugin-mozilla/lib/rules/.eslintrc
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/.eslintrc
rename from testing/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/balanced-listeners.js
rename from testing/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/import-browserjs-globals.js
rename from testing/eslint-plugin-mozilla/lib/rules/import-globals.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/import-globals.js
rename from testing/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/import-headjs-globals.js
rename from testing/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/mark-test-function-used.js
rename from testing/eslint-plugin-mozilla/lib/rules/no-aArgs.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/no-aArgs.js
rename from testing/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
rename from testing/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/reject-importGlobalProperties.js
rename from testing/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
rename to testing/eslint/eslint-plugin-mozilla/lib/rules/var-only-at-top-level.js
rename from testing/eslint-plugin-mozilla/moz.build
rename to testing/eslint/eslint-plugin-mozilla/moz.build
rename from testing/eslint-plugin-mozilla/package.json
rename to testing/eslint/eslint-plugin-mozilla/package.json
new file mode 100644
--- /dev/null
+++ b/testing/eslint/manifest.tt
@@ -0,0 +1,9 @@
+[
+{
+"size": 2218185,
+"visibility": "public",
+"digest": "ea0065d8986ceffb2870fbc8489c1c2e06afc859df4d93ce8ea87c5accb10822a30e16dbde72c19c9a03fa434f136969089467e629a18c3e6d8601645436fff9",
+"algorithm": "sha512",
+"filename": "eslint.tar.gz"
+}
+]
new file mode 100644
--- /dev/null
+++ b/testing/eslint/npm-shrinkwrap.json
@@ -0,0 +1,704 @@
+{
+  "dependencies": {
+    "acorn": {
+      "version": "3.1.0",
+      "from": "acorn@>=3.1.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.1.0.tgz"
+    },
+    "acorn-jsx": {
+      "version": "3.0.1",
+      "from": "acorn-jsx@>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz"
+    },
+    "ansi-escapes": {
+      "version": "1.4.0",
+      "from": "ansi-escapes@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz"
+    },
+    "ansi-regex": {
+      "version": "2.0.0",
+      "from": "ansi-regex@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz"
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "from": "ansi-styles@>=2.2.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz"
+    },
+    "argparse": {
+      "version": "1.0.7",
+      "from": "argparse@>=1.0.7 <2.0.0",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz"
+    },
+    "array-union": {
+      "version": "1.0.1",
+      "from": "array-union@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.1.tgz"
+    },
+    "array-uniq": {
+      "version": "1.0.2",
+      "from": "array-uniq@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.2.tgz"
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "from": "arrify@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz"
+    },
+    "balanced-match": {
+      "version": "0.4.1",
+      "from": "balanced-match@>=0.4.1 <0.5.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz"
+    },
+    "bluebird": {
+      "version": "3.3.5",
+      "from": "bluebird@>=3.1.1 <4.0.0",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.3.5.tgz"
+    },
+    "brace-expansion": {
+      "version": "1.1.4",
+      "from": "brace-expansion@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz"
+    },
+    "caller-path": {
+      "version": "0.1.0",
+      "from": "caller-path@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz"
+    },
+    "callsites": {
+      "version": "0.2.0",
+      "from": "callsites@>=0.2.0 <0.3.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz"
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "from": "chalk@>=1.1.3 <2.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz"
+    },
+    "cli-cursor": {
+      "version": "1.0.2",
+      "from": "cli-cursor@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz"
+    },
+    "cli-width": {
+      "version": "2.1.0",
+      "from": "cli-width@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz"
+    },
+    "code-point-at": {
+      "version": "1.0.0",
+      "from": "code-point-at@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.0.0.tgz"
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "from": "concat-map@0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+    },
+    "concat-stream": {
+      "version": "1.5.1",
+      "from": "concat-stream@>=1.4.6 <2.0.0",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.1.tgz"
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "from": "core-util-is@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz"
+    },
+    "d": {
+      "version": "0.1.1",
+      "from": "d@>=0.1.1 <0.2.0",
+      "resolved": "https://registry.npmjs.org/d/-/d-0.1.1.tgz"
+    },
+    "debug": {
+      "version": "2.2.0",
+      "from": "debug@>=2.1.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "from": "deep-is@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz"
+    },
+    "del": {
+      "version": "2.2.0",
+      "from": "del@>=2.0.2 <3.0.0",
+      "resolved": "https://registry.npmjs.org/del/-/del-2.2.0.tgz"
+    },
+    "doctrine": {
+      "version": "1.2.1",
+      "from": "doctrine@>=1.2.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.2.1.tgz",
+      "dependencies": {
+        "esutils": {
+          "version": "1.1.6",
+          "from": "esutils@>=1.1.6 <2.0.0",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz"
+        }
+      }
+    },
+    "dom-serializer": {
+      "version": "0.1.0",
+      "from": "dom-serializer@>=0.0.0 <1.0.0",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz",
+      "dependencies": {
+        "domelementtype": {
+          "version": "1.1.3",
+          "from": "domelementtype@>=1.1.1 <1.2.0",
+          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz"
+        }
+      }
+    },
+    "domelementtype": {
+      "version": "1.3.0",
+      "from": "domelementtype@>=1.3.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz"
+    },
+    "domhandler": {
+      "version": "2.3.0",
+      "from": "domhandler@>=2.3.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz"
+    },
+    "domutils": {
+      "version": "1.5.1",
+      "from": "domutils@>=1.5.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz"
+    },
+    "entities": {
+      "version": "1.1.1",
+      "from": "entities@>=1.1.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz"
+    },
+    "es5-ext": {
+      "version": "0.10.11",
+      "from": "es5-ext@>=0.10.8 <0.11.0",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.11.tgz"
+    },
+    "es6-iterator": {
+      "version": "2.0.0",
+      "from": "es6-iterator@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.0.tgz"
+    },
+    "es6-map": {
+      "version": "0.1.3",
+      "from": "es6-map@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.3.tgz"
+    },
+    "es6-set": {
+      "version": "0.1.4",
+      "from": "es6-set@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.4.tgz"
+    },
+    "es6-symbol": {
+      "version": "3.0.2",
+      "from": "es6-symbol@>=3.0.1 <3.1.0",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.0.2.tgz"
+    },
+    "es6-weak-map": {
+      "version": "2.0.1",
+      "from": "es6-weak-map@>=2.0.1 <3.0.0",
+      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.1.tgz"
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "from": "escape-string-regexp@>=1.0.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz"
+    },
+    "escope": {
+      "version": "3.6.0",
+      "from": "escope@>=3.6.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz"
+    },
+    "eslint": {
+      "version": "2.9.0",
+      "from": "eslint@2.9.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-2.9.0.tgz"
+    },
+    "eslint-plugin-html": {
+      "version": "1.4.0",
+      "from": "eslint-plugin-html@1.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-html/-/eslint-plugin-html-1.4.0.tgz"
+    },
+    "eslint-plugin-mozilla": {
+      "version": "0.0.3",
+      "from": "eslint-plugin-mozilla",
+      "resolved": "file:eslint-plugin-mozilla",
+      "dependencies": {
+        "espree": {
+          "version": "2.2.5",
+          "from": "espree@>=2.2.4 <3.0.0",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-2.2.5.tgz"
+        }
+      }
+    },
+    "eslint-plugin-react": {
+      "version": "4.2.3",
+      "from": "eslint-plugin-react@4.2.3",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-4.2.3.tgz"
+    },
+    "espree": {
+      "version": "3.1.4",
+      "from": "espree@3.1.4",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-3.1.4.tgz"
+    },
+    "esprima": {
+      "version": "2.7.2",
+      "from": "esprima@>=2.6.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz"
+    },
+    "esrecurse": {
+      "version": "4.1.0",
+      "from": "esrecurse@>=4.1.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.1.0.tgz",
+      "dependencies": {
+        "estraverse": {
+          "version": "4.1.1",
+          "from": "estraverse@>=4.1.0 <4.2.0",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.1.1.tgz"
+        }
+      }
+    },
+    "estraverse": {
+      "version": "4.2.0",
+      "from": "estraverse@>=4.2.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz"
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "from": "esutils@>=2.0.2 <3.0.0",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz"
+    },
+    "event-emitter": {
+      "version": "0.3.4",
+      "from": "event-emitter@>=0.3.4 <0.4.0",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.4.tgz"
+    },
+    "exit-hook": {
+      "version": "1.1.1",
+      "from": "exit-hook@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz"
+    },
+    "fast-levenshtein": {
+      "version": "1.1.3",
+      "from": "fast-levenshtein@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.3.tgz"
+    },
+    "figures": {
+      "version": "1.5.0",
+      "from": "figures@>=1.3.5 <2.0.0",
+      "resolved": "https://registry.npmjs.org/figures/-/figures-1.5.0.tgz"
+    },
+    "file-entry-cache": {
+      "version": "1.2.4",
+      "from": "file-entry-cache@>=1.1.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-1.2.4.tgz"
+    },
+    "flat-cache": {
+      "version": "1.0.10",
+      "from": "flat-cache@>=1.0.9 <2.0.0",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.0.10.tgz"
+    },
+    "generate-function": {
+      "version": "2.0.0",
+      "from": "generate-function@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz"
+    },
+    "generate-object-property": {
+      "version": "1.2.0",
+      "from": "generate-object-property@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz"
+    },
+    "glob": {
+      "version": "7.0.3",
+      "from": "glob@>=7.0.3 <8.0.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.3.tgz"
+    },
+    "globals": {
+      "version": "9.6.0",
+      "from": "globals@>=9.2.0 <10.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.6.0.tgz"
+    },
+    "globby": {
+      "version": "4.0.0",
+      "from": "globby@>=4.0.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-4.0.0.tgz",
+      "dependencies": {
+        "glob": {
+          "version": "6.0.4",
+          "from": "glob@>=6.0.1 <7.0.0",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz"
+        }
+      }
+    },
+    "graceful-fs": {
+      "version": "4.1.4",
+      "from": "graceful-fs@>=4.1.2 <5.0.0",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz"
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "from": "has-ansi@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz"
+    },
+    "htmlparser2": {
+      "version": "3.9.0",
+      "from": "htmlparser2@>=3.8.2 <4.0.0",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.0.tgz"
+    },
+    "ignore": {
+      "version": "3.1.2",
+      "from": "ignore@>=3.1.2 <4.0.0",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.1.2.tgz"
+    },
+    "imurmurhash": {
+      "version": "0.1.4",
+      "from": "imurmurhash@>=0.1.4 <0.2.0",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+    },
+    "inflight": {
+      "version": "1.0.4",
+      "from": "inflight@>=1.0.4 <2.0.0",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.4.tgz"
+    },
+    "inherits": {
+      "version": "2.0.1",
+      "from": "inherits@>=2.0.1 <2.1.0",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+    },
+    "inquirer": {
+      "version": "0.12.0",
+      "from": "inquirer@>=0.12.0 <0.13.0",
+      "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz"
+    },
+    "is-fullwidth-code-point": {
+      "version": "1.0.0",
+      "from": "is-fullwidth-code-point@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz"
+    },
+    "is-my-json-valid": {
+      "version": "2.13.1",
+      "from": "is-my-json-valid@>=2.10.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz"
+    },
+    "is-path-cwd": {
+      "version": "1.0.0",
+      "from": "is-path-cwd@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz"
+    },
+    "is-path-in-cwd": {
+      "version": "1.0.0",
+      "from": "is-path-in-cwd@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz"
+    },
+    "is-path-inside": {
+      "version": "1.0.0",
+      "from": "is-path-inside@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz"
+    },
+    "is-property": {
+      "version": "1.0.2",
+      "from": "is-property@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
+    },
+    "is-resolvable": {
+      "version": "1.0.0",
+      "from": "is-resolvable@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz"
+    },
+    "isarray": {
+      "version": "1.0.0",
+      "from": "isarray@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+    },
+    "js-yaml": {
+      "version": "3.6.0",
+      "from": "js-yaml@>=3.5.1 <4.0.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.0.tgz"
+    },
+    "json-stable-stringify": {
+      "version": "1.0.1",
+      "from": "json-stable-stringify@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz"
+    },
+    "jsonify": {
+      "version": "0.0.0",
+      "from": "jsonify@>=0.0.0 <0.1.0",
+      "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz"
+    },
+    "jsonpointer": {
+      "version": "2.0.0",
+      "from": "jsonpointer@2.0.0",
+      "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz"
+    },
+    "levn": {
+      "version": "0.3.0",
+      "from": "levn@>=0.3.0 <0.4.0",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz"
+    },
+    "lodash": {
+      "version": "4.12.0",
+      "from": "lodash@>=4.0.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.12.0.tgz"
+    },
+    "minimatch": {
+      "version": "3.0.0",
+      "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.0.tgz"
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "from": "minimist@0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "from": "mkdirp@>=0.5.0 <0.6.0",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
+    },
+    "ms": {
+      "version": "0.7.1",
+      "from": "ms@0.7.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+    },
+    "mute-stream": {
+      "version": "0.0.5",
+      "from": "mute-stream@0.0.5",
+      "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz"
+    },
+    "number-is-nan": {
+      "version": "1.0.0",
+      "from": "number-is-nan@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.0.tgz"
+    },
+    "object-assign": {
+      "version": "4.1.0",
+      "from": "object-assign@>=4.0.1 <5.0.0",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz"
+    },
+    "once": {
+      "version": "1.3.3",
+      "from": "once@>=1.3.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz"
+    },
+    "onetime": {
+      "version": "1.1.0",
+      "from": "onetime@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz"
+    },
+    "optionator": {
+      "version": "0.8.1",
+      "from": "optionator@>=0.8.1 <0.9.0",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz"
+    },
+    "os-homedir": {
+      "version": "1.0.1",
+      "from": "os-homedir@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.1.tgz"
+    },
+    "path-is-absolute": {
+      "version": "1.0.0",
+      "from": "path-is-absolute@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz"
+    },
+    "path-is-inside": {
+      "version": "1.0.1",
+      "from": "path-is-inside@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.1.tgz"
+    },
+    "pify": {
+      "version": "2.3.0",
+      "from": "pify@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz"
+    },
+    "pinkie": {
+      "version": "2.0.4",
+      "from": "pinkie@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz"
+    },
+    "pinkie-promise": {
+      "version": "2.0.1",
+      "from": "pinkie-promise@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz"
+    },
+    "pluralize": {
+      "version": "1.2.1",
+      "from": "pluralize@>=1.2.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz"
+    },
+    "prelude-ls": {
+      "version": "1.1.2",
+      "from": "prelude-ls@>=1.1.2 <1.2.0",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz"
+    },
+    "process-nextick-args": {
+      "version": "1.0.7",
+      "from": "process-nextick-args@>=1.0.6 <1.1.0",
+      "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz"
+    },
+    "progress": {
+      "version": "1.1.8",
+      "from": "progress@>=1.1.8 <2.0.0",
+      "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz"
+    },
+    "read-json-sync": {
+      "version": "1.1.1",
+      "from": "read-json-sync@>=1.1.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/read-json-sync/-/read-json-sync-1.1.1.tgz"
+    },
+    "readable-stream": {
+      "version": "2.0.6",
+      "from": "readable-stream@>=2.0.0 <2.1.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz"
+    },
+    "readline2": {
+      "version": "1.0.1",
+      "from": "readline2@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz"
+    },
+    "require-uncached": {
+      "version": "1.0.2",
+      "from": "require-uncached@>=1.0.2 <2.0.0",
+      "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.2.tgz"
+    },
+    "resolve-from": {
+      "version": "1.0.1",
+      "from": "resolve-from@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz"
+    },
+    "restore-cursor": {
+      "version": "1.0.1",
+      "from": "restore-cursor@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz"
+    },
+    "rimraf": {
+      "version": "2.5.2",
+      "from": "rimraf@>=2.2.8 <3.0.0",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.2.tgz"
+    },
+    "run-async": {
+      "version": "0.1.0",
+      "from": "run-async@>=0.1.0 <0.2.0",
+      "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz"
+    },
+    "rx-lite": {
+      "version": "3.1.2",
+      "from": "rx-lite@>=3.1.2 <4.0.0",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz"
+    },
+    "sax": {
+      "version": "1.2.1",
+      "from": "sax@>=1.1.4 <2.0.0",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz"
+    },
+    "shelljs": {
+      "version": "0.6.0",
+      "from": "shelljs@>=0.6.0 <0.7.0",
+      "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.6.0.tgz"
+    },
+    "slice-ansi": {
+      "version": "0.0.4",
+      "from": "slice-ansi@0.0.4",
+      "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz"
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "from": "sprintf-js@>=1.0.2 <1.1.0",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+    },
+    "string_decoder": {
+      "version": "0.10.31",
+      "from": "string_decoder@>=0.10.0 <0.11.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
+    },
+    "string-width": {
+      "version": "1.0.1",
+      "from": "string-width@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.1.tgz"
+    },
+    "strip-ansi": {
+      "version": "3.0.1",
+      "from": "strip-ansi@>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz"
+    },
+    "strip-json-comments": {
+      "version": "1.0.4",
+      "from": "strip-json-comments@>=1.0.1 <1.1.0",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz"
+    },
+    "supports-color": {
+      "version": "2.0.0",
+      "from": "supports-color@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"
+    },
+    "table": {
+      "version": "3.7.8",
+      "from": "table@>=3.7.8 <4.0.0",
+      "resolved": "https://registry.npmjs.org/table/-/table-3.7.8.tgz"
+    },
+    "text-table": {
+      "version": "0.2.0",
+      "from": "text-table@>=0.2.0 <0.3.0",
+      "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
+    },
+    "through": {
+      "version": "2.3.8",
+      "from": "through@>=2.3.6 <3.0.0",
+      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz"
+    },
+    "tryit": {
+      "version": "1.0.2",
+      "from": "tryit@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.2.tgz"
+    },
+    "tv4": {
+      "version": "1.2.7",
+      "from": "tv4@>=1.2.7 <2.0.0",
+      "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.7.tgz"
+    },
+    "type-check": {
+      "version": "0.3.2",
+      "from": "type-check@>=0.3.2 <0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz"
+    },
+    "typedarray": {
+      "version": "0.0.6",
+      "from": "typedarray@>=0.0.5 <0.1.0",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
+    },
+    "user-home": {
+      "version": "2.0.0",
+      "from": "user-home@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz"
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "from": "util-deprecate@>=1.0.1 <1.1.0",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+    },
+    "wordwrap": {
+      "version": "1.0.0",
+      "from": "wordwrap@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz"
+    },
+    "wrappy": {
+      "version": "1.0.1",
+      "from": "wrappy@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.1.tgz"
+    },
+    "write": {
+      "version": "0.2.1",
+      "from": "write@>=0.2.1 <0.3.0",
+      "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz"
+    },
+    "xregexp": {
+      "version": "3.1.0",
+      "from": "xregexp@>=3.0.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.1.0.tgz"
+    },
+    "xtend": {
+      "version": "4.0.1",
+      "from": "xtend@>=4.0.0 <5.0.0",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/testing/eslint/package.json
@@ -0,0 +1,12 @@
+{
+  "name": "",
+  "description": "None",
+  "repository": {},
+  "license": "MPL-2.0",
+  "dependencies": {
+    "eslint": "*",
+    "eslint-plugin-html": "*",
+    "eslint-plugin-mozilla": "*",
+    "eslint-plugin-react": "*"
+  }
+}
new file mode 100755
--- /dev/null
+++ b/testing/eslint/update
@@ -0,0 +1,65 @@
+#!/bin/sh
+# Force the scripts working directory to be projdir/testing/eslint.
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $DIR
+
+echo "To complete this script you will need the following tokens from https://api.pub.build.mozilla.org/tokenauth/"
+echo " - tooltool.upload.public"
+echo " - tooltool.download.public"
+echo ""
+read -p "Are these tokens visible at the above URL (y/n)?" choice
+case "$choice" in
+  y|Y )
+    echo ""
+    echo "1. Go to https://api.pub.build.mozilla.org/"
+    echo "2. Log In using your Mozilla LDAP account."
+    echo "3. Click on \"Tokens.\""
+    echo "4. Issue a user token with the permissions tooltool.upload.public and tooltool.download.public."
+    echo ""
+    echo "When you click issue you will be presented with a long string. Paste the string into a temporary file called ~/.tooltool-token."
+    echo ""
+    read -rsp $'Press any key to continue...\n' -n 1
+    ;;
+  n|N )
+    echo ""
+    echo "You will need to contact somebody that has these permissions... people most likely to have these permissions are members of the releng, ateam, a sheriff or mratcliffe@mozilla.com."
+    exit 1
+    ;;
+  * )
+    echo ""
+  echo "Invalid input."
+  continue
+    ;;
+esac
+
+echo ""
+echo "Removing node_modules and npm_shrinkwrap.json..."
+rm -rf node_modules/
+rm npm-shrinkwrap.json
+
+echo "Installing eslint and dependencies..."
+../../mach eslint --setup
+
+echo "Creating npm shrinkwrap..."
+npm shrinkwrap
+
+echo "Creating eslint.tar.gz..."
+tar cvfz eslint.tar.gz node_modules
+
+echo "Downloading tooltool..."
+wget https://raw.githubusercontent.com/mozilla/build-tooltool/master/tooltool.py
+chmod +x tooltool.py
+
+echo "Adding eslint.tar.gz to tooltool..."
+rm manifest.tt
+./tooltool.py add --visibility public eslint.tar.gz
+
+echo "Uploading eslint.tar.gz to tooltool..."
+./tooltool.py upload --authentication-file=~/.tooltool-token --message "node_modules folder update for testing/eslint"
+
+echo "Cleaning up..."
+rm eslint.tar.gz
+rm tooltool.py
+
+echo ""
+echo "Update complete, please commit and check in your changes."
--- a/testing/taskcluster/tasks/branches/base_jobs.yml
+++ b/testing/taskcluster/tasks/branches/base_jobs.yml
@@ -388,17 +388,17 @@ tasks:
         - '**/*.jsm'
         - '**/*.jsx'
         - '**/*.html'
         - '**/*.xml'
         # Run when eslint policies change.
         - '**/.eslintignore'
         - '**/*eslintrc*'
         # The plugin implementing custom checks.
-        - 'testing/eslint-plugin-mozilla/**'
+        - 'testing/eslint/eslint-plugin-mozilla/**'
         # Other misc lint related files.
         - 'tools/lint/**'
   android-api-15-gradle-dependencies:
     task: tasks/builds/android_api_15_gradle_dependencies.yml
     root: true
     when:
       file_patterns:
         - 'mobile/android/config/**'
--- a/testing/taskcluster/tasks/tests/eslint-gecko.yml
+++ b/testing/taskcluster/tasks/tests/eslint-gecko.yml
@@ -17,19 +17,23 @@ task:
       path: 'public/image.tar'
       taskId: '{{#task_id_for_image}}lint{{/task_id_for_image}}'
 
     command:
       - bash
       - -cx
       - >
           tc-vcs checkout ./gecko {{base_repository}} {{head_repository}} {{head_rev}} {{head_ref}} &&
-          cd gecko &&
-          npm link testing/eslint-plugin-mozilla &&
-          eslint --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
+          cd gecko/testing/eslint &&
+          /build/tooltool.py fetch -m manifest.tt &&
+          tar xvfz eslint.tar.gz &&
+          rm eslint.tar.gz &&
+          cd ../.. &&
+          testing/eslint/node_modules/.bin/eslint --plugin html --ext [.js,.jsm,.jsx,.xml,.html] -f tools/lint/eslint-formatter .
+
   extra:
     locations:
         build: null
         tests: null
     treeherder:
         machine:
             platform: lint
         groupSymbol: tc
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -49,29 +49,32 @@ Notification.prototype = {
       // This will fail if the OS doesn't support this function.
     }
     notificationsMap.get(this.extension).delete(this.id);
   },
 
   observe(subject, topic, data) {
     let notifications = notificationsMap.get(this.extension);
 
+    function emitAndDelete(event) {
+      notifications.emit(event, data);
+      notifications.delete(this.id);
+    }
+
     // Don't try to emit events if the extension has been unloaded
     if (!notifications) {
       return;
     }
 
     if (topic === "alertclickcallback") {
-      notifications.emit("clicked", data);
+      emitAndDelete("clicked");
     }
     if (topic === "alertfinished") {
-      notifications.emit("closed", data);
+      emitAndDelete("closed");
     }
-
-    notifications.delete(this.id);
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("startup", (type, extension) => {
   let map = new Map();
   EventEmitter.decorate(map);
   notificationsMap.set(extension, map);
--- a/toolkit/components/extensions/ext-webNavigation.js
+++ b/toolkit/components/extensions/ext-webNavigation.js
@@ -27,23 +27,34 @@ const frameTransitions = {
   anyFrame: {
     qualifiers: ["server_redirect", "client_redirect", "forward_back"],
   },
   topFrame: {
     types: ["reload", "form_submit"],
   },
 };
 
+const tabTransitions = {
+  topFrame: {
+    qualifiers: ["from_address_bar"],
+    types: ["auto_bookmark", "typed", "keyword", "generated", "link"],
+  },
+  subFrame: {
+    types: ["manual_subframe"],
+  },
+};
+
 function isTopLevelFrame({frameId, parentFrameId}) {
   return frameId == 0 && parentFrameId == -1;
 }
 
 function fillTransitionProperties(eventName, src, dst) {
   if (eventName == "onCommitted" || eventName == "onHistoryStateUpdated") {
     let frameTransitionData = src.frameTransitionData || {};
+    let tabTransitionData = src.tabTransitionData || {};
 
     let transitionType, transitionQualifiers = [];
 
     // Fill transition properties for any frame.
     for (let qualifier of frameTransitions.anyFrame.qualifiers) {
       if (frameTransitionData[qualifier]) {
         transitionQualifiers.push(qualifier);
       }
@@ -51,24 +62,37 @@ function fillTransitionProperties(eventN
 
     if (isTopLevelFrame(dst)) {
       for (let type of frameTransitions.topFrame.types) {
         if (frameTransitionData[type]) {
           transitionType = type;
         }
       }
 
+      for (let qualifier of tabTransitions.topFrame.qualifiers) {
+        if (tabTransitionData[qualifier]) {
+          transitionQualifiers.push(qualifier);
+        }
+      }
+
+      for (let type of tabTransitions.topFrame.types) {
+        if (tabTransitionData[type]) {
+          transitionType = type;
+        }
+      }
+
       // If transitionType is not defined, defaults it to "link".
       if (!transitionType) {
         transitionType = defaultTransitionTypes.topFrame;
       }
     } else {
       // If it is sub-frame, transitionType defaults it to "auto_subframe",
       // "manual_subframe" is set only in case of a recent user interaction.
-      transitionType = defaultTransitionTypes.subFrame;
+      transitionType = tabTransitionData.link ?
+        "manual_subframe" : defaultTransitionTypes.subFrame;
     }
 
     // Fill the transition properties in the webNavigation event object.
     dst.transitionType = transitionType;
     dst.transitionQualifiers = transitionQualifiers;
   }
 }
 
--- a/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
@@ -249,17 +249,23 @@ function backgroundScript() {
       return browser.bookmarks.search({url: "spider-man vs. batman"});
     }).then(expectedError, error => {
       browser.test.assertTrue(
         error.message.includes("spider-man vs. batman is not a valid URL"),
         "Expected error thrown when trying to search with an invalid url as an argument"
       );
     });
 
-    // queries the url
+    // queries the full url
+    return browser.bookmarks.search("http://example.org/");
+  }).then(results => {
+    browser.test.assertEq(1, results.length, "Expected number of results returned for url search");
+    checkBookmark({title: "Example", url: "http://example.org/", index: 2}, results[0]);
+
+    // queries a partial url
     return browser.bookmarks.search("example.org");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for url search");
     checkBookmark({title: "Example", url: "http://example.org/", index: 2}, results[0]);
 
     // queries the title
     return browser.bookmarks.search("EFF");
   }).then(results => {
--- a/toolkit/components/places/Bookmarks.jsm
+++ b/toolkit/components/places/Bookmarks.jsm
@@ -77,17 +77,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Sqlite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 
 // Imposed to limit database size.
 const DB_URL_LENGTH_MAX = 65536;
 const DB_TITLE_LENGTH_MAX = 4096;
 
-const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
+const MATCH_ANYWHERE_UNMODIFIED = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE_UNMODIFIED;
 const BEHAVIOR_BOOKMARK = Ci.mozIPlacesAutoComplete.BEHAVIOR_BOOKMARK;
 
 var Bookmarks = Object.freeze({
   /**
    * Item's type constants.
    * These should stay consistent with nsINavBookmarksService.idl
    */
   TYPE_BOOKMARK: 1,
@@ -923,17 +923,17 @@ function queryBookmarks(info) {
   if (info.url) {
     queryString += " AND h.url = :url";
     queryParams.url = info.url;
   }
 
   if (info.query) {
     queryString += " AND AUTOCOMPLETE_MATCH(:query, h.url, b.title, NULL, NULL, 1, 1, NULL, :matchBehavior, :searchBehavior) ";
     queryParams.query = info.query;
-    queryParams.matchBehavior = MATCH_BOUNDARY;
+    queryParams.matchBehavior = MATCH_ANYWHERE_UNMODIFIED;
     queryParams.searchBehavior = BEHAVIOR_BOOKMARK;
   }
 
   return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: queryBookmarks",
     Task.async(function*(db) {
 
     // _id, _childCount, _grandParentId and _parentId fields
     // are required to be in the result by the converting function
--- a/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js
@@ -19,20 +19,20 @@ add_task(function* search_bookmark() {
   let bm1 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  url: "http://example.com/",
                                                  title: "a bookmark" });
   let bm2 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
                                                  url: "http://example.org/",
                                                  title: "another bookmark" });
   let bm3 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.menuGuid,
                                                  url: "http://menu.org/",
-                                                 title: "a menu bookmark" });
+                                                 title: "an on-menu bookmark" });
   let bm4 = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
                                                  url: "http://toolbar.org/",
-                                                 title: "a toolbar bookmark" });
+                                                 title: "an on-toolbar bookmark" });
   checkBookmarkObject(bm1);
   checkBookmarkObject(bm2);
   checkBookmarkObject(bm3);
   checkBookmarkObject(bm4);
 
   // finds a result by query
   let results = yield PlacesUtils.bookmarks.search("example.com");
   Assert.equal(results.length, 1);
@@ -41,23 +41,23 @@ add_task(function* search_bookmark() {
 
   // finds multiple results
   results = yield PlacesUtils.bookmarks.search("example");
   Assert.equal(results.length, 2);
   checkBookmarkObject(results[0]);
   checkBookmarkObject(results[1]);
 
   // finds menu bookmarks
-  results = yield PlacesUtils.bookmarks.search("a menu bookmark");
+  results = yield PlacesUtils.bookmarks.search("an on-menu bookmark");
   Assert.equal(results.length, 1);
   checkBookmarkObject(results[0]);
   Assert.deepEqual(bm3, results[0]);
 
   // finds toolbar bookmarks
-  results = yield PlacesUtils.bookmarks.search("a toolbar bookmark");
+  results = yield PlacesUtils.bookmarks.search("an on-toolbar bookmark");
   Assert.equal(results.length, 1);
   checkBookmarkObject(results[0]);
   Assert.deepEqual(bm4, results[0]);
 
   yield PlacesUtils.bookmarks.eraseEverything();
 });
 
 add_task(function* search_bookmark_by_query_object() {
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -130,39 +130,49 @@ var Policy = {
 this.EXPORTED_SYMBOLS = ["TelemetryController"];
 
 this.TelemetryController = Object.freeze({
   Constants: Object.freeze({
     PREF_LOG_LEVEL: PREF_LOG_LEVEL,
     PREF_LOG_DUMP: PREF_LOG_DUMP,
     PREF_SERVER: PREF_SERVER,
   }),
+
   /**
    * Used only for testing purposes.
    */
-  initLogging: function() {
+  testInitLogging: function() {
     configureLogging();
   },
+
   /**
    * Used only for testing purposes.
    */
-  reset: function() {
+  testReset: function() {
     return Impl.reset();
   },
+
   /**
    * Used only for testing purposes.
    */
-  setup: function() {
+  testSetup: function() {
     return Impl.setupTelemetry(true);
   },
 
   /**
    * Used only for testing purposes.
    */
-  setupContent: function() {
+  testShutdown: function() {
+    return Impl.shutdown();
+  },
+
+  /**
+   * Used only for testing purposes.
+   */
+  testSetupContent: function() {
     return Impl.setupContentTelemetry(true);
   },
 
   /**
    * Send a notification.
    */
   observe: function (aSubject, aTopic, aData) {
     return Impl.observe(aSubject, aTopic, aData);
@@ -294,23 +304,16 @@ this.TelemetryController = Object.freeze
    *
    * @return The client id as string, or null.
    */
   get clientID() {
     return Impl.clientID;
   },
 
   /**
-   * The AsyncShutdown.Barrier to synchronize with TelemetryController shutdown.
-   */
-  get shutdown() {
-    return Impl._shutdownBarrier.client;
-  },
-
-  /**
    * The session recorder instance managed by Telemetry.
    * @return {Object} The active SessionRecorder instance or null if not available.
    */
   getSessionRecorder: function() {
     return Impl._sessionRecorder;
   },
 
   /**
@@ -616,16 +619,19 @@ var Impl = {
    * for performance reasons.
    *
    * This delayed initialization means TelemetryController init can be in the following states:
    * 1) setupTelemetry was never called
    * or it was called and
    *   2) _delayedInitTask was scheduled, but didn't run yet.
    *   3) _delayedInitTask is currently running.
    *   4) _delayedInitTask finished running and is nulled out.
+   *
+   * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully
+   *                   initialized. This is only used in tests.
    */
   setupTelemetry: function setupTelemetry(testing) {
     this._initStarted = true;
     this._testMode = testing;
 
     this._log.trace("setupTelemetry");
 
     if (this._delayedInitTask) {
@@ -649,16 +655,20 @@ var Impl = {
     // Initialize the session recorder.
     if (!this._sessionRecorder) {
       this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
       this._sessionRecorder.onStartup();
     }
 
     this._attachObservers();
 
+    // Perform a lightweight, early initialization for the component, just registering
+    // a few observers and initializing the session.
+    TelemetrySession.earlyInit(this._testMode);
+
     // For very short session durations, we may never load the client
     // id from disk.
     // We try to cache it in prefs to avoid this, even though this may
     // lead to some stale client ids.
     this._clientID = ClientID.getCachedClientID();
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
@@ -670,18 +680,21 @@ var Impl = {
         this._initialized = true;
         TelemetryEnvironment.delayedInit();
 
         yield TelemetrySend.setup(this._testMode);
 
         // Load the ClientID.
         this._clientID = yield ClientID.getClientID();
 
-        // Purge the pings archive by removing outdated pings. We don't wait for this
-        // task to complete, but TelemetryStorage blocks on it during shutdown.
+        // Perform TelemetrySession delayed init.
+        yield TelemetrySession.delayedInit();
+        // Purge the pings archive by removing outdated pings. We don't wait for
+        // this task to complete, but TelemetryStorage blocks on it during
+        // shutdown.
         TelemetryStorage.runCleanPingArchiveTask();
 
         // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from
         // the profile directory. This is a temporary measure that we should drop
         // in the future.
         TelemetryStorage.removeFHRDatabase();
 
         this._delayedInitTaskDeferred.resolve();
@@ -708,16 +721,17 @@ var Impl = {
     this._testMode = testing;
 
     // We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags
     // are in sync between chrome and content processes.
     if (!this.enableTelemetryRecording()) {
       this._log.trace("setupContentTelemetry - Content process recording disabled.");
       return;
     }
+    TelemetrySession.setupContent(testing);
   },
 
   // Do proper shutdown waiting and cleanup.
   _cleanupOnShutdown: Task.async(function*() {
     if (!this._initialized) {
       return;
     }
 
@@ -728,16 +742,18 @@ var Impl = {
     try {
       // Stop the datachoices infobar display.
       TelemetryReportingPolicy.shutdown();
       TelemetryEnvironment.shutdown();
 
       // Stop any ping sending.
       yield TelemetrySend.shutdown();
 
+      yield TelemetrySession.shutdown();
+
       // First wait for clients processing shutdown.
       yield this._shutdownBarrier.wait();
 
       // ... and wait for any outstanding async ping activity.
       yield this._connectionsBarrier.wait();
 
       // Perform final shutdown operations.
       yield TelemetryStorage.shutdown();
@@ -888,18 +904,28 @@ var Impl = {
 
     return ping;
   },
 
   reset: Task.async(function*() {
     this._clientID = null;
     this._detachObservers();
 
+    yield TelemetrySession.testReset();
+
+    this._connectionsBarrier = new AsyncShutdown.Barrier(
+      "TelemetryController: Waiting for pending ping activity"
+    );
+    this._shutdownBarrier = new AsyncShutdown.Barrier(
+      "TelemetryController: Waiting for clients."
+    );
+
     // We need to kick of the controller setup first for tests that check the
     // cached client id.
     let controllerSetup = this.setupTelemetry(true);
 
     yield TelemetrySend.reset();
     yield TelemetryStorage.reset();
+    yield TelemetryEnvironment.testReset();
 
     yield controllerSetup;
   }),
 };
--- a/toolkit/components/telemetry/TelemetryEnvironment.jsm
+++ b/toolkit/components/telemetry/TelemetryEnvironment.jsm
@@ -83,19 +83,31 @@ this.TelemetryEnvironment = {
     return getGlobal().shutdown();
   },
 
   // Policy to use when saving preferences. Exported for using them in tests.
   RECORD_PREF_STATE: 1, // Don't record the preference value
   RECORD_PREF_VALUE: 2, // We only record user-set prefs.
 
   // Testing method
-  _watchPreferences: function(prefMap) {
+  testWatchPreferences: function(prefMap) {
     return getGlobal()._watchPreferences(prefMap);
   },
+
+  /**
+   * Intended for use in tests only.
+   *
+   * In multiple tests we need a way to shut and re-start telemetry together
+   * with TelemetryEnvironment. This is problematic due to the fact that
+   * TelemetryEnvironment is a singleton. We, therefore, need this helper
+   * method to be able to re-set TelemetryEnvironment.
+   */
+  testReset: function() {
+    return getGlobal().reset();
+  },
 };
 
 const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
 const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
 const DEFAULT_ENVIRONMENT_PREFS = new Map([
   ["app.feedback.baseURL", {what: RECORD_PREF_VALUE}],
   ["app.support.baseURL", {what: RECORD_PREF_VALUE}],
   ["accessibility.browsewithcaret", {what: RECORD_PREF_VALUE}],
@@ -1407,9 +1419,14 @@ EnvironmentCache.prototype = {
       try {
         this._log.debug("_onEnvironmentChange - calling " + name);
         listener(what, oldEnvironment);
       } catch (e) {
         this._log.error("_onEnvironmentChange - listener " + name + " caught error", e);
       }
     }
   },
+
+  reset: function () {
+    this._shutdown = false;
+    this._delayedInitFinished = false;
+  }
 };
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -623,59 +623,64 @@ this.TelemetrySession = Object.freeze({
    * @return The metadata as a JS object
    */
   getMetadata: function(reason) {
     return Impl.getMetadata(reason);
   },
   /**
    * Used only for testing purposes.
    */
-  reset: function() {
+  testReset: function() {
     Impl._sessionId = null;
     Impl._subsessionId = null;
     Impl._previousSessionId = null;
     Impl._previousSubsessionId = null;
     Impl._subsessionCounter = 0;
     Impl._profileSubsessionCounter = 0;
     Impl._subsessionStartActiveTicks = 0;
     Impl._subsessionStartTimeMonotonic = 0;
-    this.uninstall();
-    return this.setup();
+    this.testUninstall();
   },
   /**
-   * Used only for testing purposes.
-   * @param {Boolean} [aForceSavePending=true] If true, always saves the ping whether Telemetry
-   *        can send pings or not, which is used for testing.
+   * Triggers shutdown of the module.
    */
-  shutdown: function(aForceSavePending = true) {
-    return Impl.shutdownChromeProcess(aForceSavePending);
+  shutdown: function() {
+    return Impl.shutdownChromeProcess();
+  },
+  /**
+   * Sets up components used in the content process.
+   */
+  setupContent: function(testing = false) {
+    return Impl.setupContentProcess(testing);
   },
   /**
    * Used only for testing purposes.
    */
-  setup: function() {
-    return Impl.setupChromeProcess(true);
-  },
-  /**
-   * Used only for testing purposes.
-   */
-  setupContent: function() {
-    return Impl.setupContentProcess(true);
-  },
-  /**
-   * Used only for testing purposes.
-   */
-  uninstall: function() {
+  testUninstall: function() {
     try {
       Impl.uninstall();
     } catch (ex) {
       // Ignore errors
     }
   },
   /**
+   * Lightweight init function, called as soon as Firefox starts.
+   */
+  earlyInit: function(aTesting = false) {
+    return Impl.earlyInit(aTesting);
+  },
+  /**
+   * Does the "heavy" Telemetry initialization later on, so we
+   * don't impact startup performance.
+   * @return {Promise} Resolved when the initialization completes.
+   */
+  delayedInit: function() {
+    return Impl.delayedInit();
+  },
+  /**
    * Send a notification.
    */
   observe: function (aSubject, aTopic, aData) {
     return Impl.observe(aSubject, aTopic, aData);
   },
 });
 
 var Impl = {
@@ -727,27 +732,25 @@ var Impl = {
   _subsessionStartDate: null,
   // Start time of the current subsession using a monotonic clock for the subsession
   // length measurements.
   _subsessionStartTimeMonotonic: 0,
   // The active ticks counted when the subsession starts
   _subsessionStartActiveTicks: 0,
   // A task performing delayed initialization of the chrome process
   _delayedInitTask: null,
-  // The deferred promise resolved when the initialization task completes.
-  _delayedInitTaskDeferred: null,
   // Need a timeout in case children are tardy in giving back their memory reports.
   _totalMemoryTimeout: undefined,
+  _testing: false,
   // An accumulator of total memory across all processes. Only valid once the final child reports.
   _totalMemory: null,
   // A Set of outstanding USS report ids
   _childrenToHearFrom: null,
   // monotonically-increasing id for USS reports
   _nextTotalMemoryId: 1,
-  _testing: false,
 
 
   get _log() {
     if (!this._logger) {
       this._logger = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
     }
     return this._logger;
   },
@@ -1392,36 +1395,32 @@ var Impl = {
       // observer and catch if it fails because the observer was not added.
       Services.obs.removeObserver(this, TOPIC_CYCLE_COLLECTOR_BEGIN);
     } catch (e) {
       this._log.warn("detachObservers - Failed to remove " + TOPIC_CYCLE_COLLECTOR_BEGIN, e);
     }
   },
 
   /**
-   * Initializes telemetry within a timer.
+   * Lightweight init function, called as soon as Firefox starts.
    */
-  setupChromeProcess: function setupChromeProcess(testing) {
+  earlyInit: function(testing) {
+    this._log.trace("earlyInit");
+
     this._initStarted = true;
-    this._log.trace("setupChromeProcess");
     this._testing = testing;
 
-    if (this._delayedInitTask) {
-      this._log.error("setupChromeProcess - init task already running");
-      return this._delayedInitTaskDeferred.promise;
-    }
-
     if (this._initialized && !testing) {
-      this._log.error("setupChromeProcess - already initialized");
-      return Promise.resolve();
+      this._log.error("earlyInit - already initialized");
+      return;
     }
 
     if (!Telemetry.canRecordBase && !testing) {
-      this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
-      return Promise.resolve();
+      this._log.config("earlyInit - Telemetry recording is disabled, skipping Chrome process setup.");
+      return;
     }
 
     // Generate a unique id once per session so the server can cope with duplicate
     // submissions, orphaning and other oddities. The id is shared across subsessions.
     this._sessionId = Policy.generateSessionUUID();
     this.startNewSubsession();
     // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
     // the very same value for |_sessionStartDate|.
@@ -1438,37 +1437,38 @@ var Impl = {
     let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null);
     let thisBuildID = Services.appinfo.appBuildID;
     // If there is no previousBuildId preference, we send null to the server.
     if (previousBuildId != thisBuildID) {
       this._previousBuildId = previousBuildId;
       Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID);
     }
 
-    TelemetryController.shutdown.addBlocker("TelemetrySession: shutting down",
-                                      () => this.shutdownChromeProcess(),
-                                      () => this._getState());
-
     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
     if (AppConstants.platform === "android") {
       Services.obs.addObserver(this, "application-background", false);
     }
     Services.obs.addObserver(this, "xul-window-visible", false);
     this._hasWindowRestoredObserver = true;
     this._hasXulWindowVisibleObserver = true;
 
     ppml.addMessageListener(MESSAGE_TELEMETRY_PAYLOAD, this);
     ppml.addMessageListener(MESSAGE_TELEMETRY_THREAD_HANGS, this);
     ppml.addMessageListener(MESSAGE_TELEMETRY_USS, this);
+},
 
-    // Delay full telemetry initialization to give the browser time to
-    // run various late initializers. Otherwise our gathered memory
-    // footprint and other numbers would be too optimistic.
-    this._delayedInitTaskDeferred = Promise.defer();
-    this._delayedInitTask = new DeferredTask(function* () {
+/**
+  * Does the "heavy" Telemetry initialization later on, so we
+  * don't impact startup performance.
+  * @return {Promise} Resolved when the initialization completes.
+  */
+  delayedInit:function() {
+    this._log.trace("delayedInit");
+
+    this._delayedInitTask = Task.spawn(function* () {
       try {
         this._initialized = true;
 
         yield this._loadSessionData();
         // Update the session data to keep track of new subsessions created before
         // the initialization.
         yield TelemetryStorage.saveSessionData(this._getSessionDataObject());
 
@@ -1479,40 +1479,37 @@ var Impl = {
 
         if (IS_UNIFIED_TELEMETRY) {
           // Check for a previously written aborted session ping.
           yield TelemetryController.checkAbortedSessionPing();
 
           // Write the first aborted-session ping as early as possible. Just do that
           // if we are not testing, since calling Telemetry.reset() will make a previous
           // aborted ping a pending ping.
-          if (!testing) {
+          if (!this._testing) {
             yield this._saveAbortedSessionPing();
           }
 
           TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
                                  (reason, data) => this._onEnvironmentChange(reason, data));
 
           // Start the scheduler.
           // We skip this if unified telemetry is off, so we don't
           // trigger the new unified ping types.
           TelemetryScheduler.init();
         }
 
-        this._delayedInitTaskDeferred.resolve();
+        this._delayedInitTask = null;
       } catch (e) {
-        this._delayedInitTaskDeferred.reject(e);
-      } finally {
         this._delayedInitTask = null;
-        this._delayedInitTaskDeferred = null;
+        throw e;
       }
-    }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
+    }.bind(this));
 
-    this._delayedInitTask.arm();
-    return this._delayedInitTaskDeferred.promise;
+    return this._delayedInitTask;
   },
 
   /**
    * Initializes telemetry for a content process.
    */
   setupContentProcess: function setupContentProcess(testing) {
     this._log.trace("setupContentProcess");
     this._testing = testing;
@@ -1839,22 +1836,16 @@ var Impl = {
    */
   observe: function (aSubject, aTopic, aData) {
     // Prevent the cycle collector begin topic from cluttering the log.
     if (aTopic != TOPIC_CYCLE_COLLECTOR_BEGIN) {
       this._log.trace("observe - " + aTopic + " notified.");
     }
 
     switch (aTopic) {
-    case "profile-after-change":
-      // profile-after-change is only registered for chrome processes.
-      return this.setupChromeProcess();
-    case "app-startup":
-      // app-startup is only registered for content processes.
-      return this.setupContentProcess();
     case "content-child-shutdown":
       // content-child-shutdown is only registered for content processes.
       Services.obs.removeObserver(this, "content-child-shutdown");
       this.uninstall();
 
       this.sendContentProcessPing(REASON_SAVED_SESSION);
       break;
     case TOPIC_CYCLE_COLLECTOR_BEGIN:
@@ -1921,21 +1912,19 @@ var Impl = {
       TelemetryController.addPendingPing(getPingType(payload), payload, options);
       break;
     }
     return undefined;
   },
 
   /**
    * This tells TelemetrySession to uninitialize and save any pending pings.
-   * @param testing Optional. If true, always saves the ping whether Telemetry
-   *                can send pings or not, which is used for testing.
    */
-  shutdownChromeProcess: function(testing = false) {
-    this._log.trace("shutdownChromeProcess - testing: " + testing);
+  shutdownChromeProcess: function() {
+    this._log.trace("shutdownChromeProcess");
 
     let cleanup = () => {
       if (IS_UNIFIED_TELEMETRY) {
         TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
         TelemetryScheduler.shutdown();
       }
       this.uninstall();
 
@@ -1951,35 +1940,34 @@ var Impl = {
           yield TelemetryController.removeAbortedSessionPing();
         }
 
         reset();
       }.bind(this));
     };
 
     // We can be in one the following states here:
-    // 1) setupChromeProcess was never called
+    // 1) delayedInit was never called
     // or it was called and
-    //   2) _delayedInitTask was scheduled, but didn't run yet.
-    //   3) _delayedInitTask is running now.
-    //   4) _delayedInitTask finished running already.
+    //   2) _delayedInitTask is running now.
+    //   3) _delayedInitTask finished running already.
 
     // This handles 1).
     if (!this._initStarted) {
       return Promise.resolve();
      }
 
-    // This handles 4).
+    // This handles 3).
     if (!this._delayedInitTask) {
       // We already ran the delayed initialization.
       return cleanup();
      }
 
-    // This handles 2) and 3).
-    return this._delayedInitTask.finalize().then(cleanup);
+     // This handles 2).
+     return this._delayedInitTask.then(cleanup);
    },
 
   /**
    * Gather and send a daily ping.
    * @return {Promise} Resolved when the ping is sent.
    */
   _sendDailyPing: function() {
     this._log.trace("_sendDailyPing");
--- a/toolkit/components/telemetry/TelemetryStartup.js
+++ b/toolkit/components/telemetry/TelemetryStartup.js
@@ -6,34 +6,31 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController",
                                   "resource://gre/modules/TelemetryController.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "TelemetrySession",
-                                  "resource://gre/modules/TelemetrySession.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
                                   "resource://gre/modules/TelemetryEnvironment.jsm");
 
 /**
  * TelemetryStartup is needed to forward the "profile-after-change" notification
  * to TelemetryController.jsm.
  */
 function TelemetryStartup() {
 }
 
 TelemetryStartup.prototype.classID = Components.ID("{117b219f-92fe-4bd2-a21b-95a342a9d474}");
 TelemetryStartup.prototype.QueryInterface = XPCOMUtils.generateQI([Components.interfaces.nsIObserver]);
 TelemetryStartup.prototype.observe = function(aSubject, aTopic, aData) {
   if (aTopic == "profile-after-change" || aTopic == "app-startup") {
     TelemetryController.observe(null, aTopic, null);
-    TelemetrySession.observe(null, aTopic, null);
   }
   if (aTopic == "profile-after-change") {
     annotateEnvironment();
     TelemetryEnvironment.registerChangeListener("CrashAnnotator", annotateEnvironment);
     TelemetryEnvironment.onInitialized().then(() => annotateEnvironment());
   }
 }
 
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -442,16 +442,23 @@ this.TelemetryStorage = {
    *                  Otherwise an object with the extracted data in the form:
    *                  { timestamp: <number>,
    *                    id: <string>,
    *                    type: <string> }
    */
   _testGetArchivedPingDataFromFileName: function(aFileName) {
     return TelemetryStorageImpl._getArchivedPingDataFromFileName(aFileName);
   },
+
+  /**
+   * Only used in tests, this helper allows cleaning up the pending ping storage.
+   */
+  testClearPendingPings: function() {
+    return TelemetryStorageImpl.runRemovePendingPingsTask();
+  }
 };
 
 /**
  * This object allows the serialisation of asynchronous tasks. This is particularly
  * useful to serialise write access to the disk in order to prevent race conditions
  * to corrupt the data being written.
  * We are using this to synchronize saving to the file that TelemetrySession persists
  * its state in.
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -294,14 +294,14 @@ if (runningInParent) {
   fakePingSendTimer((callback, timeout) => {
     Services.tm.mainThread.dispatch(() => callback(), Ci.nsIThread.DISPATCH_NORMAL);
   },
   () => {});
 
   do_register_cleanup(() => TelemetrySend.shutdown());
 }
 
-TelemetryController.initLogging();
+TelemetryController.testInitLogging();
 
 // Avoid timers interrupting test behavior.
 fakeSchedulerTimer(() => {}, () => {});
 // Make pind sending predictable.
 fakeMidnightPingFuzzingDelay(0);
--- a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -52,32 +52,30 @@ function check_histogram_values(payload)
   Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["a"].sum, 1,
                "Keyed flag test histogram should have the right value.");
   Assert.equal(kh["TELEMETRY_TEST_KEYED_FLAG"]["b"].sum, 1,
                "Keyed flag test histogram should have the right value.");
 }
 
 add_task(function*() {
   if (!runningInParent) {
-    TelemetryController.setupContent();
-    TelemetrySession.setupContent();
+    TelemetryController.testSetupContent();
     run_child_test();
     dump("... done with child test\n");
     do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
     dump("... waiting for child payload collection\n");
     yield do_await_remote_message(MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD);
     return;
   }
 
   // Setup.
   do_get_profile(true);
   loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
-  yield TelemetryController.setup();
-  yield TelemetrySession.setup();
+  yield TelemetryController.testSetup();
 
   // Run test in child, don't wait for it to finish.
   let childPromise = run_test_in_child("test_ChildHistograms.js");
   yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
 
   // Gather payload from child.
   dump("... requesting child payloads\n");
   let promiseMessage = do_await_remote_message(MESSAGE_TELEMETRY_PAYLOAD);
--- a/toolkit/components/telemetry/tests/unit/test_PingAPI.js
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -2,19 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/
 */
 
 // This tests the public Telemetry API for submitting pings.
 
 "use strict";
 
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
-Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
-Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Services.jsm", this);
 
 XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
   return OS.Path.join(OS.Constants.Path.profileDir, "datareporting", "archived");
 });
@@ -115,17 +113,17 @@ add_task(function* test_archivedPings() 
       Assert.equal(ping.creationDate, data.dateCreated.toISOString(),
                    "Archived ping should have matching creation date");
     }
   });
 
   yield checkLoadingPings();
 
   // Check that we find the archived pings again by scanning after a restart.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   let pingList = yield TelemetryArchive.promiseArchivedPingList();
   Assert.deepEqual(expectedPingList, pingList,
                    "Should have submitted pings in archive list after restart");
   yield checkLoadingPings();
 
   // Write invalid pings into the archive with both valid and invalid names.
   let writeToArchivedDir = Task.async(function*(dirname, filename, content, compressed) {
@@ -169,17 +167,17 @@ add_task(function* test_archivedPings() 
   expectedPingList.push({
     id: FAKE_ID3,
     type: "foo",
     timestampCreated: 4,
   });
   expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated);
 
   // Reset the TelemetryArchive so we scan the archived dir again.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   // Check that we are still picking up the valid archived pings on disk,
   // plus the valid ones above.
   pingList = yield TelemetryArchive.promiseArchivedPingList();
   Assert.deepEqual(expectedPingList, pingList, "Should have picked up valid archived pings");
   yield checkLoadingPings();
 
   // Now check that we fail to load the two invalid pings from above.
@@ -197,17 +195,17 @@ add_task(function* test_archiveCleanup()
 
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear();
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear();
   // Also reset these histograms to make sure normal sized pings don't get counted.
   Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear();
   Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").clear();
 
   // Build the cache. Nothing should be evicted as there's no ping directory.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testCleanupTaskPromise();
   yield TelemetryArchive.promiseArchivedPingList();
 
   let h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").snapshot();
   Assert.equal(h.sum, 0, "Telemetry must report 0 pings scanned if no archive dir exists.");
   // One directory out of four was removed as well.
   h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").snapshot();
   Assert.equal(h.sum, 0, "Telemetry must report 0 evicted dirs if no archive dir exists.");
@@ -259,17 +257,17 @@ add_task(function* test_archiveCleanup()
   Assert.equal(h.snapshot().sum, 22, "All the pings must be live-accumulated in the histogram.");
   // Reset the histogram that will be populated by the archive scan.
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear();
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear();
 
   // Move the current date 180 days ahead of the first ping.
   let now = fakeNow(2010, 7, 1, 1, 0, 0);
   // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   // Wait for the cleanup to finish.
   yield TelemetryStorage.testCleanupTaskPromise();
   // Then scan the archived dir.
   yield TelemetryArchive.promiseArchivedPingList();
 
   // Check that the archive is in the correct state.
   yield checkArchive();
 
@@ -292,17 +290,17 @@ add_task(function* test_archiveCleanup()
   // value is recorded.
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear();
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear();
   Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS").clear();
 
   // Move the current date 180 days ahead of the second ping.
   fakeNow(2010, 8, 1, 1, 0, 0);
   // Reset TelemetryController and TelemetryArchive.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   // Wait for the cleanup to finish.
   yield TelemetryStorage.testCleanupTaskPromise();
   // Then scan the archived dir again.
   yield TelemetryArchive.promiseArchivedPingList();
 
   // Move the oldest ping to the unexpected pings list.
   expectedPrunedInfo.push(expectedNotPrunedInfo.shift());
   // Check that the archive is in the correct state.
@@ -341,30 +339,30 @@ add_task(function* test_archiveCleanup()
     }
     pingsWithinQuota.push({ id: pingInfo.id, creationDate: new Date(pingInfo.timestamp) });
   }
 
   expectedNotPrunedInfo = pingsWithinQuota;
   expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota);
 
   // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testCleanupTaskPromise();
   yield TelemetryArchive.promiseArchivedPingList();
   // Check that the archive is in the correct state.
   yield checkArchive();
 
   h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").snapshot();
   Assert.equal(h.sum, pingsOutsideQuota.length,
                "Telemetry must correctly report the over quota pings evicted from the archive.");
   h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
   Assert.equal(h.sum, 300, "Archive quota was hit, a special size must be reported.");
 
   // Trigger a cleanup again and make sure we're not removing anything.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testCleanupTaskPromise();
   yield TelemetryArchive.promiseArchivedPingList();
   yield checkArchive();
 
   const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
   // Create and archive an oversized, uncompressed, ping.
   const OVERSIZED_PING = {
     id: OVERSIZED_PING_ID,
@@ -379,48 +377,48 @@ add_task(function* test_archiveCleanup()
   const oversizedPingPath =
     TelemetryStorage._testGetArchivedPingPath(OVERSIZED_PING.id, new Date(OVERSIZED_PING.creationDate), PING_TYPE) + "lz4";
   const archivedPingSizeMB = Math.floor((yield OS.File.stat(oversizedPingPath)).size / 1024 / 1024);
 
   // We expect the oversized ping to be pruned when scanning the archive.
   expectedPrunedInfo.push({ id: OVERSIZED_PING_ID, creationDate: new Date(OVERSIZED_PING.creationDate) });
 
   // Scan the archive.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testCleanupTaskPromise();
   yield TelemetryArchive.promiseArchivedPingList();
   // The following also checks that non oversized pings are not removed.
   yield checkArchive();
 
   // Make sure we're correctly updating the related histograms.
   h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").snapshot();
   Assert.equal(h.sum, 1, "Telemetry must report 1 oversized ping in the archive.");
   h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB").snapshot();
   Assert.equal(h.counts[archivedPingSizeMB], 1,
                "Telemetry must report the correct size for the oversized ping.");
 });
 
 add_task(function* test_clientId() {
   // Check that a ping submitted after the delayed telemetry initialization completed
   // should get a valid client id.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   const clientId = TelemetryController.clientID;
 
   let id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true});
   let ping = yield TelemetryArchive.promiseArchivedPingById(id);
 
   Assert.ok(!!ping, "Should have loaded the ping.");
   Assert.ok("clientId" in ping, "Ping should have a client id.");
   Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format.");
   Assert.equal(ping.clientId, clientId, "Ping client id should match the global client id.");
 
   // We should have cached the client id now. Lets confirm that by
   // checking the client id on a ping submitted before the async
   // controller setup is finished.
-  let promiseSetup = TelemetryController.reset();
+  let promiseSetup = TelemetryController.testReset();
   id = yield TelemetryController.submitExternalPing("test-type", {}, {addClientId: true});
   ping = yield TelemetryArchive.promiseArchivedPingById(id);
   Assert.equal(ping.clientId, clientId);
 
   // Finish setup.
   yield promiseSetup;
 });
 
@@ -443,17 +441,17 @@ add_task(function* test_InvalidPingType(
     Assert.ok(promiseRejects(TelemetryController.submitExternalPing(type, {})),
               "Ping type should have been rejected.");
     Assert.equal(histogram.snapshot(type).sum, 1,
                  "Should have counted this as an invalid ping type.");
   }
 });
 
 add_task(function* test_currentPingData() {
-  yield TelemetrySession.setup();
+  yield TelemetryController.testSetup();
 
   // Setup test data.
   let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
   h.clear();
   h.add(1);
   let k = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
   k.clear();
   k.add("a", 1);
@@ -472,10 +470,10 @@ add_task(function* test_currentPingData(
     Assert.equal(ping.payload.histograms[id].sum, 1, "Test count value should match.");
     id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT";
     Assert.ok(id in ping.payload.keyedHistograms, "Payload should have keyed test histogram.");
     Assert.equal(ping.payload.keyedHistograms[id]["a"].sum, 1, "Keyed test value should match.");
   }
 });
 
 add_task(function* test_shutdown() {
-  yield TelemetrySend.shutdown();
+  yield TelemetryController.testShutdown();
 });
--- a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -117,100 +117,98 @@ add_task(function* test_subsessionsChain
     fakeNow(now);
   }
 
   // Keep track of the ping reasons we're expecting in this test.
   let expectedReasons = [];
 
   // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
   // subsessionCounter: 1, subsessionId: A,  and previousSubsessionId: null to be archived.
-  yield TelemetrySession.reset();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testSetup();
+  yield TelemetryController.testShutdown();
   expectedReasons.push(REASON_SHUTDOWN);
 
   // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
   // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
   // and previousSubsessionId: A to be archived.
   moveClockForward(30);
-  TelemetrySession.reset();
-  yield TelemetrySession.shutdown();
+  TelemetryController.testReset();
+  yield TelemetryController.testShutdown();
   expectedReasons.push(REASON_SHUTDOWN);
 
   // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
   // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
   // previousSubsessionId: B to be archived.
   let schedulerTickCallback = null;
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   moveClockForward(6);
   // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
   // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
-  // sent when calling |TelemetrySession.reset()|, thus breaking some tests.
+  // sent when calling |TelemetryController.testReset()|, thus breaking some tests.
   Assert.ok(!!schedulerTickCallback);
   yield schedulerTickCallback();
   expectedReasons.push(REASON_ABORTED_SESSION);
 
   // Start Telemetry and trigger an environment change through a pref modification. We expect
   // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
   // subsessionId: D and previousSubsessionId: C to be archived.
   moveClockForward(30);
-  yield TelemetryController.reset();
-  yield TelemetrySession.reset();
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  yield TelemetryController.testReset();
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   moveClockForward(30);
   Preferences.set(PREF_TEST, 1);
   expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
 
   // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
   // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
   moveClockForward(30);
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
   expectedReasons.push(REASON_SHUTDOWN);
 
   // Start Telemetry and trigger a daily ping. We expect a daily ping with
   // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
   // previousSubsessionId: E to be archived.
   moveClockForward(30);
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // Delay the callback around midnight.
   now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
   // Trigger the daily ping.
   yield schedulerTickCallback();
   expectedReasons.push(REASON_DAILY);
 
   // Trigger an environment change ping. We expect an environment-changed ping with
   // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
   // previousSubsessionId: F to be archived.
   moveClockForward(30);
   Preferences.set(PREF_TEST, 0);
   expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
 
   // Shut down Telemetry and trigger a shutdown ping.
   moveClockForward(30);
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
   expectedReasons.push(REASON_SHUTDOWN);
 
   // Start Telemetry and trigger an environment change.
-  yield TelemetrySession.reset();
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  yield TelemetryController.testReset();
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   moveClockForward(30);
   Preferences.set(PREF_TEST, 1);
   expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
 
   // Don't shut down, instead trigger an aborted-session ping.
   moveClockForward(6);
   // Trigger the an aborted session ping save.
   yield schedulerTickCallback();
   expectedReasons.push(REASON_ABORTED_SESSION);
 
   // Start Telemetry and trigger a daily ping.
   moveClockForward(30);
-  yield TelemetryController.reset();
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   // Delay the callback around midnight.
   now = futureDate(now, MS_IN_ONE_DAY);
   fakeNow(now);
   // Trigger the daily ping.
   yield schedulerTickCallback();
   expectedReasons.push(REASON_DAILY);
 
   // Trigger an environment change.
@@ -220,18 +218,17 @@ add_task(function* test_subsessionsChain
 
   // And an aborted-session ping again.
   moveClockForward(6);
   // Trigger the an aborted session ping save.
   yield schedulerTickCallback();
   expectedReasons.push(REASON_ABORTED_SESSION);
 
   // Make sure the aborted-session ping gets archived.
-  yield TelemetryController.reset();
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   yield promiseValidateArchivedPings(expectedReasons);
 });
 
 add_task(function* () {
-  yield TelemetrySend.shutdown();
+  yield TelemetryController.testShutdown();
   do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -99,23 +99,23 @@ function run_test() {
 
   Services.prefs.setBoolPref(PREF_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
 }
 
 add_task(function* asyncSetup() {
-  yield TelemetryController.setup();
+  yield TelemetryController.testSetup();
 
   gClientID = yield ClientID.getClientID();
 
   // We should have cached the client id now. Lets confirm that by
   // checking the client id before the async ping setup is finished.
-  let promisePingSetup = TelemetryController.reset();
+  let promisePingSetup = TelemetryController.testReset();
   do_check_eq(TelemetryController.clientID, gClientID);
   yield promisePingSetup;
 });
 
 // Ensure that not overwriting an existing file fails silently
 add_task(function* test_overwritePing() {
   let ping = {id: "foo"};
   yield TelemetryStorage.savePing(ping, true);
@@ -166,37 +166,37 @@ add_task(function* test_disableDataUploa
   yield PingServer.stop();
 
   // Try to send a ping. It will be saved as pending  and get deleted when disabling upload.
   TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
 
   // Disable FHR upload to send a deletion ping again.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
 
-  // Wait on sending activity to settle, as |TelemetryController.reset()| doesn't do that.
+  // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.
   yield TelemetrySend.testWaitOnOutgoingPings();
   // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
   // trigger the shutdown, so we need to call it ourselves.
   yield TelemetryStorage.shutdown();
   // Simulate a restart, and spin the send task.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   // Disabling Telemetry upload must clear out all the pending pings.
   let pendingPings = yield TelemetryStorage.loadPendingPingList();
   Assert.equal(pendingPings.length, 1,
                "All the pending pings but the deletion ping should have been deleted");
 
   // Enable the ping server again.
   PingServer.start();
   // We set the new server using the pref, otherwise it would get reset with
-  // |TelemetryController.reset|.
+  // |TelemetryController.testReset|.
   Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
 
   // Reset the controller to spin the ping sending task.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   ping = yield PingServer.promiseNextPing();
   checkPingFormat(ping, DELETION_PING_TYPE, true, false);
 
   // Restore FHR Upload.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
 });
 
 add_task(function* test_pingHasClientId() {
@@ -296,17 +296,17 @@ add_task(function* test_midnightPingSend
 
   let waitForTimer = () => new Promise(resolve => {
     fakePingSendTimer((callback, timeout) => {
       resolve([callback, timeout]);
     }, () => {});
   });
 
   PingServer.clearRequests();
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   // A ping after midnight within the fuzzing delay should not get sent.
   now = new Date(2030, 5, 2, 0, 40, 0);
   fakeNow(now);
   PingServer.registerPingHandler((req, res) => {
     Assert.ok(false, "No ping should be received yet.");
   });
   let timerPromise = waitForTimer();
@@ -377,32 +377,32 @@ add_task(function* test_telemetryEnabled
   // Remove the default value for toolkit.telemetry.enabled from the default prefs.
   // Otherwise, we wouldn't be able to set the pref to a string.
   let defaultPrefBranch = Services.prefs.getDefaultBranch(null);
   defaultPrefBranch.deleteBranch(PREF_ENABLED);
 
   // Set the preferences controlling the Telemetry status to a string.
   Preferences.set(PREF_ENABLED, "false");
   // Check that Telemetry is not enabled.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   Assert.equal(Telemetry.canRecordExtended, false,
                "Invalid values must not enable Telemetry recording.");
 
   // Delete the pref again.
   defaultPrefBranch.deleteBranch(PREF_ENABLED);
 
   // Make sure that flipping it to true works.
   Preferences.set(PREF_ENABLED, true);
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   Assert.equal(Telemetry.canRecordExtended, true,
                "True must enable Telemetry recording.");
 
   // Also check that the false works as well.
   Preferences.set(PREF_ENABLED, false);
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   Assert.equal(Telemetry.canRecordExtended, false,
                "False must disable Telemetry recording.");
 });
 
 add_task(function* test_telemetryCleanFHRDatabase(){
   const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";
   const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";
   const DEFAULT_DB_NAME = "healthreport.sqlite";
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
@@ -15,51 +15,51 @@
 
 "use strict";
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/TelemetryController.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-// Force the Telemetry enabled preference so that TelemetrySession.reset() doesn't exit early.
+// Force the Telemetry enabled preference so that TelemetrySession.testReset() doesn't exit early.
 Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
 
 // Set up our dummy AppInfo object so we can control the appBuildID.
 Cu.import("resource://testing-common/AppInfo.jsm", this);
 updateAppInfo();
 
 // Check that when run with no previous build ID stored, we update the pref but do not
 // put anything into the metadata.
 add_task(function* test_firstRun() {
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   let metadata = TelemetrySession.getMetadata();
   do_check_false("previousBuildID" in metadata);
   let appBuildID = getAppInfo().appBuildID;
   let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
   do_check_eq(appBuildID, buildIDPref);
 });
 
 // Check that a subsequent run with the same build ID does not put prev build ID in
 // metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
 add_task(function* test_secondRun() {
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   let metadata = TelemetrySession.getMetadata();
   do_check_false("previousBuildID" in metadata);
 });
 
 // Set up telemetry with a different app build ID and check that the old build ID
 // is returned in the metadata and the pref is updated to the new build ID.
 // Assumes testFirstRun() has been called to set the previousBuildID pref.
 const NEW_BUILD_ID = "20130314";
 add_task(function* test_newBuild() {
   let info = getAppInfo();
   let oldBuildID = info.appBuildID;
   info.appBuildID = NEW_BUILD_ID;
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   let metadata = TelemetrySession.getMetadata();
   do_check_eq(metadata.previousBuildId, oldBuildID);
   let buildIDPref = Services.prefs.getCharPref(TelemetrySession.Constants.PREF_PREVIOUS_BUILDID);
   do_check_eq(NEW_BUILD_ID, buildIDPref);
 });
 
 
 function run_test() {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -32,28 +32,35 @@ function run_test() {
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD_ENABLED, true);
 
   run_next_test();
 }
 
-add_task(function* test_sendTimeout() {
-  const TIMEOUT = 100;
+/**
+ * Ensures that TelemetryController does not hang processing shutdown
+ * phases. Assumes that Telemetry shutdown routines do not take longer than
+ * CRASH_TIMEOUT_MS to complete.
+ */
+add_task(function* test_sendTelemetryShutsDownWithinReasonableTimeout() {
+  const CRASH_TIMEOUT_MS = 5 * 1000;
   // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality
   // is not available.
   Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
-  Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", TIMEOUT);
+  // Reducing the max delay for waitiing on phases to complete from 1 minute
+  // (standard) to 10 seconds to avoid blocking the tests in case of misbehavior.
+  Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", CRASH_TIMEOUT_MS);
 
   let httpServer = new HttpServer();
   httpServer.registerPrefixHandler("/", contentHandler);
   httpServer.start(-1);
 
-  yield TelemetryController.setup();
+  yield TelemetryController.testSetup();
   TelemetrySend.setServer("http://localhost:" + httpServer.identity.primaryPort);
   let submissionPromise = TelemetryController.submitExternalPing("test-ping-type", {});
 
   // Trigger the AsyncShutdown phase TelemetryController hangs off.
   AsyncShutdown.profileBeforeChange._trigger();
   AsyncShutdown.sendTelemetry._trigger();
   // Now wait for the ping submission.
   yield submissionPromise;
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -37,17 +37,17 @@ add_task(function* testSendPendingOnIdle
     type: "test-ping",
     version: 4,
     application: {},
     payload: {},
   };
   yield TelemetryStorage.savePing(PENDING_PING, true);
 
   // Telemetry will not send this ping at startup, because it's not overdue.
-  yield TelemetryController.setup();
+  yield TelemetryController.testSetup();
   TelemetrySend.setServer("http://localhost:" + gHttpServer.identity.primaryPort);
 
   let pendingPromise = new Promise(resolve =>
     gHttpServer.registerPrefixHandler("/submit/telemetry/", request => resolve(request)));
 
   let gatherPromise = PromiseUtils.defer();
   Services.obs.addObserver(gatherPromise.resolve, "gather-telemetry", false);
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -811,17 +811,17 @@ add_task(function* test_prefWatchPolicie
     [PREF_TEST_4, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
     [PREF_TEST_5, {what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true}],
   ]);
 
   Preferences.set(PREF_TEST_4, expectedValue);
   Preferences.set(PREF_TEST_5, expectedValue);
 
   // Set the Environment preferences to watch.
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   let deferred = PromiseUtils.defer();
 
   // Check that the pref values are missing or present as expected
   Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1], undefined);
   Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4], expectedValue);
   Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5], expectedValue);
 
   TelemetryEnvironment.registerChangeListener("testWatchPrefs",
@@ -859,17 +859,17 @@ add_task(function* test_prefWatch_prefRe
 
   // Set the preference to a non-default value.
   Preferences.set(PREF_TEST, false);
 
   gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
   fakeNow(gNow);
 
   // Set the Environment preferences to watch.
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   let deferred = PromiseUtils.defer();
   TelemetryEnvironment.registerChangeListener("testWatchPrefs_reset", deferred.resolve);
 
   Assert.strictEqual(TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST], "<user-set>");
 
   // Trigger a change in the watched preferences.
   Preferences.reset(PREF_TEST);
   yield deferred.promise;
@@ -1287,17 +1287,17 @@ add_task(function* test_changeThrottling
     [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
   ]);
   Preferences.reset(PREF_TEST);
 
   gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
   fakeNow(gNow);
 
   // Set the Environment preferences to watch.
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   let deferred = PromiseUtils.defer();
   let changeCount = 0;
   TelemetryEnvironment.registerChangeListener("testWatchPrefs_throttling", () => {
     ++changeCount;
     deferred.resolve();
   });
 
   // The first pref change should trigger a notification.
@@ -1445,17 +1445,17 @@ add_task(function* test_defaultSearchEng
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
   ]);
   Preferences.reset(PREF_TEST);
 
   // Set the clock in the future so our changes don't get throttled.
   gNow = fakeNow(futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE));
   // Watch the test preference.
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   deferred = PromiseUtils.defer();
   TelemetryEnvironment.registerChangeListener("testSearchEngine_pref", deferred.resolve);
   // Trigger an environment change.
   Preferences.set(PREF_TEST, 1);
   yield deferred.promise;
   TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref");
 
   // Check that the search engine information is correctly retained when prefs change.
@@ -1479,17 +1479,17 @@ add_task(function* test_environmentShutd
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_STATE}],
   ]);
   Preferences.reset(PREF_TEST);
   gNow = futureDate(gNow, 10 * MILLISECONDS_PER_MINUTE);
   fakeNow(gNow);
 
   // Set up the preferences and listener, then the trigger shutdown
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   TelemetryEnvironment.registerChangeListener("test_environmentShutdownChange", () => {
   // Register a new change listener that asserts if change is propogated
     Assert.ok(false, "No change should be propagated after shutdown.");
   });
   TelemetryEnvironment.shutdown();
 
   // Flipping  the test preference after shutdown should not trigger the listener
   Preferences.set(PREF_TEST, 1);
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLog.js
@@ -22,17 +22,19 @@ function check_event(event, id, data)
       do_check_eq(event[i + 2], data[i]);
     }
   }
 }
 
 add_task(function* ()
 {
   do_get_profile();
-  yield TelemetrySession.setup();
+  // TODO: After Bug 1254550 lands we should not need to set the pref here.
+  Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+  yield TelemetryController.testSetup();
 
   TelemetryLog.log(TEST_PREFIX + "1", ["val", 123, undefined]);
   TelemetryLog.log(TEST_PREFIX + "2", []);
   TelemetryLog.log(TEST_PREFIX + "3");
 
   var log = TelemetrySession.getPayload().log.filter(function(e) {
     // Only want events that were generated by the test.
     return TEST_REGEX.test(e[0]);
@@ -40,10 +42,10 @@ add_task(function* ()
 
   do_check_eq(log.length, 3);
   check_event(log[0], TEST_PREFIX + "1", ["val", "123", "undefined"]);
   check_event(log[1], TEST_PREFIX + "2", []);
   check_event(log[2], TEST_PREFIX + "3", undefined);
   do_check_true(log[0][1] <= log[1][1]);
   do_check_true(log[1][1] <= log[2][1]);
 
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -212,17 +212,17 @@ add_task(function* test_userNotifiedOfCu
 });
 
 add_task(function* test_canSend() {
   const TEST_PING_TYPE = "test-ping";
 
   PingServer.start();
   Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port);
 
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   TelemetryReportingPolicy.reset();
 
   // User should be reported as not notified by default.
   Assert.ok(!TelemetryReportingPolicy.testIsUserNotified(),
             "The initial state should be unnotified.");
 
   // Assert if we receive any ping before the policy is accepted.
   PingServer.registerPingHandler(() => Assert.ok(false, "Should not have received any pings now"));
@@ -243,17 +243,17 @@ add_task(function* test_canSend() {
 
   // Get the ping and check its type.
   ping = yield PingServer.promiseNextPings(1);
   Assert.equal(ping.length, 1, "We should have received one ping.");
   Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the new ping.");
 
   // Fake a restart with a pending ping.
   yield TelemetryController.addPendingPing(TEST_PING_TYPE, {});
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   // We should be immediately sending the ping out.
   ping = yield PingServer.promiseNextPings(1);
   Assert.equal(ping.length, 1, "We should have received one ping.");
   Assert.equal(ping[0].type, TEST_PING_TYPE, "We should have received the pending ping.");
 
   // Submit another ping, to make sure it gets sent.
   yield TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -115,17 +115,17 @@ add_task(function* test_sendPendingPings
                "Should have correct pending ping count");
 
   // Now enable sending to the ping server.
   now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
   PingServer.start();
   Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
 
   let timerPromise = waitForTimer();
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   let [pingSendTimerCallback, pingSendTimeout] = yield timerPromise;
   Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
 
   // We should have received 10 pings from the first send batch:
   // 5 of type B and 5 of type A, as sending is newest-first.
   // The other pings should be delayed by the 10-pings-per-minute limit.
   let pings = yield PingServer.promiseNextPings(10);
   Assert.equal(TelemetrySend.pendingPingCount, TYPE_A_COUNT - 5,
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -139,26 +139,16 @@ var assertNotSaved = Task.async(function
  * receives and decodes a Telemetry payload.
  *
  * @param aRequest the HTTP request sent from HttpServer.
  */
 function pingHandler(aRequest) {
   gSeenPings++;
 }
 
-/**
- * Clear out all pending pings.
- */
-var clearPendingPings = Task.async(function*() {
-  const pending = yield TelemetryStorage.loadPendingPingList();
-  for (let p of pending) {
-    yield TelemetryStorage.removePendingPing(p.id);
-  }
-});
-
 function run_test() {
   PingServer.start();
   PingServer.registerPingHandler(pingHandler);
   do_get_profile();
   loadAddonManager("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
 
   Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
   Services.prefs.setCharPref(TelemetryController.Constants.PREF_SERVER,
@@ -169,36 +159,36 @@ function run_test() {
 /**
  * Setup the tests by making sure the ping storage directory is available, otherwise
  * |TelemetryController.testSaveDirectoryToFile| could fail.
  */
 add_task(function* setupEnvironment() {
   // The following tests assume this pref to be true by default.
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD, true);
 
-  yield TelemetryController.setup();
+  yield TelemetryController.testSetup();
 
   let directory = TelemetryStorage.pingDirectoryPath;
   yield File.makeDir(directory, { ignoreExisting: true, unixMode: OS.Constants.S_IRWXU });
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 });
 
 /**
  * Test that really recent pings are sent on Telemetry initialization.
  */
 add_task(function* test_recent_pings_sent() {
   let pingTypes = [{ num: RECENT_PINGS }];
   let recentPings = yield createSavedPings(pingTypes);
 
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetrySend.testWaitOnOutgoingPings();
   assertReceivedPings(RECENT_PINGS);
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 });
 
 /**
  * Create an overdue ping in the old format and try to send it.
  */
 add_task(function* test_overdue_old_format() {
   // A test ping in the old, standard format.
   const PING_OLD_FORMAT = {
@@ -250,25 +240,25 @@ add_task(function* test_overdue_old_form
   yield TelemetryStorage.savePing(PING_NO_PAYLOAD, true);
   yield TelemetryStorage.savePingToFile(PING_NO_SLUG, PING_FILES_PATHS[3], true);
 
   for (let f in PING_FILES_PATHS) {
     yield File.setDates(PING_FILES_PATHS[f], null, Date.now() - OVERDUE_PING_FILE_AGE);
   }
 
   gSeenPings = 0;
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetrySend.testWaitOnOutgoingPings();
   assertReceivedPings(OLD_FORMAT_PINGS);
 
   // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,
   // so remove it manually so that the next test doesn't fail.
   yield OS.File.remove(PING_FILES_PATHS[3]);
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 });
 
 add_task(function* test_corrupted_pending_pings() {
   const TEST_TYPE = "test_corrupted";
 
   Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear();
   Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear();
 
@@ -309,42 +299,42 @@ add_task(function* test_corrupted_pendin
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").snapshot();
   Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").snapshot();
   Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");
 
   let exists = yield OS.File.exists(getSavePathForPingId(pendingPingId));
   Assert.ok(!exists, "The unparseable ping should have been removed");
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 });
 
 /**
  * Create some recent and overdue pings and verify that they get sent.
  */
 add_task(function* test_overdue_pings_trigger_send() {
   let pingTypes = [
     { num: RECENT_PINGS },
     { num: OVERDUE_PINGS, age: OVERDUE_PING_FILE_AGE },
   ];
   let pings = yield createSavedPings(pingTypes);
   let recentPings = pings.slice(0, RECENT_PINGS);
   let overduePings = pings.slice(-OVERDUE_PINGS);
 
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetrySend.testWaitOnOutgoingPings();
   assertReceivedPings(TOTAL_EXPECTED_PINGS);
 
   yield assertNotSaved(recentPings);
   yield assertNotSaved(overduePings);
 
   Assert.equal(TelemetrySend.overduePingsCount, overduePings.length,
                "Should have tracked the correct amount of overdue pings");
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 });
 
 /**
  * Create a ping in the old format, send it, and make sure the request URL contains
  * the correct version query parameter.
  */
 add_task(function* test_overdue_old_format() {
   // A test ping in the old, standard format.
@@ -380,39 +370,39 @@ add_task(function* test_overdue_old_form
 
     // Make sure the version in the query string matches the old ping format version.
     let params = request.queryString.split("&");
     Assert.ok(params.find(p => p == "v=1"));
 
     receivedPings++;
   });
 
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetrySend.testWaitOnOutgoingPings();
   Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.resetPingHandler();
 });
 
 add_task(function* test_pendingPingsQuota() {
   const PING_TYPE = "foo";
 
   // Disable upload so pings don't get sent and removed from the pending pings directory.
   Services.prefs.setBoolPref(PREF_FHR_UPLOAD, false);
 
   // Remove all the pending pings then startup and wait for the cleanup task to complete.
   // There should be nothing to remove.
-  yield clearPendingPings();
-  yield TelemetryController.reset();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
   yield TelemetrySend.testWaitOnOutgoingPings();
   yield TelemetryStorage.testPendingQuotaTaskPromise();
 
   // Remove the pending deletion ping generated when flipping FHR upload off.
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
 
   let expectedPrunedPings = [];
   let expectedNotPrunedPings = [];
 
   let checkPendingPings = Task.async(function*() {
     // Check that the pruned pings are not on disk anymore.
     for (let prunedPingId of expectedPrunedPings) {
       yield Assert.rejects(TelemetryStorage.loadPendingPing(prunedPingId),
@@ -450,17 +440,17 @@ add_task(function* test_pendingPingsQuot
   }
 
   // We need to test the pending pings size before we hit the quota, otherwise a special
   // value is recorded.
   Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear();
   Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").clear();
   Telemetry.getHistogramById("TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS").clear();
 
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testPendingQuotaTaskPromise();
 
   // Check that the correct values for quota probes are reported when no quota is hit.
   let h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
   Assert.equal(h.sum, Math.round(pingsSizeInBytes / 1024 / 1024),
                "Telemetry must report the correct pending pings directory size.");
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();
   Assert.equal(h.sum, 0, "Telemetry must report 0 evictions if quota is not hit.");
@@ -486,28 +476,28 @@ add_task(function* test_pendingPingsQuot
     }
     pingsWithinQuota.push(pingInfo.id);
   }
 
   expectedNotPrunedPings = pingsWithinQuota;
   expectedPrunedPings = pingsOutsideQuota;
 
   // Reset TelemetryController to start the pending pings cleanup.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testPendingQuotaTaskPromise();
   yield checkPendingPings();
 
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA").snapshot();
   Assert.equal(h.sum, pingsOutsideQuota.length,
                "Telemetry must correctly report the over quota pings evicted from the pending pings directory.");
   h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
   Assert.equal(h.sum, 17, "Pending pings quota was hit, a special size must be reported.");
 
   // Trigger a cleanup again and make sure we're not removing anything.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testPendingQuotaTaskPromise();
   yield checkPendingPings();
 
   const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
   // Create a pending oversized ping.
   const OVERSIZED_PING = {
     id: OVERSIZED_PING_ID,
     type: PING_TYPE,
@@ -533,17 +523,17 @@ add_task(function* test_pendingPingsQuot
   h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();
   Assert.equal(h.counts[2], 1, "Telemetry must report a 2MB, oversized, ping.");
 
   // Save the ping again to check if it gets pruned when scanning the pings directory.
   yield TelemetryStorage.savePendingPing(OVERSIZED_PING);
   expectedPrunedPings.push(OVERSIZED_PING_ID);
 
   // Scan the pending pings directory.
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
   yield TelemetryStorage.testPendingQuotaTaskPromise();
   yield checkPendingPings();
 
   // Make sure we're correctly updating the related histograms.
   h = Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").snapshot();
   Assert.equal(h.sum, 2, "Telemetry must report 1 oversized ping in the pending pings directory.");
   h = Telemetry.getHistogramById("TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB").snapshot();
   Assert.equal(h.counts[2], 2, "Telemetry must report two 2MB, oversized, pings.");
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -93,23 +93,16 @@ function sendPing() {
     TelemetrySend.setServer("http://localhost:" + PingServer.port);
     return TelemetrySession.testPing();
   } else {
     TelemetrySend.setServer("http://doesnotexist");
     return TelemetrySession.testPing();
   }
 }
 
-var clearPendingPings = Task.async(function*() {
-  const pending = yield TelemetryStorage.loadPendingPingList();
-  for (let p of pending) {
-    yield TelemetryStorage.removePendingPing(p.id);
-  }
-});
-
 function fakeGenerateUUID(sessionFunc, subsessionFunc) {
   let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
   session.Policy.generateSessionUUID = sessionFunc;
   session.Policy.generateSubsessionUUID = subsessionFunc;
 }
 
 function fakeIdleNotification(topic) {
   let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
@@ -450,18 +443,17 @@ function run_test() {
       thread.shutdown();
     });
   });
 
   Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(run_next_test));
 }
 
 add_task(function* asyncSetup() {
-  yield TelemetrySession.setup();
-  yield TelemetryController.setup();
+  yield TelemetryController.testSetup();
   // Load the client ID from the client ID provider to check for pings sanity.
   gClientID = yield ClientID.getClientID();
 });
 
 // Ensures that expired histograms are not part of the payload.
 add_task(function* test_expiredHistogram() {
   let histogram_id = "FOOBAR";
   let dummy = Telemetry.newHistogram(histogram_id, "30", Telemetry.HISTOGRAM_EXPONENTIAL, 1, 2, 3);
@@ -474,34 +466,36 @@ add_task(function* test_expiredHistogram
 
 // Sends a ping to a non existing server. If we remove this test, we won't get
 // all the histograms we need in the main ping.
 add_task(function* test_noServerPing() {
   yield sendPing();
   // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
   // are initialised. See bug 1131585.
   yield sendPing();
+  // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
+  // problems to the consequent tests.
+  yield TelemetryController.testShutdown();
 });
 
 // Checks that a sent ping is correctly received by a dummy http server.
 add_task(function* test_simplePing() {
-  yield clearPendingPings();
-  yield TelemetrySend.reset();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.start();
   Preferences.set(PREF_SERVER, "http://localhost:" + PingServer.port);
 
   let now = new Date(2020, 1, 1, 12, 0, 0);
   let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
   fakeNow(now);
   const monotonicStart = fakeMonotonicNow(5000);
 
   const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
   const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
   fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // Session and subsession start dates are faked during TelemetrySession setup. We can
   // now fake the session duration.
   const SESSION_DURATION_IN_MINUTES = 15;
   fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
   fakeMonotonicNow(monotonicStart + SESSION_DURATION_IN_MINUTES * 60 * 1000);
 
   yield sendPing();
@@ -523,18 +517,18 @@ add_task(function* test_simplePing() {
   fakeGenerateUUID(generateUUID, generateUUID);
 });
 
 // Saves the current session histograms, reloads them, performs a ping
 // and checks that the dummy http server received both the previously
 // saved ping and the new one.
 add_task(function* test_saveLoadPing() {
   // Let's start out with a defined state.
-  yield clearPendingPings();
-  yield TelemetryController.reset();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
   PingServer.clearRequests();
 
   // Setup test data and trigger pings.
   setupTestData();
   yield TelemetrySession.testSavePendingPing();
   yield sendPing();
 
   // Get requests received by dummy server.
@@ -564,17 +558,17 @@ add_task(function* test_checkSubsessionH
   if (gIsAndroid) {
     // We don't support subsessions yet on Android.
     return;
   }
 
   let now = new Date(2020, 1, 1, 12, 0, 0);
   let expectedDate = new Date(2020, 1, 1, 0, 0, 0);
   fakeNow(now);
-  yield TelemetrySession.setup();
+  yield TelemetryController.testReset();
 
   const COUNT_ID = "TELEMETRY_TEST_COUNT";
   const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
   const count = Telemetry.getHistogramById(COUNT_ID);
   const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
   const registeredIds =
     new Set(Telemetry.registeredHistograms(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, []));
 
@@ -755,17 +749,17 @@ add_task(function* test_checkSubsessionD
   let activeTicksAtSubsessionStart = sessionRecorder.activeTicks;
   let expectedActiveTicks = activeTicksAtSubsessionStart;
 
   incrementActiveTicks = () => {
     sessionRecorder.incrementActiveTicks();
     ++expectedActiveTicks;
   }
 
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // Both classic and subsession payload data should be the same on the first subsession.
   incrementActiveTicks();
   let classic = TelemetrySession.getPayload();
   let subsession = TelemetrySession.getPayload("environment-change");
   Assert.equal(classic.simpleMeasurements.activeTicks, expectedActiveTicks,
                "Classic pings must count active ticks since the beginning of the session.");
   Assert.equal(subsession.simpleMeasurements.activeTicks, expectedActiveTicks,
@@ -805,18 +799,18 @@ add_task(function* test_dailyCollection(
   PingServer.clearRequests();
 
   fakeNow(now);
 
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
 
   // Init and check timer.
-  yield clearPendingPings();
-  yield TelemetrySession.setup();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testSetup();
   TelemetrySend.setServer("http://localhost:" + PingServer.port);
 
   // Set histograms to expected state.
   const COUNT_ID = "TELEMETRY_TEST_COUNT";
   const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
   const count = Telemetry.getHistogramById(COUNT_ID);
   const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
 
@@ -887,35 +881,35 @@ add_task(function* test_dailyCollection(
   subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
   Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
 
   Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
   Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["a"].sum, 1);
   Assert.equal(ping.payload.keyedHistograms[KEYED_ID]["b"].sum, 1);
 
   // Shutdown to cleanup the aborted-session if it gets created.
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_dailyDuplication() {
   if (gIsAndroid) {
     // We don't do daily collections yet on Android.
     return;
   }
 
   yield TelemetrySend.reset();
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
 
   let schedulerTickCallback = null;
   let now = new Date(2030, 1, 1, 0, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.setup();
+  yield TelemetryController.testReset();
 
   // Make sure the daily ping gets triggered at midnight.
   // We need to make sure that we trigger this after the period where we wait for
   // the user to become idle.
   let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
   fakeNow(firstDailyDue);
 
   // Run a scheduler tick: it should trigger the daily ping.
@@ -941,31 +935,32 @@ add_task(function* test_dailyDuplication
   fakeNow(secondDailyDue);
 
   // Run a scheduler tick: it should NOT trigger the daily ping.
   Assert.ok(!!schedulerTickCallback);
   yield schedulerTickCallback();
 
   // Shutdown to cleanup the aborted-session if it gets created.
   PingServer.resetPingHandler();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_dailyOverdue() {
   if (gIsAndroid) {
     // We don't do daily collections yet on Android.
     return;
   }
 
   let schedulerTickCallback = null;
   let now = new Date(2030, 1, 1, 11, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.setup();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
 
   // Skip one hour ahead: nothing should be due.
   now.setHours(now.getHours() + 1);
   fakeNow(now);
 
   // Assert if we receive something!
   PingServer.registerPingHandler((req, res) => {
     Assert.ok(false, "No daily ping should be received if not overdue!.");
@@ -991,46 +986,45 @@ add_task(function* test_dailyOverdue() {
   // Get the first daily ping.
   let ping = yield PingServer.promiseNextPing();
   Assert.ok(!!ping);
 
   Assert.equal(ping.type, PING_TYPE_MAIN);
   Assert.equal(ping.payload.info.reason, REASON_DAILY);
 
   // Shutdown to cleanup the aborted-session if it gets created.
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_environmentChange() {
   if (gIsAndroid) {
     // We don't split subsessions on environment changes yet on Android.
     return;
   }
 
   let now = new Date(2040, 1, 1, 12, 0, 0);
   let timerCallback = null;
   let timerDelay = null;
 
-  yield clearPendingPings();
-  yield TelemetrySend.reset();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
 
   fakeNow(now);
 
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
 
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
   ]);
 
   // Setup.
-  yield TelemetrySession.setup();
+  yield TelemetryController.testReset();
   TelemetrySend.setServer("http://localhost:" + PingServer.port);
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
 
   // Set histograms to expected state.
   const COUNT_ID = "TELEMETRY_TEST_COUNT";
   const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
   const count = Telemetry.getHistogramById(COUNT_ID);
   const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
 
   count.clear();
@@ -1076,26 +1070,24 @@ add_task(function* test_environmentChang
   Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
 });
 
 add_task(function* test_savedPingsOnShutdown() {
   // On desktop, we expect both "saved-session" and "shutdown" pings. We only expect
   // the former on Android.
   const expectedPingCount = (gIsAndroid) ? 1 : 2;
   // Assure that we store the ping properly when saving sessions on shutdown.
-  // We make the TelemetrySession shutdown to trigger a session save.
+  // We make the TelemetryController shutdown to trigger a session save.
   const dir = TelemetryStorage.pingDirectoryPath;
   yield OS.File.removeDir(dir, {ignoreAbsent: true});
   yield OS.File.makeDir(dir);
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 
   PingServer.clearRequests();
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   const pings = yield PingServer.promiseNextPings(expectedPingCount);
 
   for (let ping of pings) {
     Assert.ok("type" in ping);
 
     let expectedReason =
       (ping.type == PING_TYPE_SAVED_SESSION) ? REASON_SAVED_SESSION : REASON_SHUTDOWN;
@@ -1137,34 +1129,34 @@ add_task(function* test_savedSessionData
   fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
 
   if (gIsAndroid) {
     // We don't support subsessions yet on Android, so skip the next checks.
     return;
   }
 
   // Start TelemetrySession so that it loads the session data file.
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
   // Watch a test preference, trigger and environment change and wait for it to propagate.
   // _watchPreferences triggers a subsession notification
   fakeNow(new Date(2050, 1, 1, 12, 0, 0));
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
   let changePromise = new Promise(resolve =>
     TelemetryEnvironment.registerChangeListener("test_fake_change", resolve));
   Preferences.set(PREF_TEST, 1);
   yield changePromise;
   TelemetryEnvironment.unregisterChangeListener("test_fake_change");
 
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 
   // Restore the UUID generator so we don't mess with other tests.
   fakeGenerateUUID(generateUUID, generateUUID);
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(data.sessionId, expectedSessionUUID);
@@ -1175,41 +1167,41 @@ add_task(function* test_sessionData_Shor
   if (gIsAndroid) {
     // We don't support subsessions yet on Android, so skip the next checks.
     return;
   }
 
   const SESSION_STATE_PATH = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
 
   // Shut down Telemetry and remove the session state file.
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testReset();
   yield OS.File.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
   getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
   getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
   getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
 
   const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
   const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
   fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
 
   // We intentionally don't wait for the setup to complete and shut down to simulate
   // short sessions. We expect the profile subsession counter to be 1.
-  TelemetrySession.reset();
-  yield TelemetrySession.shutdown();
+  TelemetryController.testReset();
+  yield TelemetryController.testShutdown();
 
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
   // Restore the UUID generation functions.
   fakeGenerateUUID(generateUUID, generateUUID);
 
-  // Start TelemetrySession so that it loads the session data file. We expect the profile
+  // Start TelemetryController so that it loads the session data file. We expect the profile
   // subsession counter to be incremented by 1 again.
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // We expect 2 profile subsession counter updates.
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, 2);
   Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
   Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
@@ -1225,18 +1217,18 @@ add_task(function* test_invalidSessionDa
   getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
 
   // Write test data to the session data file. This should fail to parse.
   const dataFilePath = OS.Path.join(DATAREPORTING_PATH, "session-state.json");
   const unparseableData = "{asdf:@äü";
   OS.File.writeAtomic(dataFilePath, unparseableData,
                       {encoding: "utf-8", tmpPath: dataFilePath + ".tmp"});
 
-  // Start TelemetrySession so that it loads the session data file.
-  yield TelemetrySession.reset();
+  // Start TelemetryController so that it loads the session data file.
+  yield TelemetryController.testReset();
 
   // The session data file should not load. Only expect the current subsession.
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
   // Write test data to the session data file. This should fail validation.
   const sessionState = {
@@ -1246,26 +1238,26 @@ add_task(function* test_invalidSessionDa
   yield CommonUtils.writeJSON(sessionState, dataFilePath);
 
   // The session data file should not load. Only expect the current subsession.
   const expectedSubsessions = 1;
   const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
   const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
   fakeGenerateUUID(() => expectedSessionUUID, () => expectedSubsessionUUID);
 
-  // Start TelemetrySession so that it loads the session data file.
-  yield TelemetrySession.reset();
+  // Start TelemetryController so that it loads the session data file.
+  yield TelemetryController.testReset();
 
   let payload = TelemetrySession.getPayload();
   Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
   Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
 
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 
   // Restore the UUID generator so we don't mess with other tests.
   fakeGenerateUUID(generateUUID, generateUUID);
 
   // Load back the serialised session data.
   let data = yield CommonUtils.readJSON(dataFilePath);
   Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
   Assert.equal(data.sessionId, expectedSessionUUID);
@@ -1283,17 +1275,17 @@ add_task(function* test_abortedSession()
   // Make sure the aborted sessions directory does not exist to test its creation.
   yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
 
   let schedulerTickCallback = null;
   let now = new Date(2040, 1, 1, 0, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control aborted-session flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
             "Telemetry must create the aborted session directory when starting.");
 
   // Fake now again so that the scheduled aborted-session save takes place.
   now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
   fakeNow(now);
   // The first aborted session checkpoint must take place right after the initialisation.
@@ -1320,62 +1312,57 @@ add_task(function* test_abortedSession()
 
   pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
   let updatedAbortedSessionPing = JSON.parse(pingContent);
   checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
   Assert.equal(updatedAbortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
   Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
   Assert.notEqual(abortedSessionPing.creationDate, updatedAbortedSessionPing.creationDate);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
   Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
             "No aborted session ping must be available after a shutdown.");
 
   // Write the ping to the aborted-session file. TelemetrySession will add it to the
   // saved pings directory when it starts.
   yield TelemetryStorage.savePingToFile(abortedSessionPing, ABORTED_FILE, false);
+  Assert.ok((yield OS.File.exists(ABORTED_FILE)),
+            "The aborted session ping must exist in the aborted session ping directory.");
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
-  // TODO: Remove the TelemetrySend manual setup when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySession.reset();
-  yield TelemetryController.reset();
+  yield TelemetryController.testReset();
 
   Assert.ok(!(yield OS.File.exists(ABORTED_FILE)),
             "The aborted session ping must be removed from the aborted session ping directory.");
 
+  // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+  yield TelemetryController.testReset();
+
   // We should have received an aborted-session ping.
   const receivedPing = yield PingServer.promiseNextPing();
   Assert.equal(receivedPing.type, PING_TYPE_MAIN, "Should have the correct type");
   Assert.equal(receivedPing.payload.info.reason, REASON_ABORTED_SESSION, "Ping should have the correct reason");
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_abortedSession_Shutdown() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session ping here.
     return;
   }
 
   const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
 
   let schedulerTickCallback = null;
   let now = fakeNow(2040, 1, 1, 0, 0, 0);
   // Fake scheduler functions to control aborted-session flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  // TODO: Remove the TelemetrySend manual setup/reset when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
             "Telemetry must create the aborted session directory when starting.");
 
   // Fake now again so that the scheduled aborted-session save takes place.
   now = fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
   // The first aborted session checkpoint must take place right after the initialisation.
   Assert.ok(!!schedulerTickCallback);
@@ -1383,19 +1370,17 @@ add_task(function* test_abortedSession_S
   yield schedulerTickCallback();
   // Check that the aborted session is due at the correct time.
   Assert.ok((yield OS.File.exists(ABORTED_FILE)), "There must be an aborted session ping.");
 
   // Remove the aborted session file and then shut down to make sure exceptions (e.g file
   // not found) do not compromise the shutdown.
   yield OS.File.remove(ABORTED_FILE);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_abortedDailyCoalescing() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
@@ -1407,20 +1392,19 @@ add_task(function* test_abortedDailyCoal
   let schedulerTickCallback = null;
   PingServer.clearRequests();
 
   let nowDate = new Date(2009, 10, 18, 0, 0, 0);
   fakeNow(nowDate);
 
   // Fake scheduler functions to control aborted-session flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  // TODO: Remove the TelemetrySend manual setup/reset when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
-  yield TelemetrySession.reset();
+  yield TelemetryStorage.testClearPendingPings();
+  PingServer.clearRequests();
+  yield TelemetryController.testReset();
 
   Assert.ok((yield OS.File.exists(DATAREPORTING_PATH)),
             "Telemetry must create the aborted session directory when starting.");
 
   // Delay the callback around midnight so that the aborted-session ping gets merged with the
   // daily ping.
   let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
   fakeNow(dailyDueDate);
@@ -1439,43 +1423,39 @@ add_task(function* test_abortedDailyCoal
 
   // Read aborted session ping and check that the session/subsession ids equal the
   // ones in the daily ping.
   let pingContent = yield OS.File.read(ABORTED_FILE, { encoding: "utf-8" });
   let abortedSessionPing = JSON.parse(pingContent);
   Assert.equal(abortedSessionPing.payload.info.sessionId, dailyPing.payload.info.sessionId);
   Assert.equal(abortedSessionPing.payload.info.subsessionId, dailyPing.payload.info.subsessionId);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_schedulerComputerSleep() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
   const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
 
-  clearPendingPings();
-  // TODO: Remove the TelemetrySend manual setup when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
   PingServer.clearRequests();
 
   // Remove any aborted-session ping from the previous tests.
   yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
 
   // Set a fake current date and start Telemetry.
   let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
   let schedulerTickCallback = null;
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // Set the current time 3 days in the future at midnight, before running the callback.
   nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
   Assert.ok(!!schedulerTickCallback);
   // Execute one scheduler tick.
   yield schedulerTickCallback();
 
   let dailyPing = yield PingServer.promiseNextPing();
@@ -1500,47 +1480,43 @@ add_task(function* test_schedulerCompute
   Services.obs.notifyObservers(null, "wake_notification", null);
 
   dailyPing = yield PingServer.promiseNextPing();
   Assert.equal(dailyPing.payload.info.reason, REASON_DAILY,
                "The wake notification should have triggered a daily ping.");
   Assert.equal(dailyPing.creationDate, nowDate.toISOString(),
                "The daily ping date should be correct.");
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_schedulerEnvironmentReschedules() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
   // Reset the test preference.
   const PREF_TEST = "toolkit.telemetry.test.pref1";
   Preferences.reset(PREF_TEST);
   const PREFS_TO_WATCH = new Map([
     [PREF_TEST, {what: TelemetryEnvironment.RECORD_PREF_VALUE}],
   ]);
 
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
-  // TODO: Remove the TelemetrySend manual setup/reset when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
+  yield TelemetryController.testReset();
 
   // Set a fake current date and start Telemetry.
   let nowDate = new Date(2060, 10, 18, 0, 0, 0);
   fakeNow(nowDate);
   let schedulerTickCallback = null;
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.reset();
-  TelemetryEnvironment._watchPreferences(PREFS_TO_WATCH);
+  yield TelemetryController.testReset();
+  TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
 
   // Set the current time at midnight.
   let future = futureDate(nowDate, MS_IN_ONE_DAY);
   fakeNow(future);
 
   // Trigger the environment change.
   Preferences.set(PREF_TEST, 1);
 
@@ -1550,81 +1526,71 @@ add_task(function* test_schedulerEnviron
   // We don't expect to receive any daily ping in this test, so assert if we do.
   PingServer.registerPingHandler((req, res) => {
     Assert.ok(false, "No ping should be sent/received in this test.");
   });
 
   // Execute one scheduler tick. It should not trigger a daily ping.
   Assert.ok(!!schedulerTickCallback);
   yield schedulerTickCallback();
-
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_schedulerNothingDue() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
   const ABORTED_FILE = OS.Path.join(DATAREPORTING_PATH, ABORTED_PING_FILE_NAME);
 
   // Remove any aborted-session ping from the previous tests.
   yield OS.File.removeDir(DATAREPORTING_PATH, { ignoreAbsent: true });
-  yield clearPendingPings();
-  // TODO: Remove the TelemetrySend manual setup/reset when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
 
   // We don't expect to receive any ping in this test, so assert if we do.
   PingServer.registerPingHandler((req, res) => {
     Assert.ok(false, "No ping should be sent/received in this test.");
   });
 
   // Set a current date/time away from midnight, so that the daily ping doesn't get
   // sent.
   let nowDate = new Date(2009, 10, 18, 11, 0, 0);
   fakeNow(nowDate);
   let schedulerTickCallback = null;
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.reset();
+  yield TelemetryController.testReset();
 
   // Delay the callback execution to a time when no ping should be due.
   let nothingDueDate = futureDate(nowDate, ABORTED_SESSION_UPDATE_INTERVAL_MS / 2);
   fakeNow(nothingDueDate);
   Assert.ok(!!schedulerTickCallback);
   // Execute one scheduler tick.
   yield schedulerTickCallback();
 
   // Check that no aborted session ping was written to disk.
   Assert.ok(!(yield OS.File.exists(ABORTED_FILE)));
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
   PingServer.resetPingHandler();
 });
 
 add_task(function* test_pingExtendedStats() {
   const EXTENDED_PAYLOAD_FIELDS = [
     "chromeHangs", "threadHangStats", "log", "slowSQL", "fileIOReports", "lateWrites",
     "addonHistograms", "addonDetails", "UIMeasurements", "webrtc"
   ];
 
-  // Disable sending extended statistics.
+  // Reset telemetry and disable sending extended statistics.
+  yield TelemetryStorage.testClearPendingPings();
+  PingServer.clearRequests();
+  yield TelemetryController.testReset();
   Telemetry.canRecordExtended = false;
 
-  yield clearPendingPings();
-  PingServer.clearRequests();
-  // TODO: Remove the TelemetrySend manual setup/reset when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
-  yield TelemetrySession.reset();
   yield sendPing();
 
   let ping = yield PingServer.promiseNextPing();
   checkPingFormat(ping, PING_TYPE_MAIN, true, true);
 
   // Check that the payload does not contain extended statistics fields.
   for (let f in EXTENDED_PAYLOAD_FIELDS) {
     Assert.ok(!(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
@@ -1640,17 +1606,16 @@ add_task(function* test_pingExtendedStat
             "addonManager must not be sent if the extended set is off.");
   Assert.ok(!("UITelemetry" in ping.payload.simpleMeasurements),
             "UITelemetry must not be sent if the extended set is off.");
 
   // Restore the preference.
   Telemetry.canRecordExtended = true;
 
   // Send a new ping that should contain the extended data.
-  yield TelemetrySession.reset();
   yield sendPing();
   ping = yield PingServer.promiseNextPing();
   checkPingFormat(ping, PING_TYPE_MAIN, true, true);
 
   // Check that the payload now contains extended statistics fields.
   for (let f in EXTENDED_PAYLOAD_FIELDS) {
     Assert.ok(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
               EXTENDED_PAYLOAD_FIELDS[f] + " must be in the payload if the extended set is on.");
@@ -1673,18 +1638,18 @@ add_task(function* test_schedulerUserIdl
 
   let now = new Date(2010, 1, 1, 11, 0, 0);
   fakeNow(now);
 
   let schedulerTimeout = 0;
   fakeSchedulerTimer((callback, timeout) => {
     schedulerTimeout = timeout;
   }, () => {});
-  yield TelemetrySession.reset();
-  yield clearPendingPings();
+  yield TelemetryController.testReset();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
 
   // When not idle, the scheduler should have a 5 minutes tick interval.
   Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
 
   // Send an "idle" notification to the scheduler.
   fakeIdleNotification("idle");
 
@@ -1699,47 +1664,43 @@ add_task(function* test_schedulerUserIdl
 
   // We should not miss midnight when going to idle.
   now.setHours(23);
   now.setMinutes(50);
   fakeNow(now);
   fakeIdleNotification("idle");
   Assert.equal(schedulerTimeout, 10 * 60 * 1000);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_DailyDueAndIdle() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
-  yield TelemetrySession.reset();
-  yield clearPendingPings();
+  yield TelemetryStorage.testClearPendingPings();
   PingServer.clearRequests();
-  // TODO: Remove the TelemetrySend setup when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
 
   let receivedPingRequest = null;
   // Register a ping handler that will assert when receiving multiple daily pings.
   PingServer.registerPingHandler(req => {
     Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
     receivedPingRequest = req;
   });
 
+  // Faking scheduler timer has to happen before resetting TelemetryController
+  // to be effective.
   let schedulerTickCallback = null;
   let now = new Date(2030, 1, 1, 0, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.setup();
+  yield TelemetryController.testReset();
 
   // Trigger the daily ping.
   let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
   fakeNow(firstDailyDue);
 
   // Run a scheduler tick: it should trigger the daily ping.
   Assert.ok(!!schedulerTickCallback);
   let tickPromise = schedulerTickCallback();
@@ -1754,47 +1715,40 @@ add_task(function* test_DailyDueAndIdle(
   yield TelemetrySend.testWaitOnOutgoingPings();
 
   // Decode the ping contained in the request and check that's a daily ping.
   Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
   const receivedPing = decodeRequestPayload(receivedPingRequest);
   checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
   Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* test_userIdleAndSchedlerTick() {
   if (gIsAndroid || gIsGonk) {
     // We don't have the aborted session or the daily ping here.
     return;
   }
 
-  yield TelemetrySession.reset();
-  yield clearPendingPings();
-  PingServer.clearRequests();
-  // TODO: Remove the TelemetrySend setup when bug 1145188 lands.
-  yield TelemetrySend.setup(true);
-  yield TelemetrySend.reset();
-
   let receivedPingRequest = null;
   // Register a ping handler that will assert when receiving multiple daily pings.
   PingServer.registerPingHandler(req => {
     Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
     receivedPingRequest = req;
   });
 
   let schedulerTickCallback = null;
   let now = new Date(2030, 1, 1, 0, 0, 0);
   fakeNow(now);
   // Fake scheduler functions to control daily collection flow in tests.
   fakeSchedulerTimer(callback => schedulerTickCallback = callback, () => {});
-  yield TelemetrySession.setup();
+  yield TelemetryStorage.testClearPendingPings();
+  yield TelemetryController.testReset();
+  PingServer.clearRequests();
 
   // Move the current date/time to midnight.
   let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
   fakeNow(firstDailyDue);
 
   // The active notification should trigger a scheduler tick. The latter will send the
   // due daily ping.
   fakeIdleNotification("active");
@@ -1809,17 +1763,15 @@ add_task(function* test_userIdleAndSched
   yield TelemetrySend.testWaitOnOutgoingPings();
 
   // Decode the ping contained in the request and check that's a daily ping.
   Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
   const receivedPing = decodeRequestPayload(receivedPingRequest);
   checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
   Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
 
-  // TODO: Remove the TelemetrySend manual shutdown when bug 1145188 lands.
-  yield TelemetrySend.shutdown();
-  yield TelemetrySession.shutdown();
+  yield TelemetryController.testShutdown();
 });
 
 add_task(function* stopServer(){
   yield PingServer.stop();
   do_test_finished();
 });
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -27,23 +27,23 @@ function loadAddonManager() {
   startupManager();
 }
 
 function getSimpleMeasurementsFromTelemetryController() {
   return TelemetrySession.getPayload().simpleMeasurements;
 }
 
 function initialiseTelemetry() {
-  return TelemetryController.setup().then(TelemetrySession.setup);
+  return TelemetryController.testSetup();
 }
 
 function run_test() {
   // Telemetry needs the AddonManager.
   loadAddonManager();
-  // Make profile available for |TelemetrySession.shutdown()|.
+  // Make profile available for |TelemetryController.testShutdown()|.
   do_get_profile();
 
   do_test_pending();
   const Telemetry = Services.telemetry;
   Telemetry.asyncFetchTelemetryData(run_next_test);
 }
 
 add_task(function* actualTest() {
@@ -80,12 +80,12 @@ add_task(function* actualTest() {
 
   // Test that the data gets added to the telemetry ping properly
   let simpleMeasurements = getSimpleMeasurementsFromTelemetryController();
   do_check_true(simpleMeasurements != null); // got simple measurements from ping data
   do_check_true(simpleMeasurements.foo > 1); // foo was included
   do_check_true(simpleMeasurements.bar > 1); // bar was included
   do_check_eq(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
 
-  yield TelemetrySession.shutdown(false);
+  yield TelemetryController.testShutdown();
 
   do_test_finished();
 });
--- a/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
+++ b/toolkit/crashreporter/test/unit/test_crashreporter_crash.js
@@ -34,18 +34,18 @@ function run_test()
   // check setting some basic data
   do_crash(function() {
              crashReporter.annotateCrashReport("TestKey", "TestValue");
              crashReporter.annotateCrashReport("\u2665", "\u{1F4A9}");
              crashReporter.appendAppNotesToCrashReport("Junk");
              crashReporter.appendAppNotesToCrashReport("MoreJunk");
              // TelemetrySession setup will trigger the session annotation
              let scope = {};
-             Components.utils.import("resource://gre/modules/TelemetrySession.jsm", scope);
-             scope.TelemetrySession.setup();
+             Components.utils.import("resource://gre/modules/TelemetryController.jsm", scope);
+             scope.TelemetryController.testSetup();
            },
            function(mdump, extra) {
              do_check_eq(extra.TestKey, "TestValue");
              do_check_eq(extra["\u2665"], "\u{1F4A9}");
              do_check_eq(extra.Notes, "JunkMoreJunk");
              do_check_true(!("TelemetrySessionId" in extra));
            });
 }
--- a/toolkit/crashreporter/test/unit/test_event_files.js
+++ b/toolkit/crashreporter/test/unit/test_event_files.js
@@ -23,18 +23,18 @@ add_task(function* test_main_process_cra
   Assert.ok(cm, "CrashManager available.");
 
   let basename;
   let deferred = Promise.defer();
   do_crash(
     function() {
       // TelemetrySession setup will trigger the session annotation
       let scope = {};
-      Components.utils.import("resource://gre/modules/TelemetrySession.jsm", scope);
-      scope.TelemetrySession.setup();
+      Components.utils.import("resource://gre/modules/TelemetryController.jsm", scope);
+      scope.TelemetryController.testSetup();
       crashType = CrashTestUtils.CRASH_RUNTIMEABORT;
       crashReporter.annotateCrashReport("ShutdownProgress", "event-test");
     },
     (minidump, extra) => {
       basename = minidump.leafName;
       cm._eventsDirs = [getEventDir()];
       cm.aggregateEventsFiles().then(deferred.resolve, deferred.reject);
     },
--- a/toolkit/modules/addons/WebNavigation.jsm
+++ b/toolkit/modules/addons/WebNavigation.jsm
@@ -8,31 +8,48 @@ const EXPORTED_SYMBOLS = ["WebNavigation
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
+                                  "resource:///modules/RecentWindow.jsm");
+
+// Maximum amount of time that can be passed and still consider
+// the data recent (similar to how is done in nsNavHistory,
+// e.g. nsNavHistory::CheckIsRecentEvent, but with a lower threshold value).
+const RECENT_DATA_THRESHOLD = 5 * 1000000;
+
 // TODO:
 // onCreatedNavigationTarget
 
 var Manager = {
   listeners: new Map(),
 
   init() {
+    // Collect recent tab transition data in a WeakMap:
+    //   browser -> tabTransitionData
+    this.recentTabTransitionData = new WeakMap();
+    Services.obs.addObserver(this, "autocomplete-did-enter-text", true);
+
     Services.mm.addMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.addMessageListener("Extension:StateChange", this);
     Services.mm.addMessageListener("Extension:DocumentChange", this);
     Services.mm.addMessageListener("Extension:HistoryChange", this);
     Services.mm.loadFrameScript("resource://gre/modules/WebNavigationContent.js", true);
   },
 
   uninit() {
+    // Stop collecting recent tab transition data and reset the WeakMap.
+    Services.obs.removeObserver(this, "autocomplete-did-enter-text", true);
+    this.recentTabTransitionData = new WeakMap();
+
     Services.mm.removeMessageListener("Extension:StateChange", this);
     Services.mm.removeMessageListener("Extension:DocumentChange", this);
     Services.mm.removeMessageListener("Extension:HistoryChange", this);
     Services.mm.removeMessageListener("Extension:DOMContentLoaded", this);
     Services.mm.removeDelayedFrameScript("resource://gre/modules/WebNavigationContent.js");
     Services.mm.broadcastAsyncMessage("Extension:DisableWebNavigation");
   },
 
@@ -58,16 +75,153 @@ var Manager = {
       this.listeners.delete(type);
     }
 
     if (this.listeners.size == 0) {
       this.uninit();
     }
   },
 
+  /**
+   *  Support nsIObserver interface to observe the urlbar autocomplete events used
+   *  to keep track of the urlbar user interaction.
+   */
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+  /**
+   *  Observe autocomplete-did-enter-text topic to track the user interaction with
+   *  the awesome bar.
+   */
+  observe: function(subject, topic, data) {
+    if (topic == "autocomplete-did-enter-text") {
+      this.onURLBarAutoCompletion(subject, topic, data);
+    }
+  },
+
+  /**
+   *  Recognize the type of urlbar user interaction (e.g. typing a new url,
+   *  clicking on an url generated from a searchengine or a keyword, or a
+   *  bookmark found by the urlbar autocompletion).
+   */
+  onURLBarAutoCompletion(subject, topic, data) {
+    if (subject && subject instanceof Ci.nsIAutoCompleteInput) {
+      // We are only interested in urlbar autocompletion events
+      if (subject.id !== "urlbar") {
+        return;
+      }
+
+      let controller = subject.popup.view.QueryInterface(Ci.nsIAutoCompleteController);
+      let idx = subject.popup.selectedIndex;
+
+      let tabTransistionData = {
+        from_address_bar: true,
+      };
+
+      if (idx < 0 || idx >= controller.matchCount) {
+        // Recognize when no valid autocomplete results has been selected.
+        tabTransistionData.typed = true;
+      } else {
+        let value = controller.getValueAt(idx);
+        let action = subject._parseActionUrl(value);
+
+        if (action) {
+          // Detect keywork and generated and more typed scenarios.
+          switch (action.type) {
+            case "keyword":
+              tabTransistionData.keyword = true;
+              break;
+            case "searchengine":
+            case "searchsuggestion":
+              tabTransistionData.generated = true;
+              break;
+            case "visiturl":
+              // Visiturl are autocompletion results related to
+              // history suggestions.
+              tabTransistionData.typed = true;
+              break;
+            case "remotetab":
+              // Remote tab are autocomplete results related to
+              // tab urls from a remote synchronized Firefox.
+              tabTransistionData.typed = true;
+              break;
+            case "switchtab":
+              // This "switchtab" autocompletion should be ignored, because
+              // it is not related to a navigation.
+              return;
+            default:
+              // Fallback on "typed" if unable to detect a known moz-action type.
+              tabTransistionData.typed = true;
+          }
+        } else {
+          // Special handling for bookmark urlbar autocompletion
+          // (which happens when we got a null action and a valid selectedIndex)
+          let styles = new Set(controller.getStyleAt(idx).split(/\s+/));
+
+          if (styles.has("bookmark")) {
+            tabTransistionData.auto_bookmark = true;
+          } else {
+            // Fallback on "typed" if unable to detect a specific actionType
+            // (and when in the styles there are "autofill" or "history").
+            tabTransistionData.typed = true;
+          }
+        }
+      }
+
+      this.setRecentTabTransitionData(tabTransistionData);
+    }
+  },
+
+  /**
+   *  Keep track of a recent user interaction and cache it in a
+   *  map associated to the current selected tab.
+   */
+  setRecentTabTransitionData(tabTransitionData) {
+    let window = RecentWindow.getMostRecentBrowserWindow();
+    if (window && window.gBrowser && window.gBrowser.selectedTab &&
+        window.gBrowser.selectedTab.linkedBrowser) {
+      let browser = window.gBrowser.selectedTab.linkedBrowser;
+
+      // Get recent tab transition data to update if any.
+      let prevData = this.getAndForgetRecentTabTransitionData(browser);
+
+      let newData = Object.assign(
+        {time: Date.now()},
+        prevData,
+        tabTransitionData
+      );
+      this.recentTabTransitionData.set(browser, newData);
+    }
+  },
+
+  /**
+   *  Retrieve recent data related to a recent user interaction give a
+   *  given tab's linkedBrowser (only if is is more recent than the
+   *  `RECENT_DATA_THRESHOLD`).
+   *
+   *  NOTE: this method is used to retrieve the tab transition data
+   *  collected when one of the `onCommitted`, `onHistoryStateUpdated`
+   *  or `onReferenceFragmentUpdated` events has been received.
+   */
+  getAndForgetRecentTabTransitionData(browser) {
+    let data = this.recentTabTransitionData.get(browser);
+    this.recentTabTransitionData.delete(browser);
+
+    // Return an empty object if there isn't any tab transition data
+    // or if it's less recent than RECENT_DATA_THRESHOLD.
+    if (!data || (data.time - Date.now()) > RECENT_DATA_THRESHOLD) {
+      return {};
+    }
+
+    return data;
+  },
+
+  /**
+   *  Receive messages from the WebNavigationContent.js framescript
+   *  over message manager events.
+   */
   receiveMessage({name, data, target}) {
     switch (name) {
       case "Extension:StateChange":
         this.onStateChange(target, data);
         break;
 
       case "Extension:DocumentChange":
         this.onDocumentChange(target, data);
@@ -100,26 +254,28 @@ var Manager = {
     }
   },
 
   onDocumentChange(browser, data) {
     let extra = {
       url: data.location,
       // Transition data which is coming from the content process.
       frameTransitionData: data.frameTransitionData,
+      tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
     };
 
     this.fire("onCommitted", browser, data, extra);
   },
 
   onHistoryChange(browser, data) {
     let extra = {
       url: data.location,
       // Transition data which is coming from the content process.
       frameTransitionData: data.frameTransitionData,
+      tabTransitionData: this.getAndForgetRecentTabTransitionData(browser),
     };
 
     if (data.isReferenceFragmentUpdated) {
       this.fire("onReferenceFragmentUpdated", browser, data, extra);
     } else if (data.isHistoryStateUpdated) {
       this.fire("onHistoryStateUpdated", browser, data, extra);
     }
   },
--- a/toolkit/themes/shared/aboutReader.css
+++ b/toolkit/themes/shared/aboutReader.css
@@ -153,8 +153,22 @@ body.light blockquote {
 
 body.sepia blockquote {
   border-inline-start: 2px solid #5b4636 !important;
 }
 
 body.dark blockquote {
   border-inline-start: 2px solid #eeeeee !important;
 }
+
+/* Add toolbar transition base on loaded class  */
+
+body.loaded .toolbar {
+  transition: transform 0.3s ease-out;
+}
+
+body:not(.loaded) .toolbar:-moz-locale-dir(ltr) {
+  transform: translateX(-100%);
+}
+
+body:not(.loaded) .toolbar:-moz-locale-dir(rtl) {
+  transform: translateX(100%);
+}