Merge m-c to graphics, a=merge
authorKartikaya Gupta <kgupta@mozilla.com>
Thu, 01 Jun 2017 09:56:23 -0400
changeset 361815 db1f0de460e574f9fc9a0fd0fdf5e880df109896
parent 361814 67f689dd202dca5fe70622a131b087408f7119f2 (current diff)
parent 361760 0bcea6bac1797e14b00af45cc7c368d12460ab7f (diff)
child 361816 0e9853e31da9848ab638bdd0df4eb734a5ebc232
push id31944
push userryanvm@gmail.com
push dateThu, 01 Jun 2017 16:44:22 +0000
treeherdermozilla-central@0e9853e31da9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to graphics, a=merge MozReview-Commit-ID: COZj8Itgjcz
browser/base/content/usercontext.svg
browser/components/uitour/test/browser_UITour_heartbeat.js
browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
browser/extensions/activity-stream/lib/SearchFeed.jsm
browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
browser/themes/shared/heartbeat-icon.svg
browser/themes/shared/heartbeat-star-lit.svg
browser/themes/shared/heartbeat-star-off.svg
browser/themes/shared/tabbrowser/tab-audio.svg
dom/browser-element/mochitest/browserElement_SetVisible.js
dom/browser-element/mochitest/browserElement_SetVisibleFrames.js
dom/browser-element/mochitest/browserElement_SetVisibleFrames2.js
dom/browser-element/mochitest/browserElement_VisibilityChange.js
dom/browser-element/mochitest/file_browserElement_SetVisibleFrames2_Outer.html
dom/browser-element/mochitest/file_browserElement_SetVisibleFrames_Outer.html
dom/browser-element/mochitest/priority/chrome.ini
dom/browser-element/mochitest/priority/file_MultipleFrames.html
dom/browser-element/mochitest/priority/file_NestedFramesOuter.html
dom/browser-element/mochitest/priority/file_WebGLContextLost.html
dom/browser-element/mochitest/priority/mochitest.ini
dom/browser-element/mochitest/priority/silence.ogg
dom/browser-element/mochitest/priority/test_Background.html
dom/browser-element/mochitest/priority/test_MultipleFrames.html
dom/browser-element/mochitest/priority/test_NestedFrames.html
dom/browser-element/mochitest/priority/test_Simple.html
dom/browser-element/mochitest/priority/test_Visibility.html
dom/browser-element/mochitest/priority/test_WebGLContextLost.html
dom/browser-element/mochitest/test_browserElement_inproc_SetVisible.html
dom/browser-element/mochitest/test_browserElement_inproc_SetVisibleFrames.html
dom/browser-element/mochitest/test_browserElement_inproc_SetVisibleFrames2.html
dom/browser-element/mochitest/test_browserElement_inproc_VisibilityChange.html
dom/browser-element/mochitest/test_browserElement_oop_SetVisible.html
dom/browser-element/mochitest/test_browserElement_oop_SetVisibleFrames.html
dom/browser-element/mochitest/test_browserElement_oop_SetVisibleFrames2.html
dom/browser-element/mochitest/test_browserElement_oop_VisibilityChange.html
layout/base/FrameProperties.cpp
layout/painting/nsDisplayList.cpp
testing/web-platform/harness/.gitignore
testing/web-platform/harness/.travis.yml
testing/web-platform/harness/MANIFEST.in
testing/web-platform/harness/README.rst
testing/web-platform/harness/docs/Makefile
testing/web-platform/harness/docs/architecture.svg
testing/web-platform/harness/docs/conf.py
testing/web-platform/harness/docs/design.rst
testing/web-platform/harness/docs/expectation.rst
testing/web-platform/harness/docs/index.rst
testing/web-platform/harness/docs/make.bat
testing/web-platform/harness/docs/usage.rst
testing/web-platform/harness/requirements.txt
testing/web-platform/harness/requirements_chrome.txt
testing/web-platform/harness/requirements_firefox.txt
testing/web-platform/harness/requirements_servo.txt
testing/web-platform/harness/setup.py
testing/web-platform/harness/test/metadata/reftest/reftest_and_fail.html.ini
testing/web-platform/harness/test/metadata/reftest/reftest_cycle_fail.html.ini
testing/web-platform/harness/test/metadata/reftest/reftest_match_fail.html.ini
testing/web-platform/harness/test/metadata/reftest/reftest_mismatch_fail.html.ini
testing/web-platform/harness/test/metadata/reftest/reftest_ref_timeout.html.ini
testing/web-platform/harness/test/metadata/reftest/reftest_timeout.html.ini
testing/web-platform/harness/test/metadata/testharness/firefox/__dir__.ini
testing/web-platform/harness/test/metadata/testharness/firefox/subdir/test_pref_reset.html.ini
testing/web-platform/harness/test/metadata/testharness/firefox/test_pref_set.html.ini
testing/web-platform/harness/test/metadata/testharness/subdir/__dir__.ini
testing/web-platform/harness/test/metadata/testharness/subdir/testharness_1.html.ini
testing/web-platform/harness/test/metadata/testharness/testharness_0.html.ini
testing/web-platform/harness/test/metadata/testharness/testharness_error.html.ini
testing/web-platform/harness/test/metadata/testharness/testharness_timeout.html.ini
testing/web-platform/harness/test/test.cfg.example
testing/web-platform/harness/test/test.py
testing/web-platform/harness/test/testdata/reftest/green-ref.html
testing/web-platform/harness/test/testdata/reftest/green.html
testing/web-platform/harness/test/testdata/reftest/red.html
testing/web-platform/harness/test/testdata/reftest/reftest.https.html
testing/web-platform/harness/test/testdata/reftest/reftest_and_fail.html
testing/web-platform/harness/test/testdata/reftest/reftest_and_fail_0-ref.html
testing/web-platform/harness/test/testdata/reftest/reftest_cycle.html
testing/web-platform/harness/test/testdata/reftest/reftest_cycle_0-ref.html
testing/web-platform/harness/test/testdata/reftest/reftest_cycle_1-ref.html
testing/web-platform/harness/test/testdata/reftest/reftest_cycle_fail.html
testing/web-platform/harness/test/testdata/reftest/reftest_cycle_fail_0-ref.html
testing/web-platform/harness/test/testdata/reftest/reftest_match.html
testing/web-platform/harness/test/testdata/reftest/reftest_match_fail.html
testing/web-platform/harness/test/testdata/reftest/reftest_mismatch.html
testing/web-platform/harness/test/testdata/reftest/reftest_mismatch_fail.html
testing/web-platform/harness/test/testdata/reftest/reftest_or_0.html
testing/web-platform/harness/test/testdata/reftest/reftest_ref_timeout-ref.html
testing/web-platform/harness/test/testdata/reftest/reftest_ref_timeout.html
testing/web-platform/harness/test/testdata/reftest/reftest_timeout.html
testing/web-platform/harness/test/testdata/reftest/reftest_wait_0.html
testing/web-platform/harness/test/testdata/testharness/firefox/subdir/test_pref_inherit.html
testing/web-platform/harness/test/testdata/testharness/firefox/subdir/test_pref_reset.html
testing/web-platform/harness/test/testdata/testharness/firefox/test_pref_dir.html
testing/web-platform/harness/test/testdata/testharness/firefox/test_pref_set.html
testing/web-platform/harness/test/testdata/testharness/subdir/testharness_1.html
testing/web-platform/harness/test/testdata/testharness/testharness.https.html
testing/web-platform/harness/test/testdata/testharness/testharness_0.html
testing/web-platform/harness/test/testdata/testharness/testharness_error.html
testing/web-platform/harness/test/testdata/testharness/testharness_long_timeout.html
testing/web-platform/harness/test/testdata/testharness/testharness_timeout.html
testing/web-platform/harness/tox.ini
testing/web-platform/harness/wptrunner.default.ini
testing/web-platform/harness/wptrunner/__init__.py
testing/web-platform/harness/wptrunner/browsers/__init__.py
testing/web-platform/harness/wptrunner/browsers/b2g_setup/certtest_app.zip
testing/web-platform/harness/wptrunner/browsers/base.py
testing/web-platform/harness/wptrunner/browsers/chrome.py
testing/web-platform/harness/wptrunner/browsers/edge.py
testing/web-platform/harness/wptrunner/browsers/firefox.py
testing/web-platform/harness/wptrunner/browsers/server-locations.txt
testing/web-platform/harness/wptrunner/browsers/servo.py
testing/web-platform/harness/wptrunner/browsers/servodriver.py
testing/web-platform/harness/wptrunner/config.json
testing/web-platform/harness/wptrunner/config.py
testing/web-platform/harness/wptrunner/environment.py
testing/web-platform/harness/wptrunner/executors/__init__.py
testing/web-platform/harness/wptrunner/executors/base.py
testing/web-platform/harness/wptrunner/executors/executormarionette.py
testing/web-platform/harness/wptrunner/executors/executorselenium.py
testing/web-platform/harness/wptrunner/executors/executorservo.py
testing/web-platform/harness/wptrunner/executors/executorservodriver.py
testing/web-platform/harness/wptrunner/executors/process.py
testing/web-platform/harness/wptrunner/executors/pytestrunner/__init__.py
testing/web-platform/harness/wptrunner/executors/pytestrunner/fixtures.py
testing/web-platform/harness/wptrunner/executors/pytestrunner/runner.py
testing/web-platform/harness/wptrunner/executors/reftest-wait.js
testing/web-platform/harness/wptrunner/executors/reftest-wait_servodriver.js
testing/web-platform/harness/wptrunner/executors/reftest-wait_webdriver.js
testing/web-platform/harness/wptrunner/executors/reftest.js
testing/web-platform/harness/wptrunner/executors/testharness_marionette.js
testing/web-platform/harness/wptrunner/executors/testharness_servodriver.js
testing/web-platform/harness/wptrunner/executors/testharness_webdriver.js
testing/web-platform/harness/wptrunner/expected.py
testing/web-platform/harness/wptrunner/hosts.py
testing/web-platform/harness/wptrunner/manifestexpected.py
testing/web-platform/harness/wptrunner/manifestinclude.py
testing/web-platform/harness/wptrunner/manifestupdate.py
testing/web-platform/harness/wptrunner/metadata.py
testing/web-platform/harness/wptrunner/products.py
testing/web-platform/harness/wptrunner/reduce.py
testing/web-platform/harness/wptrunner/testharness_runner.html
testing/web-platform/harness/wptrunner/testharnessreport-servo.js
testing/web-platform/harness/wptrunner/testharnessreport-servodriver.js
testing/web-platform/harness/wptrunner/testharnessreport.js
testing/web-platform/harness/wptrunner/testloader.py
testing/web-platform/harness/wptrunner/testrunner.py
testing/web-platform/harness/wptrunner/tests/__init__.py
testing/web-platform/harness/wptrunner/tests/test_chunker.py
testing/web-platform/harness/wptrunner/tests/test_hosts.py
testing/web-platform/harness/wptrunner/tests/test_testloader.py
testing/web-platform/harness/wptrunner/tests/test_update.py
testing/web-platform/harness/wptrunner/update/__init__.py
testing/web-platform/harness/wptrunner/update/base.py
testing/web-platform/harness/wptrunner/update/metadata.py
testing/web-platform/harness/wptrunner/update/state.py
testing/web-platform/harness/wptrunner/update/sync.py
testing/web-platform/harness/wptrunner/update/tree.py
testing/web-platform/harness/wptrunner/update/update.py
testing/web-platform/harness/wptrunner/vcs.py
testing/web-platform/harness/wptrunner/webdriver_server.py
testing/web-platform/harness/wptrunner/wptcommandline.py
testing/web-platform/harness/wptrunner/wptlogging.py
testing/web-platform/harness/wptrunner/wptmanifest/__init__.py
testing/web-platform/harness/wptrunner/wptmanifest/backends/__init__.py
testing/web-platform/harness/wptrunner/wptmanifest/backends/conditional.py
testing/web-platform/harness/wptrunner/wptmanifest/backends/static.py
testing/web-platform/harness/wptrunner/wptmanifest/node.py
testing/web-platform/harness/wptrunner/wptmanifest/parser.py
testing/web-platform/harness/wptrunner/wptmanifest/serializer.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/__init__.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/test_conditional.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/test_parser.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/test_serializer.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/test_static.py
testing/web-platform/harness/wptrunner/wptmanifest/tests/test_tokenizer.py
testing/web-platform/harness/wptrunner/wptrunner.py
testing/web-platform/harness/wptrunner/wpttest.py
testing/web-platform/meta/html/semantics/forms/the-option-element/option-element-constructor.html.ini
third_party/rust/serde-0.9.9/.cargo-checksum.json
third_party/rust/serde-0.9.9/.cargo-ok
third_party/rust/serde-0.9.9/Cargo.toml
third_party/rust/serde-0.9.9/LICENSE-APACHE
third_party/rust/serde-0.9.9/LICENSE-MIT
third_party/rust/serde-0.9.9/README.md
third_party/rust/serde-0.9.9/src/bytes.rs
third_party/rust/serde-0.9.9/src/de/content.rs
third_party/rust/serde-0.9.9/src/de/from_primitive.rs
third_party/rust/serde-0.9.9/src/de/impls.rs
third_party/rust/serde-0.9.9/src/de/mod.rs
third_party/rust/serde-0.9.9/src/de/private.rs
third_party/rust/serde-0.9.9/src/de/value.rs
third_party/rust/serde-0.9.9/src/error.rs
third_party/rust/serde-0.9.9/src/export.rs
third_party/rust/serde-0.9.9/src/iter.rs
third_party/rust/serde-0.9.9/src/lib.rs
third_party/rust/serde-0.9.9/src/macros.rs
third_party/rust/serde-0.9.9/src/ser/content.rs
third_party/rust/serde-0.9.9/src/ser/impls.rs
third_party/rust/serde-0.9.9/src/ser/impossible.rs
third_party/rust/serde-0.9.9/src/ser/mod.rs
third_party/rust/serde-0.9.9/src/ser/private.rs
third_party/rust/serde-0.9.9/src/utils.rs
third_party/rust/serde/src/de/ignored_any.rs
third_party/rust/serde/src/de/utf8.rs
third_party/rust/serde/src/private/de.rs
third_party/rust/serde/src/private/macros.rs
third_party/rust/serde/src/private/mod.rs
third_party/rust/serde/src/private/ser.rs
third_party/rust/serde_test/.cargo-checksum.json
third_party/rust/serde_test/.cargo-ok
third_party/rust/serde_test/Cargo.toml
third_party/rust/serde_test/LICENSE-APACHE
third_party/rust/serde_test/LICENSE-MIT
third_party/rust/serde_test/README.md
third_party/rust/serde_test/src/assert.rs
third_party/rust/serde_test/src/de.rs
third_party/rust/serde_test/src/error.rs
third_party/rust/serde_test/src/lib.rs
third_party/rust/serde_test/src/ser.rs
third_party/rust/serde_test/src/token.rs
third_party/rust/unicode-bidi-0.2.5/.cargo-checksum.json
third_party/rust/unicode-bidi-0.2.5/.cargo-ok
third_party/rust/unicode-bidi-0.2.5/.gitignore
third_party/rust/unicode-bidi-0.2.5/.travis.yml
third_party/rust/unicode-bidi-0.2.5/AUTHORS
third_party/rust/unicode-bidi-0.2.5/COPYRIGHT
third_party/rust/unicode-bidi-0.2.5/Cargo.toml
third_party/rust/unicode-bidi-0.2.5/LICENSE-APACHE
third_party/rust/unicode-bidi-0.2.5/LICENSE-MIT
third_party/rust/unicode-bidi-0.2.5/README.md
third_party/rust/unicode-bidi-0.2.5/src/lib.rs
third_party/rust/unicode-bidi-0.2.5/src/tables.rs
third_party/rust/unicode-bidi-0.2.5/tools/generate.py
third_party/rust/unicode-bidi/benches/udhr.rs
third_party/rust/unicode-bidi/src/char_data/mod.rs
third_party/rust/unicode-bidi/src/explicit.rs
third_party/rust/unicode-bidi/src/format_chars.rs
third_party/rust/unicode-bidi/src/implicit.rs
third_party/rust/unicode-bidi/src/prepare.rs
third_party/rust/unicode-bidi/tests/conformance_tests.rs
third_party/rust/url/rust-url-todo
toolkit/library/gtest/rust/Cargo.lock
toolkit/library/rust/Cargo.lock
--- a/.gitignore
+++ b/.gitignore
@@ -85,16 +85,19 @@ devtools/**/node_modules
 GTAGS
 GRTAGS
 GSYMS
 GPATH
 
 # Git clone directory for updating web-platform-tests
 testing/web-platform/sync/
 
+# Third party metadata for web-platform-tests
+testing/web-platform/products/
+
 # Android Gradle artifacts.
 mobile/android/gradle/.gradle
 
 # XCode project cruft
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
 # Ignore mozharness execution files
--- a/.hgignore
+++ b/.hgignore
@@ -93,16 +93,19 @@
 GTAGS
 GRTAGS
 GSYMS
 GPATH
 
 # Git clone directory for updating web-platform-tests
 ^testing/web-platform/sync/
 
+# Third party metadata for web-platform-tests
+^testing/web-platform/products/
+
 # Android Gradle artifacts.
 ^mobile/android/gradle/.gradle
 
 # XCode project cruft
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/project.xcworkspace/xcuserdata
 ^embedding/ios/GeckoEmbed/GeckoEmbed.xcodeproj/xcuserdata
 
 # Ignore mozharness execution files
--- a/accessible/base/MarkupMap.h
+++ b/accessible/base/MarkupMap.h
@@ -90,16 +90,20 @@ MARKUPMAP(h4,
 MARKUPMAP(h5,
           New_HyperText,
           roles::HEADING)
 
 MARKUPMAP(h6,
           New_HyperText,
           roles::HEADING)
 
+MARKUPMAP(input,
+          New_HTMLInput,
+          0)
+
 MARKUPMAP(label,
           New_HTMLLabel,
           roles::LABEL)
 
 MARKUPMAP(legend,
           New_HTMLLegend,
           roles::LABEL)
 
--- a/accessible/base/nsAccessibilityService.cpp
+++ b/accessible/base/nsAccessibilityService.cpp
@@ -190,16 +190,29 @@ New_HTMLDefinition(nsIContent* aContent,
   if (aContext->IsList())
     return new HyperTextAccessibleWrap(aContent, aContext->Document());
   return nullptr;
 }
 
 static Accessible* New_HTMLLabel(nsIContent* aContent, Accessible* aContext)
   { return new HTMLLabelAccessible(aContent, aContext->Document()); }
 
+static Accessible* New_HTMLInput(nsIContent* aContent, Accessible* aContext)
+{
+  if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                            nsGkAtoms::checkbox, eIgnoreCase)) {
+    return new HTMLCheckboxAccessible(aContent, aContext->Document());
+  }
+  if (aContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
+                            nsGkAtoms::radio, eIgnoreCase)) {
+    return new HTMLRadioButtonAccessible(aContent, aContext->Document());
+  }
+  return nullptr;
+}
+
 static Accessible* New_HTMLOutput(nsIContent* aContent, Accessible* aContext)
   { return new HTMLOutputAccessible(aContent, aContext->Document()); }
 
 static Accessible* New_HTMLProgress(nsIContent* aContent, Accessible* aContext)
   { return new HTMLProgressMeterAccessible(aContent, aContext->Document()); }
 
 static Accessible* New_HTMLSummary(nsIContent* aContent, Accessible* aContext)
   { return new HTMLSummaryAccessible(aContent, aContext->Document()); }
--- a/accessible/tests/mochitest/events/test_scroll.xul
+++ b/accessible/tests/mochitest/events/test_scroll.xul
@@ -59,17 +59,23 @@
       ];
 
       this.unexpectedEventSeq = [
         new invokerChecker(EVENT_SCROLLING_START, getAnchorJumpInTabDocument, 1)
       ];
 
       this.invoke = function loadTabInBackground_invoke()
       {
-        tabBrowser().loadOneTab(aURL, null, "", null, true);
+        tabBrowser().loadOneTab(aURL, {
+          referrerURI: null,
+          charset: "",
+          postData: null,
+          inBackground: true,
+          triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+        });
       }
 
       this.getID = function loadTabInBackground_getID()
       {
         return "load tab in background: " + aURL;
       }
     }
 
--- a/accessible/tests/mochitest/relations/test_embeds.xul
+++ b/accessible/tests/mochitest/relations/test_embeds.xul
@@ -51,17 +51,23 @@
         return "load uri " + aURI;
       }
     }
 
     function loadOneTab(aURI)
     {
       this.invoke = function loadOneTab_invoke()
       {
-        tabBrowser().loadOneTab(aURI, null, null, null, false);
+        tabBrowser().loadOneTab(aURI, {
+          referrerURI: null,
+          charset: null,
+          postData: null,
+          inBackground: false,
+          triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+        });
       }
 
       this.eventSeq = [
         new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument)
       ];
 
       this.finalCheck = function loadURI_finalCheck()
       {
--- a/accessible/tests/mochitest/states/test_visibility.html
+++ b/accessible/tests/mochitest/states/test_visibility.html
@@ -48,17 +48,23 @@
     function addTabInvoker(aURL, aFunc)
     {
       this.eventSeq = [
         new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1)
       ];
 
       this.invoke = function addTabInvoker_invoke()
       {
-        tabBrowser().loadOneTab(aURL, null, "", null, false);
+        tabBrowser().loadOneTab(aURL, {
+          referrerURI: null,
+          charset: "",
+          postData: null,
+          inBackground: false,
+          triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+        });
       }
 
       this.finalCheck = function addTabInvoker_finalCheck()
       {
         aFunc.call();
       }
 
       this.getID = function addTabInvoker_getID()
--- a/addon-sdk/source/lib/toolkit/loader.js
+++ b/addon-sdk/source/lib/toolkit/loader.js
@@ -1177,23 +1177,23 @@ function Loader(options) {
     modules[uri] = freeze(module);
   }
 
   // Create the unique sandbox we will be using for all modules,
   // so that we prevent creating a new comportment per module.
   // The side effect is that all modules will share the same
   // global objects.
   let sharedGlobalSandbox = Sandbox({
-    name: "Addon-SDK",
+    name: options.sandboxName || "Addon-SDK",
     wantXrays: false,
     wantGlobalProperties: [],
     invisibleToDebugger: options.invisibleToDebugger || false,
     metadata: {
-      addonID: options.id,
-      URI: "Addon-SDK"
+      addonID: options.noSandboxAddonId ? undefined : options.id,
+      URI: options.sandboxName || "Addon-SDK"
     },
     prototype: options.sandboxPrototype || globals,
   });
 
   if (options.sandboxPrototype) {
     // If we were given a sandboxPrototype, we have to define the globals on
     // the sandbox directly. Note that this will not work for callers who
     // depend on being able to add globals after the loader was created.
--- a/browser/app/permissions
+++ b/browser/app/permissions
@@ -7,16 +7,17 @@
 # See nsPermissionManager.cpp for more...
 
 # UITour
 origin	uitour	1	https://www.mozilla.org
 origin	uitour	1	https://support.mozilla.org
 origin	uitour	1	https://addons.mozilla.org
 origin	uitour	1	https://discovery.addons.mozilla.org
 origin	uitour	1	about:home
+origin	uitour	1	about:newtab
 
 # XPInstall
 origin	install	1	https://addons.mozilla.org
 origin	install	1	https://testpilot.firefox.com
 
 # Remote troubleshooting
 origin	remote-troubleshooting	1	https://input.mozilla.org
 origin	remote-troubleshooting	1	https://support.mozilla.org
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1038,17 +1038,17 @@ pref("security.sandbox.windows.log.stack
 // SetSecurityLevelForGPUProcess() in
 // security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp
 pref("security.sandbox.gpu.level", 0);
 #endif
 
 #if defined(XP_MACOSX) && defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // This pref is discussed in bug 1083344, the naming is inspired from its
 // Windows counterpart, but on Mac it's an integer which means:
-// 0 -> "no sandbox"
+// 0 -> "no sandbox" (nightly only)
 // 1 -> "preliminary content sandboxing enabled: write access to
 //       home directory is prevented"
 // 2 -> "preliminary content sandboxing enabled with profile protection:
 //       write access to home directory is prevented, read and write access
 //       to ~/Library and profile directories are prevented (excluding
 //       $PROFILE/{extensions,weave})"
 // This setting is read when the content process is started. On Mac the content
 // process is killed when all windows are closed, so a change will take effect
@@ -1599,16 +1599,17 @@ pref("dom.ipc.processPrelaunch.enabled",
 pref("browser.migrate.automigrate.enabled", true);
 #else
 pref("browser.migrate.automigrate.enabled", false);
 #endif
 // 4 here means the suggestion notification will be automatically
 // hidden the 4th day, so it will actually be shown on 3 different days.
 pref("browser.migrate.automigrate.daysToOfferUndo", 4);
 pref("browser.migrate.automigrate.ui.enabled", true);
+pref("browser.migrate.automigrate.inpage.ui.enabled", false);
 
 // See comments in bug 1340115 on how we got to these numbers.
 pref("browser.migrate.chrome.history.limit", 2000);
 pref("browser.migrate.chrome.history.maxAgeInDays", 180);
 
 // Enable browser frames for use on desktop.  Only exposed to chrome callers.
 pref("dom.mozBrowserFramesEnabled", true);
 
@@ -1666,8 +1667,11 @@ pref("browser.sessionstore.restore_tabs_
 
 // Enable safebrowsing v4 tables (suffixed by "-proto") update.
 #ifdef NIGHTLY_BUILD
 pref("urlclassifier.malwareTable", "goog-malware-shavar,goog-unwanted-shavar,goog-malware-proto,goog-unwanted-proto,test-malware-simple,test-unwanted-simple");
 pref("urlclassifier.phishTable", "goog-phish-shavar,goog-phish-proto,test-phish-simple");
 #endif
 
 pref("browser.suppress_first_window_animation", true);
+
+// Preferences for Photon onboarding system extension
+pref("browser.onboarding.disabled", false);
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -347,20 +347,24 @@
 
     <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" />
 
     <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;"
          modifiers="accel,shift" reserved="true"/>
     <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/>
 #ifdef XP_MACOSX
     <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/>
-    <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" modifiers="accel" reserved="true"/>
-#elifdef XP_UNIX
-    <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" command="cmd_quitApplication" modifiers="accel" reserved="true"/>
 #endif
+    <key id="key_quitApplication" key="&quitApplicationCmd.key;"
+#ifdef XP_WIN
+         modifiers="accel,shift"
+#else
+         modifiers="accel"
+#endif
+         reserved="true"/>
 
 #ifdef FULL_BROWSER_WINDOW
     <key id="key_undoCloseTab" command="History:UndoCloseTab" key="&tabCmd.commandkey;" modifiers="accel,shift"/>
 #endif
     <key id="key_undoCloseWindow" command="History:UndoCloseWindow" key="&newNavigatorCmd.key;" modifiers="accel,shift"/>
 
 #ifdef XP_GNOME
 #define NUM_SELECT_TAB_MODIFIER alt
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -1,14 +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/. */
 
 /* eslint-env mozilla/browser-window */
-/* global OpenGraphBuilder:false, DynamicResizeWatcher:false */
+/* global OpenGraphBuilder:false, DynamicResizeWatcher:false, Utils:false*/
 
 // the "exported" symbols
 var SocialUI,
     SocialShare,
     SocialActivationListener;
 
 (function() {
 "use strict";
@@ -20,16 +20,22 @@ XPCOMUtils.defineLazyGetter(this, "OpenG
 });
 
 XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
   let tmp = {};
   Cu.import("resource:///modules/Social.jsm", tmp);
   return tmp.DynamicResizeWatcher;
 });
 
+XPCOMUtils.defineLazyGetter(this, "Utils", function() {
+  let tmp = {};
+  Cu.import("resource://gre/modules/sessionstore/Utils.jsm", tmp);
+  return tmp.Utils;
+});
+
 let messageManager = window.messageManager;
 let openUILinkIn = window.openUILinkIn;
 
 SocialUI = {
   _initialized: false,
 
   // Called on delayed startup to initialize the UI
   init: function SocialUI_init() {
@@ -198,17 +204,21 @@ SocialActivationListener = {
           SocialShare.populateProviderMenu();
           if (SocialShare.panel.state == "open") {
             SocialShare.sharePage(provider.origin);
           }
         }
         if (provider.postActivationURL) {
           // if activated from an open share panel, we load the landing page in
           // a background tab
-          gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
+          let triggeringPrincipal = Utils.deserializePrincipal(aMessage.data.triggeringPrincipal);
+          gBrowser.loadOneTab(provider.postActivationURL, {
+            inBackground: SocialShare.panel.state == "open",
+            triggeringPrincipal,
+          });
         }
       });
     }, options);
   }
 }
 
 SocialShare = {
   get _dynamicResizer() {
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -108,17 +108,17 @@ panelview {
   transition: transform var(--panelui-subview-transition-duration);
 }
 
 panelview:not([mainview]):not([current]) {
   transition: visibility 0s linear var(--panelui-subview-transition-duration);
   visibility: collapse;
 }
 
-panelview:not([title]) > .panel-header {
+panelview[mainview] > .panel-header {
   display: none;
 }
 
 tabbrowser {
   -moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser");
 }
 
 .tabbrowser-tabs {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2286,21 +2286,24 @@ function delayedOpenWindow(chrome, flags
   // window.arguments[1] be an integer instead of null.
   setTimeout(function() { openDialog(chrome, "_blank", flags, href, null, null, postData); }, 10);
 }
 
 /* Required because the tab needs time to set up its content viewers and get the load of
    the URI kicked off before becoming the active content area. */
 function delayedOpenTab(aUrl, aReferrer, aCharset, aPostData, aAllowThirdPartyFixup) {
   gBrowser.loadOneTab(aUrl, {
-                      referrerURI: aReferrer,
-                      charset: aCharset,
-                      postData: aPostData,
-                      inBackground: false,
-                      allowThirdPartyFixup: aAllowThirdPartyFixup});
+    referrerURI: aReferrer,
+    charset: aCharset,
+    postData: aPostData,
+    inBackground: false,
+    allowThirdPartyFixup: aAllowThirdPartyFixup,
+    // Bug 1367168: only use systemPrincipal till we can remove that function
+    triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+  });
 }
 
 var gLastOpenDirectory = {
   _lastDir: null,
   get path() {
     if (!this._lastDir || !this._lastDir.exists()) {
       try {
         this._lastDir = gPrefService.getComplexValue("browser.open.lastDir",
@@ -2598,17 +2601,18 @@ function BrowserViewSourceOfDocument(aAr
       // descriptor for the tab (when possible) or fallback to the network if
       // that fails.  Either way, the view source module will manage the tab's
       // location, so use "about:blank" here to avoid unnecessary redundant
       // requests.
       let tab = tabBrowser.loadOneTab("about:blank", {
         relatedToCurrent: true,
         inBackground: false,
         preferredRemoteType,
-        sameProcessAsFrameLoader: args.browser ? args.browser.frameLoader : null
+        sameProcessAsFrameLoader: args.browser ? args.browser.frameLoader : null,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
       });
       args.viewSourceBrowser = tabBrowser.getBrowserForTab(tab);
       top.gViewSourceUtils.viewSourceInBrowser(args);
     } else {
       top.gViewSourceUtils.viewSource(args);
     }
   }
 
@@ -3407,17 +3411,18 @@ var PrintPreviewListener = {
   _lastRequestedPrintPreviewTab: null,
 
   _createPPBrowser() {
     let browser = this.getSourceBrowser();
     let preferredRemoteType = browser.remoteType;
     return gBrowser.loadOneTab("about:printpreview", {
       inBackground: true,
       preferredRemoteType,
-      sameProcessAsFrameLoader: browser.frameLoader
+      sameProcessAsFrameLoader: browser.frameLoader,
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
   getPrintPreviewBrowser() {
     if (!this._printPreviewTab) {
       this._printPreviewTab = this._createPPBrowser();
     }
     gBrowser._allowTabChange = true;
     this._lastRequestedPrintPreviewTab = gBrowser.selectedTab = this._printPreviewTab;
@@ -3432,17 +3437,18 @@ var PrintPreviewListener = {
     this._lastRequestedPrintPreviewTab = gBrowser.selectedTab = this._simplifiedPrintPreviewTab;
     gBrowser._allowTabChange = false;
     return gBrowser.getBrowserForTab(this._simplifiedPrintPreviewTab);
   },
   createSimplifiedBrowser() {
     let browser = this.getSourceBrowser();
     this._simplifyPageTab = gBrowser.loadOneTab("about:printpreview", {
       inBackground: true,
-      sameProcessAsFrameLoader: browser.frameLoader
+      sameProcessAsFrameLoader: browser.frameLoader,
+      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
      });
     return this.getSimplifiedSourceBrowser();
   },
   getSourceBrowser() {
     if (!this._tabBeforePrintPreview) {
       this._tabBeforePrintPreview = gBrowser.selectedTab;
     }
     return this._tabBeforePrintPreview.linkedBrowser;
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -606,16 +606,17 @@
                 accesskey="&selectAllCmd.accesskey;"
                 cmd="cmd_selectAll"/>
       <menuseparator/>
       <menuitem label="&syncSyncNowItem.label;"
                 accesskey="&syncSyncNowItem.accesskey;"
                 id="syncedTabsRefreshFilter"/>
     </menupopup>
   </popupset>
+  <box id="appMenu-viewCache" hidden="true"/>
 
 #ifdef CAN_DRAW_IN_TITLEBAR
 <vbox id="titlebar">
   <hbox id="titlebar-content">
     <spacer id="titlebar-spacer" flex="1"/>
     <hbox id="titlebar-buttonbox-container">
 #ifdef XP_WIN
       <hbox id="private-browsing-indicator-titlebar">
@@ -1189,16 +1190,21 @@
                      ondragenter="newWindowButtonObserver.onDragOver(event)"
                      ondragexit="newWindowButtonObserver.onDragExit(event)"/>
 
       <toolbarbutton id="fullscreen-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
                      observes="View:FullScreen"
                      type="checkbox"
                      label="&fullScreenCmd.label;"
                      tooltip="dynamic-shortcut-tooltip"/>
+#ifdef MOZ_PHOTON_THEME
+      <toolbarbutton id="library-button" class="toolbarbutton-1 chromeclass-toolbar-additional"
+                     oncommand="PanelUI.showSubView('appMenu-libraryView', this, null, true);"
+                     label="&places.library.title;"/>
+#endif
     </toolbarpalette>
   </toolbox>
 
   <hbox id="fullscr-toggler" hidden="true"/>
 
   <deck id="content-deck" flex="1">
     <hbox flex="1" id="browser">
       <vbox id="browser-border-start" hidden="true" layer="true"/>
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -36,16 +36,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormSubmitObserver",
   "resource:///modules/FormSubmitObserver.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
   "resource://gre/modules/PageMetadata.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils",
   "resource:///modules/PlacesUIUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+  "resource://gre/modules/sessionstore/Utils.jsm");
 XPCOMUtils.defineLazyGetter(this, "PageMenuChild", function() {
   let tmp = {};
   Cu.import("resource://gre/modules/PageMenu.jsm", tmp);
   return new tmp.PageMenuChild();
 });
 XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
   "resource://gre/modules/WebNavigationFrames.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Feeds",
@@ -803,17 +805,18 @@ addEventListener("ActivateSocialFeature"
   } else {
     Cu.reportError("Social Service manifest not available");
     return;
   }
 
   sendAsyncMessage("Social:Activation", {
     url: ownerDocument.location.href,
     origin: ownerDocument.nodePrincipal.origin,
-    manifest: data
+    manifest: data,
+    triggeringPrincipal: Utils.serializePrincipal(ownerDocument.nodePrincipal),
   });
 }, true, true);
 
 addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
   let video = message.objects.target;
   let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   canvas.width = video.videoWidth;
   canvas.height = video.videoHeight;
--- a/browser/base/content/newtab/page.js
+++ b/browser/base/content/newtab/page.js
@@ -246,15 +246,11 @@ var gPage = {
       this.onPageVisibleAndLoaded();
     } else {
       addEventListener("load", this);
     }
   },
 
   onPageVisibleAndLoaded() {
     // Maybe tell the user they can undo an initial automigration
-    this.maybeShowAutoMigrationUndoNotification();
-  },
-
-  maybeShowAutoMigrationUndoNotification() {
-    sendAsyncMessage("NewTab:MaybeShowAutoMigrationUndoNotification");
+    sendAsyncMessage("NewTab:MaybeShowMigrateMessage");
   },
 };
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -1174,17 +1174,18 @@ nsContextMenu.prototype = {
       // In the case of popups, we need to find a non-popup browser window.
       if (!tabBrowser || !window.toolbar.visible) {
         // This returns only non-popup browser windows by default.
         let browserWindow = RecentWindow.getMostRecentBrowserWindow();
         tabBrowser = browserWindow.gBrowser;
       }
       let tab = tabBrowser.loadOneTab("about:blank", {
         relatedToCurrent: true,
-        inBackground: false
+        inBackground: false,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
       });
       return tabBrowser.getBrowserForTab(tab);
     }
 
     let target = aContext == "mathml" ? this.target : null;
     top.gViewSourceUtils.viewPartialSourceInBrowser(gBrowser.selectedBrowser, target, openSelectionFn);
   },
 
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -11,16 +11,18 @@ var {classes: Cc, interfaces: Ci, utils:
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
   "resource:///modules/E10SUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
   "resource://gre/modules/BrowserUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Utils",
+  "resource://gre/modules/sessionstore/Utils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AboutReader",
   "resource://gre/modules/AboutReader.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode",
   "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() {
   let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery;
@@ -155,17 +157,17 @@ var AboutHomeListener = {
     docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
   },
 
   onPageLoad() {
     addMessageListener("AboutHome:Update", this);
     addEventListener("click", this, true);
     addEventListener("pagehide", this, true);
 
-    sendAsyncMessage("AboutHome:MaybeShowAutoMigrationUndoNotification");
+    sendAsyncMessage("AboutHome:MaybeShowMigrateMessage");
     sendAsyncMessage("AboutHome:RequestUpdate");
   },
 
   onClick(aEvent) {
     if (!aEvent.isTrusted || // Don't trust synthetic events
         aEvent.button == 2 || aEvent.target.localName != "button") {
       return;
     }
@@ -652,27 +654,28 @@ let PrerenderContentHandler = {
             break;
           }
         }
         break;
       }
     }
   },
 
-  startPrerenderingDocument(aHref, aReferrer) {
+  startPrerenderingDocument(aHref, aReferrer, aTriggeringPrincipal) {
     // XXX: Make this constant a pref
     if (this._pending.length >= 2) {
       return;
     }
 
     let id = ++this._idMonotonic;
     sendAsyncMessage("Prerender:Request", {
       href: aHref.spec,
       referrer: aReferrer ? aReferrer.spec : null,
       id,
+      triggeringPrincipal: Utils.serializePrincipal(aTriggeringPrincipal),
     });
 
     this._pending.push({
       href: aHref,
       referrer: aReferrer,
       id,
       success: null,
       failure: null,
@@ -724,19 +727,19 @@ var WebBrowserChrome = {
   },
 
   // Try to reload the currently active or currently loading page in a new process.
   reloadInFreshProcess(aDocShell, aURI, aReferrer, aTriggeringPrincipal, aLoadFlags) {
     E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, aTriggeringPrincipal, true, aLoadFlags);
     return true;
   },
 
-  startPrerenderingDocument(aHref, aReferrer) {
+  startPrerenderingDocument(aHref, aReferrer, aTriggeringPrincipal) {
     if (PrerenderContentHandler.initialized) {
-      PrerenderContentHandler.startPrerenderingDocument(aHref, aReferrer);
+      PrerenderContentHandler.startPrerenderingDocument(aHref, aReferrer, aTriggeringPrincipal);
     }
   },
 
   shouldSwitchToPrerenderedDocument(aHref, aReferrer, aSuccess, aFailure) {
     if (PrerenderContentHandler.initialized) {
       return PrerenderContentHandler.shouldSwitchToPrerenderedDocument(
         aHref, aReferrer, aSuccess, aFailure);
     }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -4105,30 +4105,42 @@
             },
 
             // This function is called after all the main state changes to
             // make sure we display the right tab.
             updateDisplay() {
               let requestedTabState = this.getTabState(this.requestedTab);
               let requestedBrowser = this.requestedTab.linkedBrowser;
 
-              // It is more desirable to show a blank tab when appropriate than
+              // It is often more desirable to show a blank tab when appropriate than
               // the tab switch spinner - especially since the spinner is usually
               // preceded by a perceived lag of TAB_SWITCH_TIMEOUT ms in the
               // tab switch. We can hide this lag, and hide the time being spent
               // constructing TabChild's, layer trees, etc, by showing a blank
               // tab instead and focusing it immediately.
               let shouldBeBlank = false;
               if (requestedBrowser.isRemoteBrowser) {
-                // If a tab is remote, we can show a blank tab instead of a
-                // spinner if we know it has never presented before, or if it
-                // has just crashed and we haven't started showing the tab crashed
-                // page yet.
+                // If a tab is remote and the window is not minimized, we can show a
+                // blank tab instead of a spinner in the following cases:
+                //
+                // 1. The tab has just crashed, and we haven't started showing the
+                //    tab crashed page yet (in this case, the TabParent is null)
+                // 2. The tab has never presented, and has not finished loading
+                //    a non-blank page.
+                //
+                // For (2), "finished loading a non-blank page" is determined by
+                // looking at the loaded URI and the busy state on the tab element.
+                let hasSufficientlyLoaded =
+                  !this.requestedTab.hasAttribute("busy") &&
+                  requestedBrowser.currentURI.spec != "about:blank";
+
                 let fl = requestedBrowser.frameLoader;
-                shouldBeBlank = !this.minimized && (!fl.tabParent || !fl.tabParent.hasPresented);
+                shouldBeBlank = !this.minimized &&
+                                (!fl.tabParent ||
+                                 (!hasSufficientlyLoaded && !fl.tabParent.hasPresented));
               }
 
               this.log("Tab should be blank: " + shouldBeBlank);
               this.log("Requested tab is remote?: " + requestedBrowser.isRemoteBrowser);
 
               // Figure out which tab we actually want visible right now.
               let showTab = null;
               if (requestedTabState != this.STATE_LOADED &&
@@ -4623,17 +4635,16 @@
                 this.switchInProgress = false;
               }
             },
 
             spinnerDisplayed() {
               this.assert(!this.spinnerTab);
               let browser = this.requestedTab.linkedBrowser;
               this.assert(browser.isRemoteBrowser);
-              this.assert(browser.frameLoader.tabParent.hasPresented);
               TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_MS", window);
               // We have a second, similar probe for capturing recordings of
               // when the spinner is displayed for very long periods.
               TelemetryStopwatch.start("FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS", window);
               this.addMarker("AsyncTabSwitch:SpinnerShown");
             },
 
             spinnerHidden() {
@@ -5206,16 +5217,17 @@
 
               let newTab = this.loadOneTab(data.href, {
                 referrerURI: (data.referrer ? makeURI(data.referrer) : null),
                 referrerPolicy: Ci.nsIHttpChannel.REFERRER_POLICY_UNSET,
                 postData: null,
                 allowThirdPartyFixup: true,
                 relatedToCurrent: true,
                 isPrerendered: true,
+                triggeringPrincipal: Utils.deserializePrincipal(data.triggeringPrincipal),
               });
               let partialSHistory = newTab.linkedBrowser.frameLoader.partialSHistory;
               groupedSHistory.addPrerenderingPartialSHistory(partialSHistory, data.id);
               break;
             }
 
             case "Prerender:Cancel": {
               let groupedSHistory = browser.frameLoader.groupedSHistory;
@@ -6036,30 +6048,27 @@
           else
             this.visible = true;
         ]]></body>
       </method>
 
       <field name="_closeButtonsUpdatePending">false</field>
       <method name="adjustTabstrip">
         <body><![CDATA[
+          // If we're overflowing, tabs are at their minimum widths.
+          if (this.getAttribute("overflow") == "true") {
+            this.setAttribute("closebuttons", "activetab");
+            return;
+          }
+
           if (this._closeButtonsUpdatePending) {
             return;
           }
           this._closeButtonsUpdatePending = true;
 
-          // If we're overflowing, tabs are at their minimum widths.
-          if (this.getAttribute("overflow") == "true") {
-            window.requestAnimationFrame(() => {
-              this._closeButtonsUpdatePending = false;
-              this.setAttribute("closebuttons", "activetab");
-            });
-            return;
-          }
-
           // Wait until after the next paint to get current layout data from
           // getBoundsWithoutFlushing.
           window.requestAnimationFrame(() => {
             window.requestAnimationFrame(() => {
               this._closeButtonsUpdatePending = false;
 
               // The scrollbox may have started overflowing since we checked
               // overflow earlier, so check again.
--- a/browser/base/content/test/general/browser_bug462289.js
+++ b/browser/base/content/test/general/browser_bug462289.js
@@ -10,39 +10,39 @@ function focus_in_navbar() {
 
 function test() {
   waitForExplicitFinish();
 
   tab1 = BrowserTestUtils.addTab(gBrowser, "about:blank", {skipAnimation: true});
   tab2 = BrowserTestUtils.addTab(gBrowser, "about:blank", {skipAnimation: true});
 
   EventUtils.synthesizeMouseAtCenter(tab1, {});
-  executeSoon(step2);
+  setTimeout(step2, 0);
 }
 
 function step2() {
   is(gBrowser.selectedTab, tab1, "1st click on tab1 selects tab");
   isnot(document.activeElement, tab1, "1st click on tab1 does not activate tab");
 
   EventUtils.synthesizeMouseAtCenter(tab1, {});
-  executeSoon(step3);
+  setTimeout(step3, 0);
 }
 
 function step3() {
   is(gBrowser.selectedTab, tab1, "2nd click on selected tab1 keeps tab selected");
   isnot(document.activeElement, tab1, "2nd click on selected tab1 does not activate tab");
 
   ok(true, "focusing URLBar then sending 1 Shift+Tab.");
   gURLBar.focus();
   EventUtils.synthesizeKey("VK_TAB", {shiftKey: true});
   is(gBrowser.selectedTab, tab1, "tab key to selected tab1 keeps tab selected");
   is(document.activeElement, tab1, "tab key to selected tab1 activates tab");
 
   EventUtils.synthesizeMouseAtCenter(tab1, {});
-  executeSoon(step4);
+  setTimeout(step4, 0);
 }
 
 function step4() {
   is(gBrowser.selectedTab, tab1, "3rd click on activated tab1 keeps tab selected");
   is(document.activeElement, tab1, "3rd click on activated tab1 keeps tab activated");
 
   gBrowser.addEventListener("TabSwitchDone", step5);
   EventUtils.synthesizeMouseAtCenter(tab2, {});
@@ -55,17 +55,17 @@ function step5() {
   // listener, and focuses the current tab if another tab previously had focus.
   is(gBrowser.selectedTab, tab2, "click on tab2 while tab1 is activated selects tab");
   is(document.activeElement, tab2, "click on tab2 while tab1 is activated activates tab");
 
   info("focusing content then sending middle-button mousedown to tab2.");
   gBrowser.selectedBrowser.focus();
 
   EventUtils.synthesizeMouseAtCenter(tab2, {button: 1, type: "mousedown"});
-  executeSoon(step6);
+  setTimeout(step6, 0);
 }
 
 function step6() {
   is(gBrowser.selectedTab, tab2, "middle-button mousedown on selected tab2 keeps tab selected");
   isnot(document.activeElement, tab2, "middle-button mousedown on selected tab2 does not activate tab");
 
   gBrowser.removeTab(tab2);
   gBrowser.removeTab(tab1);
--- a/browser/base/content/test/performance/browser.ini
+++ b/browser/base/content/test/performance/browser.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 support-files =
   head.js
+[browser_startup.js]
 [browser_tabclose_reflows.js]
 [browser_tabopen_reflows.js]
 [browser_toolbariconcolor_restyles.js]
 [browser_windowopen_reflows.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/performance/browser_startup.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* This test records at which phase of startup the JS components and modules
+ * are first loaded.
+ * If you made changes that cause this test to fail, it's likely because you
+ * are loading more JS code during startup.
+ * Most code has no reason to run off of the app-startup notification
+ * (this is very early, before we have selected the user profile, so
+ *  preferences aren't accessible yet).
+ * If your code isn't strictly required to show the first browser window,
+ * it shouldn't be loaded before we are done with first paint.
+ * Finally, if your code isn't really needed during startup, it should not be
+ * loaded before we have started handling user events.
+ */
+
+"use strict";
+
+const startupPhases = {
+  // For app-startup, we have a whitelist of acceptable JS files.
+  // Anything loaded during app-startup must have a compelling reason
+  // to run before we have even selected the user profile.
+  // Consider loading your code after first paint instead,
+  // eg. from nsBrowserGlue.js' _onFirstWindowLoaded method).
+  "before profile selection": {whitelist: {
+    components: new Set([
+      "nsBrowserGlue.js",
+      "MainProcessSingleton.js",
+
+      // Bugs to fix: The following components shouldn't be initialized that early.
+      "WebContentConverter.js",
+      "nsSessionStartup.js",
+      "PushComponents.js",
+    ]),
+    modules: new Set([
+      "resource://gre/modules/AppConstants.jsm",
+      "resource://gre/modules/XPCOMUtils.jsm",
+      "resource://gre/modules/Services.jsm",
+
+      // Bugs to fix: Probably loaded too early, needs investigation.
+      "resource://gre/modules/Log.jsm",
+      "resource://gre/modules/AsyncPrefs.jsm",
+      "resource://gre/modules/RemotePageManager.jsm",
+      "resource://gre/modules/TelemetryStopwatch.jsm",
+      "resource://gre/modules/PrivateBrowsingUtils.jsm",
+      "resource://gre/modules/Promise.jsm"
+    ])
+  }},
+
+  // For the following phases of startup we have only a black list for now
+
+  // We are at this phase after creating the first browser window (ie. after final-ui-startup).
+  "before opening first browser window": {blacklist: {
+    components: new Set([
+      "nsSearchService.js",
+    ])
+  }},
+
+  // We reach this phase right after showing the first browser window.
+  // This means that anything already loaded at this point has been loaded
+  // before first paint and delayed it.
+  "before first paint": {},
+
+  // We are at this phase once we are ready to handle user events.
+  // Anything loaded at this phase or before gets in the way of the user
+  // interacting with the first browser window.
+  "before handling user events": {},
+}
+
+function test() {
+  if (!AppConstants.NIGHTLY_BUILD && !AppConstants.DEBUG) {
+    ok(!("@mozilla.org/test/startuprecorder;1" in Cc),
+       "the startup recorder component shouldn't exist in this non-nightly non-debug build.");
+    return;
+  }
+
+  let data = Cc["@mozilla.org/test/startuprecorder;1"].getService().wrappedJSObject.data;
+  // Keep only the file name for components, as the path is an absolute file
+  // URL rather than a resource:// URL like for modules.
+  for (let phase in data) {
+    data[phase].components =
+      data[phase].components.map(f => f.replace(/.*\//, ""))
+                            .filter(c => c != "startupRecorder.js");
+  }
+
+  // This block only adds debug output to help find the next bugs to file,
+  // it doesn't contribute to the actual test.
+  SimpleTest.requestCompleteLog();
+  let previous;
+  for (let phase in data) {
+    for (let scriptType in data[phase]) {
+      for (let f of data[phase][scriptType]) {
+        // phases are ordered, so if a script wasn't loaded yet at the immediate
+        // previous phase, it wasn't loaded during any of the previous phases
+        // either, and is new in the current phase.
+        if (!previous || !data[previous][scriptType].includes(f))
+          info(`${scriptType} loaded ${phase}: ${f}`);
+      }
+    }
+    previous = phase;
+  }
+
+  for (let phase in startupPhases) {
+    let loadedList = data[phase];
+    let whitelist = startupPhases[phase].whitelist || null;
+    if (whitelist) {
+      for (let scriptType in loadedList) {
+        loadedList[scriptType] = loadedList[scriptType].filter(c => {
+          if (!whitelist[scriptType].has(c))
+            return true;
+          whitelist[scriptType].delete(c);
+          return false;
+        });
+        is(loadedList[scriptType].length, 0,
+           `should have no unexpected ${scriptType} loaded ${phase}`);
+        for (let script of loadedList[scriptType]) {
+          ok(false, `unexpected ${scriptType}: ${script}`);
+        }
+        is(whitelist[scriptType].size, 0,
+           `all ${scriptType} whitelist entries should have been used`);
+        for (let script of whitelist[scriptType]) {
+          ok(false, `unused ${scriptType} whitelist entry: ${script}`);
+        }
+      }
+    }
+    let blacklist = startupPhases[phase].blacklist || null;
+    if (blacklist) {
+      for (let scriptType in blacklist) {
+        for (let file of blacklist[scriptType]) {
+          ok(!loadedList[scriptType].includes(file), `${file} is not allowed ${phase}`);
+        }
+      }
+    }
+  }
+}
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -33,16 +33,17 @@ subsuite = clipboard
 support-files =
   redirect_bug623155.sjs
 [browser_bug783614.js]
 [browser_canonizeURL.js]
 [browser_dragdropURL.js]
 [browser_locationBarCommand.js]
 [browser_locationBarExternalLoad.js]
 [browser_moz_action_link.js]
+[browser_new_tab_urlbar_reset.js]
 [browser_pasteAndGo.js]
 subsuite = clipboard
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 subsuite = clipboard
 [browser_search_favicon.js]
 [browser_tabMatchesInAwesomebar.js]
 support-files =
   moz.png
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_new_tab_urlbar_reset.js
@@ -0,0 +1,16 @@
+"use strict";
+
+/**
+ * Verify that urlbar state is reset when openig a new tab, so searching for the
+ * same text will reopen the results popup.
+ */
+add_task(async function() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+  await promiseAutocompleteResultPopup("m");
+  ok(gURLBar.popupOpen, "The popup is open");
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+  await promiseAutocompleteResultPopup("m");
+  ok(gURLBar.popupOpen, "The popup is open");
+  await BrowserTestUtils.removeTab(tab);
+  await BrowserTestUtils.removeTab(tab2);
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -117,16 +117,21 @@ file, You can obtain one at http://mozil
           pasteAndGo.setAttribute("label", label);
           pasteAndGo.setAttribute("anonid", "paste-and-go");
           pasteAndGo.setAttribute("oncommand",
               "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();");
           cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling);
         }
 
         this._enableOrDisableOneOffSearches();
+
+        // The autocomplete controller uses heuristic on some internal caches
+        // to handle cases like backspace, autofill or repeated searches.
+        // Ensure to clear those internal caches when switching tabs.
+        gBrowser.tabContainer.addEventListener("TabSelect", this);
       ]]></constructor>
 
       <destructor><![CDATA[
         this._prefs.removeObserver("", this);
         this._prefs = null;
         this.inputField.controllers.removeController(this._copyCutController);
         this.inputField.removeEventListener("paste", this);
         this.inputField.removeEventListener("mousedown", this);
@@ -1058,16 +1063,17 @@ file, You can obtain one at http://mozil
                 break;
               case "delay":
                 this.timeout = this._prefs.getIntPref(aData);
                 break;
               case "formatting.enabled":
                 this._formattingEnabled = this._prefs.getBoolPref(aData);
                 break;
               case "suggest.searches":
+              case "userMadeSearchSuggestionsChoice":
                 // Mirror the value for future use, see the comment in the
                 // binding's constructor.
                 this._prefs.setBoolPref("searchSuggestionsChoice",
                   this._prefs.getBoolPref("suggest.searches"));
                 // Clear the cached value to allow changing conditions in tests.
                 delete this._whichSearchSuggestionsNotification;
                 break;
               case "trimURLs":
@@ -1142,16 +1148,20 @@ file, You can obtain one at http://mozil
                 break;
               }
               this.setAttribute("textoverflow", "true");
               break;
             case "underflow":
               this.removeAttribute("textoverflow");
               this._hideURLTooltip();
               break;
+            case "TabSelect":
+              this.detachController();
+              this.attachController();
+              break;
           }
         ]]></body>
       </method>
 
       <!--
         onBeforeTextValueSet is called by the base-binding's .textValue getter.
         It should return the value that the getter should use.
       -->
new file mode 100644
--- /dev/null
+++ b/browser/base/content/usercontext-briefcase.svg
@@ -0,0 +1,9 @@
+<!-- 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="32" height="32" viewBox="0 0 32 32">
+  <path fill="context-fill" fill-rule="evenodd"
+        d="M22,9.99887085 L21.635468,10 L29.0034652,10 C29.5538362,10 30,10.4449463 30,10.9933977 L30,27.0066023 C30,27.5552407 29.5601869,28 29.0034652,28 L2.99653482,28 C2.44616384,28 2,27.5550537 2,27.0066023 L2,10.9933977 C2,10.4447593 2.43981314,10 2.99653482,10 L8,10 L8,7.99922997 C8,5.79051625 10.0426627,4 12.5635454,4 L19.4364546,4 C21.9568311,4 24,5.79246765 24,7.99922997 L24,9.99267578 L22,9.99887085 L22,10 L10,10 L10,7.99922997 C10,6.89421235 11.0713286,6 12.3917227,6 L19.6082773,6 C20.9273761,6 22,6.89552665 22,7.99922997 L22,9.99887085 Z"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/usercontext-cart.svg
@@ -0,0 +1,9 @@
+<!-- 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="32" height="32" viewBox="0 0 32 32">
+  <path fill="context-fill" fill-rule="evenodd"
+        d="M20.8195396,14 L15.1804604,14 L15.1804604,14 L15.8471271,18 L20.1528729,18 L20.8195396,14 Z M22.8471271,14 L27.6125741,14 L27.6125741,14 L26.2792408,18 L22.1804604,18 L22.8471271,14 Z M21.1528729,12 L14.8471271,12 L14.8471271,12 L14.1804604,8 L21.8195396,8 L21.1528729,12 Z M23.1804604,12 L28.2792408,12 L28.2792408,12 L29.6125741,8 L23.8471271,8 L23.1804604,12 Z M13.1528729,14 L8.47703296,14 L10.077033,18 L10.077033,18 L13.8195396,18 L13.1528729,14 Z M12.8195396,12 L7.67703296,12 L6.07703296,8 L12.1528729,8 L12.8195396,12 L12.8195396,12 Z M31.7207592,8 L32,8 L32,6 L31,6 L5.27703296,6 L5.27703296,6 L4,2.8074176 L4,2 L3,2 L1,2 L0,2 L0,4 L1,4 L2.32296704,4 L9.78931928,22.6658806 L9.78931928,22.6658806 C8.71085924,23.3823847 8,24.6081773 8,26 C8,28.209139 9.790861,30 12,30 C14.209139,30 16,28.209139 16,26 C16,25.2714257 15.8052114,24.5883467 15.4648712,24 L22.5351288,24 C22.1947886,24.5883467 22,25.2714257 22,26 C22,28.209139 23.790861,30 26,30 C28.209139,30 30,28.209139 30,26 C30,23.790861 28.209139,22 26,22 L11.677033,22 L10.877033,20 L27,20 L28,20 L28,19.1622777 L31.7207592,8 L31.7207592,8 Z M26,28 C27.1045695,28 28,27.1045695 28,26 C28,24.8954305 27.1045695,24 26,24 C24.8954305,24 24,24.8954305 24,26 C24,27.1045695 24.8954305,28 26,28 Z M12,28 C13.1045695,28 14,27.1045695 14,26 C14,24.8954305 13.1045695,24 12,24 C10.8954305,24 10,24.8954305 10,26 C10,27.1045695 10.8954305,28 12,28 Z"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/usercontext-circle.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="32" height="32" viewBox="0 0 32 32">
+  <circle fill="context-fill" cx="16" cy="16" r="16"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/usercontext-dollar.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="32" height="32" viewBox="0 0 32 32">
+  <path fill="context-fill" d="M17.3857868,14.0527919 C14.2304569,13.0862944 13.4913706,12.4609137 13.4913706,11.0964467 C13.4913706,9.61827411 14.7137056,8.85076142 16.4192893,8.85076142 C17.9827411,8.85076142 19.3187817,9.33401015 20.5979695,10.4994924 L22.4456853,8.42436548 C21.1664975,7.20203046 19.3187819,6.26535905 17,6.00952148 L17,2 L15,2 L15,6.00952148 C12.3827412,6.43591742 9.76751269,8.53807107 9.76751269,11.3238579 C9.76751269,14.1664975 11.4730964,15.786802 15.4812183,17.0091371 C18.4375635,17.9187817 19.2335025,18.6294416 19.2335025,20.2213198 C19.2335025,22.0690355 17.7553299,23.035533 15.7370558,23.035533 C13.7756345,23.035533 12.2406091,22.3248731 10.9329949,21.1025381 L9,23.2345178 C10.4213198,24.6274112 12.8659899,25.8324934 15,26.0030518 L15,30 L17,30 L17,26.0030518 C20.7116753,25.4060974 22.9857868,22.893401 22.9857868,20.022335 C22.9857868,16.4690355 20.7116751,15.1045685 17.3857868,14.0527919 Z"/>
+</svg>
+
new file mode 100644
--- /dev/null
+++ b/browser/base/content/usercontext-fingerprint.svg
@@ -0,0 +1,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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="32" height="32" viewBox="0 0 32 32">
+  <path fill="context-fill" d="M7.17741905,12 C7.10965537,12 7.041327,11.9953181 6.97243393,11.985018 C6.33263187,11.8918489 5.90515601,11.3862071 6.01809547,10.8552833 C7.41798011,4.26321358 12.2613889,2.57493207 15.0238882,2.15590491 C19.6448063,1.45690206 24.3408291,3.21541158 25.8344535,5.29743816 C26.1664955,5.76047488 25.9835336,6.35881757 25.4244832,6.63364321 C24.8654329,6.9098734 24.1437497,6.75583996 23.8122724,6.29327142 C22.8923805,5.01043967 19.1749781,3.51130562 15.4479759,4.07406612 C12.8080159,4.474834 9.43056132,6.03623689 8.33561323,11.1942506 C8.23453242,11.666651 7.73816348,12 7.17741905,12 Z M16.63127,26 C16.1452186,26 15.6509104,25.9658335 15.147795,25.8938767 C10.637921,25.257137 6.71207921,21.8114952 6.01575422,17.8807924 C5.91171832,17.2932317 6.33391695,16.7382846 6.95813239,16.6404441 C7.58454965,16.5343208 8.17298555,16.9406954 8.27757192,17.5272206 C8.80876054,20.5255916 11.9766264,23.26409 15.4885263,23.7610576 C17.3975027,24.02766 20.959494,23.8221432 23.3220449,19.3789425 C24.4625867,17.2331815 23.0049831,11.881462 19.9521622,9.34692739 C18.2380468,7.92384005 16.4573263,7.76905536 14.6628445,8.89499751 C13.26469,9.77142052 11.8070864,12.2857658 11.8665355,14.6287608 C11.9127737,16.4835887 12.8386382,17.9325598 14.6171568,18.9363308 C15.2210054,19.2764429 16.9411759,19.4933486 17.9424527,18.8296898 C18.7257495,18.3104622 18.9591422,17.2761485 18.6365758,15.7583267 C18.3822659,14.5650869 17.2219077,12.4452096 16.6664991,12.3711821 C16.6692513,12.3722175 16.4666841,12.4312324 16.1276041,12.9095636 C15.8545786,13.2936782 15.58981,14.7297074 15.9476054,15.3581643 C16.0142104,15.4761941 16.0725586,15.5465978 16.3202632,15.5465978 C16.9532859,15.5465978 17.46686,16.0290705 17.46686,16.6249139 C17.46686,17.2207573 16.9543868,17.7042653 16.3213641,17.7042653 C15.2644914,17.7042653 14.4140391,17.2336992 13.9268868,16.3774655 C13.1083609,14.9388479 13.5536787,12.6548678 14.2202791,11.7137354 C15.2540327,10.2564816 16.3631986,10.1151564 17.1123672,10.2564816 C19.7066595,10.7389543 20.8763754,15.2908666 20.8857331,15.3359043 C21.5303153,18.3648181 20.3594985,19.8665919 19.264094,20.593407 C17.4151172,21.8192603 14.6920186,21.493643 13.4380832,20.7859819 C10.3280151,19.0310652 9.62013053,16.497566 9.5744428,14.6805283 C9.49022326,11.3643051 11.4779146,8.30018945 13.391845,7.10021984 C16.0417332,5.43848454 18.9877658,5.66781436 21.4714167,7.72919442 C25.1176276,10.7565552 27.0871539,17.1229168 25.3746898,20.3433702 C23.4326862,23.9950465 20.2983981,26 16.63127,26 Z M16.0845157,30 C14.9348455,30 13.9050564,29.8557557 13.0394288,29.6610017 C10.2114238,29.0257442 7.58700058,27.4599412 6.18892823,25.5735955 C5.84440518,25.1078371 5.98426642,24.4803503 6.50105099,24.1700066 C7.01675554,23.8596629 7.71552172,23.986423 8.06112477,24.4507244 C9.89498097,26.9252176 15.9397944,29.9781448 22.2508301,26.1937972 C22.7676147,25.8844249 23.4658409,26.0087566 23.8109039,26.474515 C24.155427,26.9397877 24.0161057,27.5672745 23.4993212,27.8776182 C20.7987573,29.4963593 18.2315746,30 16.0845157,30 Z"/>
+</svg>
+
deleted file mode 100644
--- a/browser/base/content/usercontext.svg
+++ /dev/null
@@ -1,23 +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/. -->
-<svg xmlns="http://www.w3.org/2000/svg"
-     xmlns:xlink="http://www.w3.org/1999/xlink"
-     width="32" height="32" viewBox="0 0 32 32">
-  <style>
-    path, circle {
-      fill: menutext;
-    }
-    path:not(:target),
-    circle:not(:target) {
-      display: none;
-    }
-  </style>
-  <path id="dollar" d="M17.3857868,14.0527919 C14.2304569,13.0862944 13.4913706,12.4609137 13.4913706,11.0964467 C13.4913706,9.61827411 14.7137056,8.85076142 16.4192893,8.85076142 C17.9827411,8.85076142 19.3187817,9.33401015 20.5979695,10.4994924 L22.4456853,8.42436548 C21.1664975,7.20203046 19.3187819,6.26535905 17,6.00952148 L17,2 L15,2 L15,6.00952148 C12.3827412,6.43591742 9.76751269,8.53807107 9.76751269,11.3238579 C9.76751269,14.1664975 11.4730964,15.786802 15.4812183,17.0091371 C18.4375635,17.9187817 19.2335025,18.6294416 19.2335025,20.2213198 C19.2335025,22.0690355 17.7553299,23.035533 15.7370558,23.035533 C13.7756345,23.035533 12.2406091,22.3248731 10.9329949,21.1025381 L9,23.2345178 C10.4213198,24.6274112 12.8659899,25.8324934 15,26.0030518 L15,30 L17,30 L17,26.0030518 C20.7116753,25.4060974 22.9857868,22.893401 22.9857868,20.022335 C22.9857868,16.4690355 20.7116751,15.1045685 17.3857868,14.0527919 Z"/>
-  <path id="briefcase" fill-rule="evenodd" d="M22,9.99887085 L21.635468,10 L29.0034652,10 C29.5538362,10 30,10.4449463 30,10.9933977 L30,27.0066023 C30,27.5552407 29.5601869,28 29.0034652,28 L2.99653482,28 C2.44616384,28 2,27.5550537 2,27.0066023 L2,10.9933977 C2,10.4447593 2.43981314,10 2.99653482,10 L8,10 L8,7.99922997 C8,5.79051625 10.0426627,4 12.5635454,4 L19.4364546,4 C21.9568311,4 24,5.79246765 24,7.99922997 L24,9.99267578 L22,9.99887085 L22,10 L10,10 L10,7.99922997 C10,6.89421235 11.0713286,6 12.3917227,6 L19.6082773,6 C20.9273761,6 22,6.89552665 22,7.99922997 L22,9.99887085 Z"/>
-  <path id="fingerprint" d="M7.17741905,12 C7.10965537,12 7.041327,11.9953181 6.97243393,11.985018 C6.33263187,11.8918489 5.90515601,11.3862071 6.01809547,10.8552833 C7.41798011,4.26321358 12.2613889,2.57493207 15.0238882,2.15590491 C19.6448063,1.45690206 24.3408291,3.21541158 25.8344535,5.29743816 C26.1664955,5.76047488 25.9835336,6.35881757 25.4244832,6.63364321 C24.8654329,6.9098734 24.1437497,6.75583996 23.8122724,6.29327142 C22.8923805,5.01043967 19.1749781,3.51130562 15.4479759,4.07406612 C12.8080159,4.474834 9.43056132,6.03623689 8.33561323,11.1942506 C8.23453242,11.666651 7.73816348,12 7.17741905,12 Z M16.63127,26 C16.1452186,26 15.6509104,25.9658335 15.147795,25.8938767 C10.637921,25.257137 6.71207921,21.8114952 6.01575422,17.8807924 C5.91171832,17.2932317 6.33391695,16.7382846 6.95813239,16.6404441 C7.58454965,16.5343208 8.17298555,16.9406954 8.27757192,17.5272206 C8.80876054,20.5255916 11.9766264,23.26409 15.4885263,23.7610576 C17.3975027,24.02766 20.959494,23.8221432 23.3220449,19.3789425 C24.4625867,17.2331815 23.0049831,11.881462 19.9521622,9.34692739 C18.2380468,7.92384005 16.4573263,7.76905536 14.6628445,8.89499751 C13.26469,9.77142052 11.8070864,12.2857658 11.8665355,14.6287608 C11.9127737,16.4835887 12.8386382,17.9325598 14.6171568,18.9363308 C15.2210054,19.2764429 16.9411759,19.4933486 17.9424527,18.8296898 C18.7257495,18.3104622 18.9591422,17.2761485 18.6365758,15.7583267 C18.3822659,14.5650869 17.2219077,12.4452096 16.6664991,12.3711821 C16.6692513,12.3722175 16.4666841,12.4312324 16.1276041,12.9095636 C15.8545786,13.2936782 15.58981,14.7297074 15.9476054,15.3581643 C16.0142104,15.4761941 16.0725586,15.5465978 16.3202632,15.5465978 C16.9532859,15.5465978 17.46686,16.0290705 17.46686,16.6249139 C17.46686,17.2207573 16.9543868,17.7042653 16.3213641,17.7042653 C15.2644914,17.7042653 14.4140391,17.2336992 13.9268868,16.3774655 C13.1083609,14.9388479 13.5536787,12.6548678 14.2202791,11.7137354 C15.2540327,10.2564816 16.3631986,10.1151564 17.1123672,10.2564816 C19.7066595,10.7389543 20.8763754,15.2908666 20.8857331,15.3359043 C21.5303153,18.3648181 20.3594985,19.8665919 19.264094,20.593407 C17.4151172,21.8192603 14.6920186,21.493643 13.4380832,20.7859819 C10.3280151,19.0310652 9.62013053,16.497566 9.5744428,14.6805283 C9.49022326,11.3643051 11.4779146,8.30018945 13.391845,7.10021984 C16.0417332,5.43848454 18.9877658,5.66781436 21.4714167,7.72919442 C25.1176276,10.7565552 27.0871539,17.1229168 25.3746898,20.3433702 C23.4326862,23.9950465 20.2983981,26 16.63127,26 Z M16.0845157,30 C14.9348455,30 13.9050564,29.8557557 13.0394288,29.6610017 C10.2114238,29.0257442 7.58700058,27.4599412 6.18892823,25.5735955 C5.84440518,25.1078371 5.98426642,24.4803503 6.50105099,24.1700066 C7.01675554,23.8596629 7.71552172,23.986423 8.06112477,24.4507244 C9.89498097,26.9252176 15.9397944,29.9781448 22.2508301,26.1937972 C22.7676147,25.8844249 23.4658409,26.0087566 23.8109039,26.474515 C24.155427,26.9397877 24.0161057,27.5672745 23.4993212,27.8776182 C20.7987573,29.4963593 18.2315746,30 16.0845157,30 Z"/>
-  <path id="cart" fill-rule="evenodd" d="M20.8195396,14 L15.1804604,14 L15.1804604,14 L15.8471271,18 L20.1528729,18 L20.8195396,14 Z M22.8471271,14 L27.6125741,14 L27.6125741,14 L26.2792408,18 L22.1804604,18 L22.8471271,14 Z M21.1528729,12 L14.8471271,12 L14.8471271,12 L14.1804604,8 L21.8195396,8 L21.1528729,12 Z M23.1804604,12 L28.2792408,12 L28.2792408,12 L29.6125741,8 L23.8471271,8 L23.1804604,12 Z M13.1528729,14 L8.47703296,14 L10.077033,18 L10.077033,18 L13.8195396,18 L13.1528729,14 Z M12.8195396,12 L7.67703296,12 L6.07703296,8 L12.1528729,8 L12.8195396,12 L12.8195396,12 Z M31.7207592,8 L32,8 L32,6 L31,6 L5.27703296,6 L5.27703296,6 L4,2.8074176 L4,2 L3,2 L1,2 L0,2 L0,4 L1,4 L2.32296704,4 L9.78931928,22.6658806 L9.78931928,22.6658806 C8.71085924,23.3823847 8,24.6081773 8,26 C8,28.209139 9.790861,30 12,30 C14.209139,30 16,28.209139 16,26 C16,25.2714257 15.8052114,24.5883467 15.4648712,24 L22.5351288,24 C22.1947886,24.5883467 22,25.2714257 22,26 C22,28.209139 23.790861,30 26,30 C28.209139,30 30,28.209139 30,26 C30,23.790861 28.209139,22 26,22 L11.677033,22 L10.877033,20 L27,20 L28,20 L28,19.1622777 L31.7207592,8 L31.7207592,8 Z M26,28 C27.1045695,28 28,27.1045695 28,26 C28,24.8954305 27.1045695,24 26,24 C24.8954305,24 24,24.8954305 24,26 C24,27.1045695 24.8954305,28 26,28 Z M12,28 C13.1045695,28 14,27.1045695 14,26 C14,24.8954305 13.1045695,24 12,24 C10.8954305,24 10,24.8954305 10,26 C10,27.1045695 10.8954305,28 12,28 Z"/>
-  <circle id="circle" r="16" cx="16" cy="16" fill-rule="evenodd" />
-</svg>
-
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -133,17 +133,21 @@ browser.jar:
         content/browser/sanitizeDialog.js             (content/sanitizeDialog.js)
         content/browser/sanitizeDialog.css            (content/sanitizeDialog.css)
         content/browser/contentSearchUI.js            (content/contentSearchUI.js)
         content/browser/contentSearchUI.css           (content/contentSearchUI.css)
         content/browser/tabbrowser.css                (content/tabbrowser.css)
         content/browser/tabbrowser.xml                (content/tabbrowser.xml)
 *       content/browser/urlbarBindings.xml            (content/urlbarBindings.xml)
         content/browser/utilityOverlay.js             (content/utilityOverlay.js)
-        content/browser/usercontext.svg               (content/usercontext.svg)
+        content/browser/usercontext-briefcase.svg     (content/usercontext-briefcase.svg)
+        content/browser/usercontext-cart.svg          (content/usercontext-cart.svg)
+        content/browser/usercontext-circle.svg        (content/usercontext-circle.svg)
+        content/browser/usercontext-dollar.svg        (content/usercontext-dollar.svg)
+        content/browser/usercontext-fingerprint.svg   (content/usercontext-fingerprint.svg)
         content/browser/web-panels.js                 (content/web-panels.js)
 *       content/browser/web-panels.xul                (content/web-panels.xul)
         content/browser/webext-panels.js              (content/webext-panels.js)
 *       content/browser/webext-panels.xul             (content/webext-panels.xul)
 *       content/browser/baseMenuOverlay.xul           (content/baseMenuOverlay.xul)
         content/browser/nsContextMenu.js              (content/nsContextMenu.js)
 # XXX: We should exclude this one as well (bug 71895)
 *       content/browser/hiddenWindow.xul              (content/hiddenWindow.xul)
--- a/browser/components/contextualidentity/content/usercontext.css
+++ b/browser/components/contextualidentity/content/usercontext.css
@@ -34,33 +34,33 @@
 }
 
 [data-identity-color="purple"] {
   --identity-tab-color: #7a2f7a;
   --identity-icon-color: #7a2f7a;
 }
 
 [data-identity-icon="fingerprint"] {
-  --identity-icon: url("chrome://browser/content/usercontext.svg#fingerprint");
+  --identity-icon: url("chrome://browser/content/usercontext-fingerprint.svg");
 }
 
 [data-identity-icon="briefcase"] {
-  --identity-icon: url("chrome://browser/content/usercontext.svg#briefcase");
+  --identity-icon: url("chrome://browser/content/usercontext-briefcase.svg");
 }
 
 [data-identity-icon="dollar"] {
-  --identity-icon: url("chrome://browser/content/usercontext.svg#dollar");
+  --identity-icon: url("chrome://browser/content/usercontext-dollar.svg");
 }
 
 [data-identity-icon="cart"] {
-  --identity-icon: url("chrome://browser/content/usercontext.svg#cart");
+  --identity-icon: url("chrome://browser/content/usercontext-cart.svg");
 }
 
 [data-identity-icon="circle"] {
-  --identity-icon: url("chrome://browser/content/usercontext.svg#circle");
+  --identity-icon: url("chrome://browser/content/usercontext-circle.svg");
 }
 
 #userContext-indicator {
   height: 16px;
   width: 16px;
 }
 
 #userContext-label {
@@ -78,14 +78,14 @@
   background-repeat: no-repeat;
 }
 
 .userContext-icon,
 .menuitem-iconic[data-usercontextid] > .menu-iconic-left > .menu-iconic-icon,
 .subviewbutton[usercontextid] > .toolbarbutton-icon,
 #userContext-indicator {
   background-image: var(--identity-icon);
-  filter: url(chrome://global/skin/filters.svg#fill);
+  -moz-context-properties: fill;
   fill: var(--identity-icon-color);
   background-size: contain;
   background-repeat: no-repeat;
   background-position: center center;
 }
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -213,20 +213,22 @@ CustomizeMode.prototype = {
     // Exiting; want to re-enter once we've done that.
     if (this._handler.isExitingCustomizeMode) {
       log.debug("Attempted to enter while we're in the middle of exiting. " +
                 "We'll exit after we've entered");
       return;
     }
 
     if (!gTab) {
-      this.setTab(this.browser.loadOneTab("about:blank",
-                                          { inBackground: false,
-                                            forceNotRemote: true,
-                                            skipAnimation: true }));
+      this.setTab(this.browser.loadOneTab("about:blank", {
+        inBackground: false,
+        forceNotRemote: true,
+        skipAnimation: true,
+        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+      }));
       return;
     }
     if (!gTab.selected) {
       // This will force another .enter() to be called via the
       // onlocationchange handler of the tabbrowser, so we return early.
       gTab.ownerGlobal.gBrowser.selectedTab = gTab;
       return;
     }
--- a/browser/components/customizableui/PanelMultiView.jsm
+++ b/browser/components/customizableui/PanelMultiView.jsm
@@ -242,16 +242,21 @@ this.PanelMultiView = class {
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewContainer");
     this._mainViewContainer =
       document.getAnonymousElementByAttribute(this.node, "anonid", "mainViewContainer");
     this._subViews =
       document.getAnonymousElementByAttribute(this.node, "anonid", "subViews");
     this._viewStack =
       document.getAnonymousElementByAttribute(this.node, "anonid", "viewStack");
 
+    XPCOMUtils.defineLazyGetter(this, "_panelViewCache", () => {
+      let viewCacheId = this.node.getAttribute("viewCacheId");
+      return viewCacheId ? document.getElementById(viewCacheId) : null;
+    });
+
     this._panel.addEventListener("popupshowing", this);
     this._panel.addEventListener("popuphidden", this);
     this._panel.addEventListener("popupshown", this);
     if (this.panelViews) {
       let cs = window.getComputedStyle(document.documentElement);
       // Set CSS-determined attributes now to prevent a layout flush when we do
       // it when transitioning between panels.
       this._dir = cs.direction;
@@ -283,47 +288,76 @@ this.PanelMultiView = class {
       Object.defineProperty(this.node, method, {
         enumerable: true,
         value: (...args) => this[method](...args)
       });
     });
   }
 
   destructor() {
+    // Guard against re-entrancy.
+    if (!this.node)
+      return;
+
     if (this._mainView) {
-      this._mainView.removeAttribute("mainview");
+      let mainView = this._mainView;
+      if (this._panelViewCache)
+        this._panelViewCache.appendChild(mainView);
+      mainView.removeAttribute("mainview");
     }
+    if (this._subViews)
+      this._moveOutKids(this._subViews);
+
     if (this.panelViews) {
+      this._moveOutKids(this._viewStack);
       this.panelViews.clear();
     } else {
       this._clickCapturer.removeEventListener("click", this);
     }
     this._panel.removeEventListener("popupshowing", this);
     this._panel.removeEventListener("popupshown", this);
     this._panel.removeEventListener("popuphidden", this);
     this.node = this._clickCapturer = this._viewContainer = this._mainViewContainer =
-      this._subViews = this._viewStack = this.__dwu = null;
+      this._subViews = this._viewStack = this.__dwu = this._panelViewCache = null;
+  }
+
+  /**
+   * Remove any child subviews into the panelViewCache, to ensure
+   * they remain usable even if this panelmultiview instance is removed
+   * from the DOM.
+   * @param viewNodeContainer the container from which to remove subviews
+   */
+  _moveOutKids(viewNodeContainer) {
+    if (!this._panelViewCache)
+      return;
+
+    // Node.children and Node.childNodes is live to DOM changes like the
+    // ones we're about to do, so iterate over a static copy:
+    let subviews = Array.from(viewNodeContainer.childNodes);
+    for (let subview of subviews) {
+      // XBL lists the 'children' XBL element explicitly. :-(
+      if (subview.nodeName != "children")
+        this._panelViewCache.appendChild(subview);
+    }
   }
 
   goBack(target) {
     let [current, previous] = this.panelViews.back();
     return this.showSubView(current, target, previous);
   }
 
   /**
-   * Checks whether it is possible to navigate backwards currently.
-   * Since the visibility of the back button is dependent - right now - on the
-   * fact that there's a view title set, we use that heuristic to determine this
-   * capability.
+   * Checks whether it is possible to navigate backwards currently. Returns
+   * false if this is the panelmultiview's mainview, true otherwise.
    *
    * @param  {panelview} view View to check, defaults to the currently active view.
    * @return {Boolean}
    */
   _canGoBack(view = this._currentSubView) {
-    return !!view.getAttribute("title");
+    return view != this._mainView;
   }
 
   setMainView(aNewMainView) {
     if (this._mainView) {
       if (!this.panelViews)
         this._subViews.appendChild(this._mainView);
       this._mainView.removeAttribute("mainview");
     }
@@ -355,17 +389,17 @@ this.PanelMultiView = class {
           this.node.setAttribute("viewtype", "main");
         });
       }
 
       this._shiftMainView();
     }
   }
 
-  showSubView(aViewId, aAnchor, aPreviousView, aAdopted = false) {
+  showSubView(aViewId, aAnchor, aPreviousView) {
     const {document, window} = this;
     return (async () => {
       // Support passing in the node directly.
       let viewNode = typeof aViewId == "string" ? this.node.querySelector("#" + aViewId) : aViewId;
       if (!viewNode) {
         viewNode = document.getElementById(aViewId);
         if (viewNode) {
           if (this.panelViews) {
@@ -410,17 +444,17 @@ this.PanelMultiView = class {
       let detail = {
         blockers: new Set(),
         addBlocker(aPromise) {
           this.blockers.add(aPromise);
         },
       };
 
       // Make sure that new panels always have a title set.
-      if (this.panelViews && aAdopted && aAnchor) {
+      if (this.panelViews && aAnchor) {
         if (aAnchor && !viewNode.hasAttribute("title"))
           viewNode.setAttribute("title", aAnchor.getAttribute("label"));
         viewNode.classList.add("PanelUI-subView");
         let custWidget = CustomizableWidgets.find(widget => widget.viewId == viewNode.id);
         if (custWidget) {
           if (custWidget.onInit)
             custWidget.onInit(aAnchor);
           custWidget.onViewShowing({ target: aAnchor, detail });
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -4,17 +4,18 @@
 
 <panel id="PanelUI-popup"
        role="group"
        type="arrow"
        hidden="true"
        flip="slide"
        position="bottomcenter topright"
        noautofocus="true">
-  <panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
+  <panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView"
+                  viewCacheId="appMenu-viewCache">
     <panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
       <vbox id="PanelUI-contents-scroller">
         <vbox id="PanelUI-contents" class="panelUI-grid"/>
       </vbox>
 
       <footer id="PanelUI-footer">
         <vbox id="PanelUI-footer-addons"></vbox>
         <toolbarbutton class="panel-banner-item"
@@ -498,17 +499,19 @@
 <panel id="appMenu-popup"
        class="cui-widget-panel"
        role="group"
        type="arrow"
        hidden="true"
        flip="slide"
        position="bottomcenter topright"
        noautofocus="true">
-  <photonpanelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView" descriptionheightworkaround="true">
+  <photonpanelmultiview id="appMenu-multiView" mainViewId="appMenu-mainView"
+                        descriptionheightworkaround="true"
+                        viewCacheId="appMenu-viewCache">
     <panelview id="appMenu-mainView" class="PanelUI-subView">
       <vbox class="panel-subview-body">
         <vbox id="appMenu-addon-banners"/>
         <toolbarbutton class="panel-banner-item"
                        label-update-available="&updateAvailable.panelUI.label;"
                        label-update-manual="&updateManual.panelUI.label;"
                        label-update-restart="&updateRestart.panelUI.label2;"
                        oncommand="PanelUI._onBannerItemSelected(event)"
@@ -561,16 +564,45 @@
                          command="cmd_copy"
                          tooltip="dynamic-shortcut-tooltip"/>
           <toolbarbutton id="appMenu-paste-button"
                          class="subviewbutton subviewbutton-iconic"
                          command="cmd_paste"
                          tooltip="dynamic-shortcut-tooltip"/>
         </toolbaritem>
         <toolbarseparator/>
+        <toolbarbutton id="appMenu-library-button"
+                       class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+                       label="&places.library.title;"
+                       closemenu="none"
+                       oncommand="PanelUI.showSubView('appMenu-libraryView', this)"/>
+        <toolbarbutton id="appMenu-addons-button"
+                       class="subviewbutton subviewbutton-iconic"
+                       label="&addons.label;"
+                       key="key_openAddons"
+                       command="Tools:Addons"
+                       />
+        <toolbarbutton id="appMenu-preferences-button"
+                       class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+                       label="&preferencesCmd2.label;"
+#else
+                       label="&preferencesCmdUnix.label;"
+#ifdef XP_MACOSX
+                       key="key_preferencesCmdMac"
+#endif
+#endif
+                       oncommand="openPreferences()"
+                       />
+        <toolbarbutton id="appMenu-customize-button"
+                       class="subviewbutton subviewbutton-iconic"
+                       label="&viewCustomizeToolbar.label;"
+                       command="cmd_CustomizeToolbars"
+                       />
+        <toolbarseparator/>
         <toolbarbutton id="appMenu-open-file-button"
                        class="subviewbutton"
                        label="&openFileCmd.label;"
                        key="openFileKb"
                        command="Browser:OpenFile"
                        />
         <toolbarbutton id="appMenu-save-file-button"
                        class="subviewbutton"
@@ -589,76 +621,74 @@
                        key="printKb"
 #ifdef XP_MACOSX
                        command="cmd_print"
 #else
                        command="cmd_printPreview"
 #endif
                        />
         <toolbarseparator/>
-        <toolbarbutton id="appMenu-library-button"
-                       class="subviewbutton subviewbutton-iconic"
-                       label="&places.library.title;"
-                       command="Browser:ShowAllBookmarks"
-                       />
-        <toolbarbutton id="appMenu-addons-button"
-                       class="subviewbutton subviewbutton-iconic"
-                       label="&addons.label;"
-                       key="key_openAddons"
-                       command="Tools:Addons"
-                       />
-        <toolbarbutton id="appMenu-preferences-button"
-                       class="subviewbutton subviewbutton-iconic"
-#ifdef XP_WIN
-                       label="&preferencesCmd2.label;"
-#else
-                       label="&preferencesCmdUnix.label;"
-#ifdef XP_MACOSX
-                       key="key_preferencesCmdMac"
-#endif
-#endif
-                       oncommand="openPreferences()"
-                       />
-        <toolbarbutton id="appMenu-customize-button"
-                       class="subviewbutton subviewbutton-iconic"
-                       label="&viewCustomizeToolbar.label;"
-                       command="cmd_CustomizeToolbars"
-                       />
-        <toolbarseparator/>
         <toolbarbutton id="appMenu-find-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&findOnCmd.label;"
                        key="key_find"
                        command="cmd_find"/>
         <toolbarbutton id="appMenu-more-button"
                        class="subviewbutton subviewbutton-nav"
                        label="&moreMenu.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('appMenu-moreView', this)"/>
         <toolbarbutton id="appMenu-developer-button"
                        class="subviewbutton subviewbutton-nav"
                        label="&webDeveloperMenu.label;"
                        closemenu="none"
-                       oncommand="PanelUI.showSubView('PanelUI-developer', this, null, true)"/>
+                       oncommand="PanelUI.showSubView('PanelUI-developer', this)"/>
         <toolbarbutton id="appMenu-help-button"
                        class="subviewbutton subviewbutton-iconic subviewbutton-nav"
                        label="&appMenuHelp.label;"
                        closemenu="none"
-                       oncommand="PanelUI.showSubView('PanelUI-helpView', this, null, true)"/>
+                       oncommand="PanelUI.showSubView('PanelUI-helpView', this)"/>
+#ifndef XP_MACOSX
+        <toolbarseparator/>
+        <toolbarbutton id="appMenu-quit-button"
+                       class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+                       label="&quitApplicationCmdWin2.label;"
+                       tooltiptext="&quitApplicationCmdWin2.tooltip;"
+#else
+                       label="&quitApplicationCmd.label;"
+#endif
+                       key="key_quitApplication"
+                       command="cmd_quitApplication"/>
+#endif
       </vbox>
     </panelview>
     <panelview id="appMenu-moreView" title="&moreMenu.label;" class="PanelUI-subView">
       <vbox class="panel-subview-body">
         <toolbarbutton id="appMenu-characterencoding-button"
                        class="subviewbutton subviewbutton-nav"
                        label="&charsetMenu2.label;"
                        closemenu="none"
-                       oncommand="PanelUI.showSubView('PanelUI-characterEncodingView', this, null, true)"/>
+                       oncommand="PanelUI.showSubView('PanelUI-characterEncodingView', this)"/>
         <toolbarbutton id="appMenu-workoffline-button"
                        class="subviewbutton"
                        label="&goOfflineCmd.label;"
                        type="checkbox"
                        observes="workOfflineMenuitemState"
                        oncommand="BrowserOffline.toggleOfflineStatus();"/>
       </vbox>
     </panelview>
+    <panelview id="appMenu-libraryView" class="PanelUI-subView">
+      <vbox class="panel-subview-body">
+        <toolbarbutton id="appMenu-library-history-button"
+                       class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+                       label="&historyMenu.label;"
+                       closemenu="none"
+                       oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
+        <toolbarbutton id="appMenu-library-remotetabs-button"
+                       class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+                       label="&appMenuRemoteTabs.label;"
+                       closemenu="none"
+                       oncommand="PanelUI.showSubView('PanelUI-remotetabs', this)"/>
+      </vbox>
+    </panelview>
   </photonpanelmultiview>
 </panel>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -439,32 +439,32 @@ const PanelUI = {
 
   /**
    * Shows a subview in the panel with a given ID.
    *
    * @param aViewId the ID of the subview to show.
    * @param aAnchor the element that spawned the subview.
    * @param aPlacementArea the CustomizableUI area that aAnchor is in.
    */
-  async showSubView(aViewId, aAnchor, aPlacementArea, aAdopted = false) {
+  async showSubView(aViewId, aAnchor, aPlacementArea) {
     this._ensureEventListenersAdded();
     let viewNode = document.getElementById(aViewId);
     if (!viewNode) {
       Cu.reportError("Could not show panel subview with id: " + aViewId);
       return;
     }
 
     if (!aAnchor) {
       Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
       return;
     }
 
     let container = aAnchor.closest("panelmultiview,photonpanelmultiview");
     if (container) {
-      container.showSubView(aViewId, aAnchor, null, aAdopted);
+      container.showSubView(aViewId, aAnchor);
     } else if (!aAnchor.open) {
       aAnchor.open = true;
 
       let tempPanel = document.createElement("panel");
       tempPanel.setAttribute("type", "arrow");
       tempPanel.setAttribute("id", "customizationui-widget-panel");
       tempPanel.setAttribute("class", "cui-widget-panel");
       tempPanel.setAttribute("viewId", aViewId);
@@ -475,19 +475,24 @@ const PanelUI = {
         tempPanel.setAttribute("animate", "false");
       }
       tempPanel.setAttribute("context", "");
       document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
       // If the view has a footer, set a convenience class on the panel.
       tempPanel.classList.toggle("cui-widget-panelWithFooter",
                                  viewNode.querySelector(".panel-subview-footer"));
 
-      let multiView = document.createElement("panelmultiview");
+      let multiView = document.createElement(gPhotonStructure ? "photonpanelmultiview" : "panelmultiview");
       multiView.setAttribute("id", "customizationui-widget-multiview");
       multiView.setAttribute("nosubviews", "true");
+      multiView.setAttribute("viewCacheId", "appMenu-viewCache");
+      if (gPhotonStructure) {
+        multiView.setAttribute("mainViewId", viewNode.id);
+        multiView.appendChild(viewNode);
+      }
       tempPanel.appendChild(multiView);
       multiView.setAttribute("mainViewIsSubView", "true");
       multiView.setMainView(viewNode);
       viewNode.classList.add("cui-widget-panelview");
 
       let viewShown = false;
       let panelRemover = () => {
         viewNode.classList.remove("cui-widget-panelview");
@@ -495,17 +500,19 @@ const PanelUI = {
           CustomizableUI.removePanelCloseListeners(tempPanel);
           tempPanel.removeEventListener("popuphidden", panelRemover);
 
           let evt = new CustomEvent("ViewHiding", {detail: viewNode});
           viewNode.dispatchEvent(evt);
         }
         aAnchor.open = false;
 
-        this.multiView.appendChild(viewNode);
+        // Ensure we run the destructor:
+        multiView.instance.destructor();
+
         tempPanel.remove();
       };
 
       // Emit the ViewShowing event so that the widget definition has a chance
       // to lazily populate the subview with things.
       let detail = {
         blockers: new Set(),
         addBlocker(aPromise) {
--- a/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_browser_style.js
@@ -1,12 +1,14 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+requestLongerTimeout(2);
+
 async function testOptionsBrowserStyle(optionsUI, assertMessage) {
   function optionsScript() {
     browser.test.onMessage.addListener((msgName, optionsUI, assertMessage) => {
       if (msgName !== "check-style") {
         browser.test.notifyFail("options-ui-browser_style");
       }
 
       let style = window.getComputedStyle(document.getElementById("button"));
@@ -47,19 +49,19 @@ async function testOptionsBrowserStyle(o
   let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
 
   await extension.startup();
   await extension.awaitMessage("options-ui-ready");
 
   extension.sendMessage("check-style", optionsUI, assertMessage);
   await extension.awaitFinish("options-ui-browser_style");
 
-  await extension.unload();
+  await BrowserTestUtils.removeTab(tab);
 
-  await BrowserTestUtils.removeTab(tab);
+  await extension.unload();
 }
 
 add_task(async function test_options_without_setting_browser_style() {
   await testOptionsBrowserStyle({
     "page": "options.html",
   }, "Expected correct style when browser_style is excluded");
 });
 
--- a/browser/components/migration/AutoMigrate.jsm
+++ b/browser/components/migration/AutoMigrate.jsm
@@ -6,31 +6,34 @@
 
 this.EXPORTED_SYMBOLS = ["AutoMigrate"];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 
 const kAutoMigrateEnabledPref = "browser.migrate.automigrate.enabled";
 const kUndoUIEnabledPref = "browser.migrate.automigrate.ui.enabled";
 
+const kInPageUIEnabledPref = "browser.migrate.automigrate.inpage.ui.enabled";
+
 const kAutoMigrateBrowserPref = "browser.migrate.automigrate.browser";
 const kAutoMigrateImportedItemIds = "browser.migrate.automigrate.imported-items";
 
 const kAutoMigrateLastUndoPromptDateMsPref = "browser.migrate.automigrate.lastUndoPromptDateMs";
 const kAutoMigrateDaysToOfferUndoPref = "browser.migrate.automigrate.daysToOfferUndo";
 
 const kAutoMigrateUndoSurveyPref = "browser.migrate.automigrate.undo-survey";
 const kAutoMigrateUndoSurveyLocalePref = "browser.migrate.automigrate.undo-survey-locales";
 
 const kNotificationId = "automigration-undo";
 
 Cu.import("resource:///modules/MigrationUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
                                   "resource://gre/modules/NewTabUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
@@ -309,102 +312,125 @@ const AutoMigrate = {
     let browserId = Services.prefs.getCharPref(kAutoMigrateBrowserPref);
     if (browserId) {
       return MigrationUtils.getBrowserName(browserId);
     }
     return null;
   },
 
   /**
-   * Show the user a notification bar indicating we automatically imported
-   * their data and offering them the possibility of removing it.
+   * Decide if we need to show [the user] a prompt indicating we automatically
+   * imported their data.
    * @param target (xul:browser)
    *        The browser in which we should show the notification.
+   * @returns {Boolean} return true when need to show the prompt.
    */
-  async maybeShowUndoNotification(target) {
+  async shouldShowMigratePrompt(target) {
     if (!(await this.canUndo())) {
-      return;
+      return false;
     }
 
     // The tab might have navigated since we requested the undo state:
     let canUndoFromThisPage = ["about:home", "about:newtab"].includes(target.currentURI.spec);
     if (!canUndoFromThisPage ||
         !Preferences.get(kUndoUIEnabledPref, false)) {
-      return;
-    }
-
-    let win = target.ownerGlobal;
-    let notificationBox = win.gBrowser.getNotificationBox(target);
-    if (!notificationBox || notificationBox.getNotificationWithValue(kNotificationId)) {
-      return;
+      return false;
     }
 
     // At this stage we're committed to show the prompt - unless we shouldn't,
     // in which case we remove the undo prefs (which will cause canUndo() to
     // return false from now on.):
-    if (!this.shouldStillShowUndoPrompt()) {
+    if (this.isMigratePromptExpired()) {
       this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_EXPIRED);
       this._removeNotificationBars();
-      return;
+      return false;
     }
 
+    let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0);
+    Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays);
+
+    return true;
+  },
+
+  /**
+   * Return the message that denotes the user data is migrated from the other browser.
+   * @returns {String} imported message with the brand and the browser name
+   */
+  getUndoMigrationMessage() {
     let browserName = this.getBrowserUsedForMigration();
     if (!browserName) {
       browserName = MigrationUtils.getLocalizedString("automigration.undo.unknownbrowser");
     }
-    const kMessageId = "automigration.undo.message." +
+    const kMessageId = "automigration.undo.message2." +
                       Preferences.get(kAutoMigrateImportedItemIds, "all");
     const kBrandShortName = gBrandBundle.GetStringFromName("brandShortName");
-    let message = MigrationUtils.getLocalizedString(kMessageId,
-                                                    [browserName, kBrandShortName]);
+    return MigrationUtils.getLocalizedString(kMessageId,
+                                             [kBrandShortName, browserName]);
+  },
 
+  /**
+   * Show the user a notification bar indicating we automatically imported
+   * their data and offering them the possibility of removing it.
+   * @param target (xul:browser)
+   *        The browser in which we should show the notification.
+   */
+  showUndoNotificationBar(target) {
+    let isInPage = Preferences.get(kInPageUIEnabledPref, false);
+    let win = target.ownerGlobal;
+    let notificationBox = win.gBrowser.getNotificationBox(target);
+    if (isInPage || !notificationBox || notificationBox.getNotificationWithValue(kNotificationId)) {
+      return;
+    }
+    let message = this.getUndoMigrationMessage();
     let buttons = [
       {
         label: MigrationUtils.getLocalizedString("automigration.undo.keep2.label"),
         accessKey: MigrationUtils.getLocalizedString("automigration.undo.keep2.accesskey"),
         callback: () => {
-          this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_REJECTED);
+          this.keepAutoMigration();
           this._removeNotificationBars();
         },
       },
       {
         label: MigrationUtils.getLocalizedString("automigration.undo.dontkeep2.label"),
         accessKey: MigrationUtils.getLocalizedString("automigration.undo.dontkeep2.accesskey"),
         callback: () => {
-          this._maybeOpenUndoSurveyTab(win);
-          this.undo();
+          this.undoAutoMigration(win);
         },
       },
     ];
     notificationBox.appendNotification(
       message, kNotificationId, null, notificationBox.PRIORITY_INFO_HIGH, buttons
     );
-    let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 0);
-    Services.telemetry.getHistogramById("FX_STARTUP_MIGRATION_UNDO_OFFERED").add(4 - remainingDays);
   },
 
-  shouldStillShowUndoPrompt() {
+
+  /**
+   * Return true if we have shown the prompt to user several days.
+   * (defined in kAutoMigrateDaysToOfferUndoPref)
+   */
+  isMigratePromptExpired() {
     let today = new Date();
     // Round down to midnight:
     today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
     // We store the unix timestamp corresponding to midnight on the last day
     // on which we prompted. Fetch that and compare it to today's date.
     // (NB: stored as a string because int prefs are too small for unix
     // timestamps.)
     let previousPromptDateMsStr = Preferences.get(kAutoMigrateLastUndoPromptDateMsPref, "0");
     let previousPromptDate = new Date(parseInt(previousPromptDateMsStr, 10));
     if (previousPromptDate < today) {
       let remainingDays = Preferences.get(kAutoMigrateDaysToOfferUndoPref, 4) - 1;
       Preferences.set(kAutoMigrateDaysToOfferUndoPref, remainingDays);
       Preferences.set(kAutoMigrateLastUndoPromptDateMsPref, today.valueOf().toString());
       if (remainingDays <= 0) {
-        return false;
+        return true;
       }
     }
-    return true;
+    return false;
   },
 
   UNDO_REMOVED_REASON_UNDO_USED: 0,
   UNDO_REMOVED_REASON_SYNC_SIGNIN: 1,
   UNDO_REMOVED_REASON_PASSWORD_CHANGE: 2,
   UNDO_REMOVED_REASON_BOOKMARK_CHANGE: 3,
   UNDO_REMOVED_REASON_OFFER_EXPIRED: 4,
   UNDO_REMOVED_REASON_OFFER_REJECTED: 5,
@@ -651,11 +677,27 @@ const AutoMigrate = {
     let url = Services.urlFormatter.formatURL(rawURL);
     url = url.replace("%IMPORTEDBROWSER%", encodeURIComponent(migrationBrowser));
     chromeWindow.openUILinkIn(url, "tab");
   },
 
   QueryInterface: XPCOMUtils.generateQI(
     [Ci.nsIObserver, Ci.nsINavBookmarkObserver, Ci.nsISupportsWeakReference]
   ),
+
+  /**
+   * Undo action called by the UndoNotification or by the newtab
+   * @param chromeWindow A reference to the window in which to open a link.
+   */
+  undoAutoMigration(chromeWindow) {
+    this._maybeOpenUndoSurveyTab(chromeWindow);
+    this.undo();
+  },
+
+  /**
+   * Keep the automigration result and not prompt anymore
+   */
+  keepAutoMigration() {
+    this._purgeUndoState(this.UNDO_REMOVED_REASON_OFFER_REJECTED);
+  },
 };
 
 AutoMigrate.init();
--- a/browser/components/migration/MigrationUtils.jsm
+++ b/browser/components/migration/MigrationUtils.jsm
@@ -1105,16 +1105,17 @@ this.MigrationUtils = Object.freeze({
 
   gAvailableMigratorKeys,
 
   MIGRATION_ENTRYPOINT_UNKNOWN: 0,
   MIGRATION_ENTRYPOINT_FIRSTRUN: 1,
   MIGRATION_ENTRYPOINT_FXREFRESH: 2,
   MIGRATION_ENTRYPOINT_PLACES: 3,
   MIGRATION_ENTRYPOINT_PASSWORDS: 4,
+  MIGRATION_ENTRYPOINT_NEWTAB: 5,
 
   _sourceNameToIdMapping: {
     "nothing":    1,
     "firefox":    2,
     "edge":       3,
     "ie":         4,
     "chrome":     5,
     "chromium":   6,
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -61,16 +61,18 @@ XPIDL_MODULE = 'browsercompsbase'
 
 EXTRA_PP_COMPONENTS += [
     'BrowserComponents.manifest',
 ]
 
 EXTRA_COMPONENTS += [
     'nsBrowserContentHandler.js',
     'nsBrowserGlue.js',
+    'tests/startupRecorder.js',
+    'tests/testComponents.manifest',
 ]
 
 EXTRA_JS_MODULES += [
     'distribution.js',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
     'safebrowsing/content/test/browser.ini',
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -144,17 +144,17 @@ const listeners = {
     // PLEASE KEEP THIS LIST IN SYNC WITH THE LISTENERS ADDED IN ContentPrefServiceParent.init
     "FeedConverter:addLiveBookmark": ["Feeds"],
     "WCCR:setAutoHandler": ["Feeds"],
     "webrtc:UpdateGlobalIndicators": ["webrtcUI"],
     "webrtc:UpdatingIndicators": ["webrtcUI"],
   },
 
   mm: {
-    "AboutHome:MaybeShowAutoMigrationUndoNotification": ["AboutHome"],
+    "AboutHome:MaybeShowMigrateMessage": ["AboutHome"],
     "AboutHome:RequestUpdate": ["AboutHome"],
     "Content:Click": ["ContentClick"],
     "ContentSearch": ["ContentSearch"],
     "FormValidation:ShowPopup": ["FormValidationHandler"],
     "FormValidation:HidePopup": ["FormValidationHandler"],
     "Prompt:Open": ["RemotePrompt"],
     "Reader:ArticleGet": ["ReaderParent"],
     "Reader:FaviconRequest": ["ReaderParent"],
@@ -1704,17 +1704,17 @@ BrowserGlue.prototype = {
         return;
       this._openPreferences("sync", { origin: "doorhanger" });
     }
     AlertsService.showAlertNotification(null, title, body, true, null, clickCallback);
   },
 
   // eslint-disable-next-line complexity
   _migrateUI: function BG__migrateUI() {
-    const UI_VERSION = 46;
+    const UI_VERSION = 47;
     const BROWSER_DOCURL = "chrome://browser/content/browser.xul";
 
     let currentUIVersion;
     if (Services.prefs.prefHasUserValue("browser.migration.version")) {
       currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
     } else {
       // This is a new profile, nothing to migrate.
       Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
@@ -2007,29 +2007,40 @@ BrowserGlue.prototype = {
       const LEGACY_PREF = "browser.shell.skipDefaultBrowserCheck";
       if (Services.prefs.prefHasUserValue(LEGACY_PREF)) {
         Services.prefs.setBoolPref("browser.shell.didSkipDefaultBrowserCheckOnFirstRun",
                                    !Services.prefs.getBoolPref(LEGACY_PREF));
         Services.prefs.clearUserPref(LEGACY_PREF);
       }
     }
 
-    if (currentUIVersion < 46) {
+    // Version 46 has been replaced by 47
+    if (currentUIVersion < 47) {
       // Search suggestions are now on by default.
       // For privacy reasons, we want to respect previously made user's choice
       // regarding the feature, so if it's known reflect that choice into the
       // current pref.
       // Note that in case of downgrade/upgrade we won't guarantee anything.
       try {
-        Services.prefs.setBoolPref(
-          "browser.urlbar.suggest.searches",
-          Services.prefs.getBoolPref("browser.urlbar.searchSuggestionsChoice")
-        );
+        if (Services.prefs.prefHasUserValue("browser.urlbar.searchSuggestionsChoice")) {
+          Services.prefs.setBoolPref(
+            "browser.urlbar.suggest.searches",
+            Services.prefs.getBoolPref("browser.urlbar.searchSuggestionsChoice")
+          );
+        } else if (Services.prefs.getBoolPref("browser.urlbar.userMadeSearchSuggestionsChoice")) {
+          // If the user made a choice but searchSuggestionsChoice is not set,
+          // something went wrong in the upgrade path. For example, due to a
+          // now fixed bug, some profilespicking "no" at the opt-in bar and
+          // upgrading in the same session wouldn't mirror the pref.
+          // Users could also lack the mirrored pref due to skipping one version.
+          // In this case just fallback to the safest side and disable suggestions.
+          Services.prefs.setBoolPref("browser.urlbar.suggest.searches", false);
+        }
       } catch (ex) {
-        // The pref is not set, nothing to do.
+        // A missing pref is not a fatal error.
       }
     }
 
     // Update the migration version.
     Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
   },
 
   // ------------------------------
new file mode 100644
--- /dev/null
+++ b/browser/components/tests/startupRecorder.js
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {classes: Cc, utils: Cu, interfaces: Ci} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/**
+  * The startupRecorder component observes notifications at various stages of
+  * startup and records the set of JS components and modules that were already
+  * loaded at each of these points.
+  * The records are meant to be used by startup tests in
+  * browser/base/content/test/performance
+  * This component only exists in nightly and debug builds, it doesn't ship in
+  * our release builds.
+  */
+function startupRecorder() {
+  this.wrappedJSObject = this;
+  this.loader = Cc["@mozilla.org/moz/jsloader;1"].getService(Ci.xpcIJSModuleLoader);
+  this.data = {};
+}
+startupRecorder.prototype = {
+  classID: Components.ID("{11c095b2-e42e-4bdf-9dd0-aed87595f6a4}"),
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  record(name) {
+    this.data[name] = {
+      components: this.loader.loadedComponents(),
+      modules: this.loader.loadedModules()
+    };
+  },
+
+  observe(subject, topic, data) {
+
+    if (topic == "app-startup") {
+      // We can't ensure our observer will be called first or last, so the list of
+      // topics we observe here should avoid the topics used to trigger things
+      // during startup (eg. the topics observed by nsBrowserGlue.js).
+      let topics = [
+        "profile-do-change", // This catches stuff loaded during app-startup
+        "toplevel-window-ready", // Catches stuff from final-ui-startup
+        "widget-first-paint",
+        "sessionstore-windows-restored",
+      ];
+      for (let t of topics)
+        Services.obs.addObserver(this, t);
+      return;
+    }
+
+    Services.obs.removeObserver(this, topic);
+
+    if (topic == "sessionstore-windows-restored") {
+      // We use idleDispatch here to record the set of loaded scripts after we
+      // are fully done with startup and ready to react to user events.
+      Services.tm.mainThread.idleDispatch(
+        this.record.bind(this, "before handling user events"));
+    } else {
+      const topicsToNames = {
+        "profile-do-change": "before profile selection",
+        "toplevel-window-ready": "before opening first browser window",
+        "widget-first-paint": "before first paint",
+      };
+      this.record(topicsToNames[topic]);
+    }
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([startupRecorder]);
new file mode 100644
--- /dev/null
+++ b/browser/components/tests/testComponents.manifest
@@ -0,0 +1,5 @@
+# This component restricts its registration for the app-startup category
+# to the browser app so it doesn't get loaded in xpcshell.
+component {11c095b2-e42e-4bdf-9dd0-aed87595f6a4} startupRecorder.js
+contract @mozilla.org/test/startuprecorder;1 {11c095b2-e42e-4bdf-9dd0-aed87595f6a4}
+category app-startup startupRecorder service,@mozilla.org/test/startuprecorder;1 application={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -177,58 +177,16 @@ if (typeof Mozilla == "undefined") {
    */
   Mozilla.UITour.registerPageID = function(pageID) {
     _sendEvent("registerPageID", {
       pageID
     });
   };
 
   /**
-   * Show a global notification bar with a prompt and optional buttons.
-   *
-   * Only intended for use by Self Support.
-   *
-   * @deprecated Use Heartbeat from
-   * {@link https://wiki.mozilla.org/Firefox/Shield/Heartbeat|Shield} instead.
-   *
-   * @param {String} message - Text to show in the notification bar before an action is taken.
-   * @param {String} thankyouMessage - Text to show in the notification bar after a vote.
-   * @param {String} flowId - An identifier for this rating flow. Please note that this is only used
-   *                          to identify the notification box.
-   * @param {String} engagementURL - URL to open in a new tab once the user has engaged.
-   * @param {String} learnMoreLabel - The label of the learn more link. No link will be shown if
-   *                                  this is null.
-   * @param {String} learnMoreURL - URL to open when clicking on the learn more link. No link will be
-   *                                shown if this is an invalid URL.
-   * @param {Object} options - Options to control behavior.
-   */
-  Mozilla.UITour.showHeartbeat = function(message, thankyouMessage, flowId, engagementURL,
-					  learnMoreLabel, learnMoreURL, options) {
-    var args = {
-      message,
-      thankyouMessage,
-      flowId,
-      engagementURL,
-      learnMoreLabel,
-      learnMoreURL,
-    };
-
-    if (options) {
-      for (var option in options) {
-	if (!options.hasOwnProperty(option)) {
-	  continue;
-	}
-	args[option] = options[option];
-      }
-    }
-
-    _sendEvent("showHeartbeat", args);
-  };
-
-  /**
    * @typedef {String} Mozilla.UITour.HighlightEffect
    *
    * Specifies the effect/animation to use when highlighting UI elements.
    * @description Valid values:<ul>
    * <li>random
    * <li>wobble
    * <li>zoom
    * <li>color
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -8,17 +8,16 @@ this.EXPORTED_SYMBOLS = ["UITour"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource:///modules/RecentWindow.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ResetProfile",
@@ -32,17 +31,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
   "resource:///modules/ReaderParent.jsm");
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL      = "browser.uitour.loglevel";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
-const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
 
 const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
   "forceShowReaderIcon",
   "getConfiguration",
   "getTreatmentTag",
   "hideHighlight",
   "hideInfo",
   "hideMenu",
@@ -356,52 +354,16 @@ this.UITour = {
 
         this.addSeenPageID(data.pageID);
         this.pageIDSourceBrowsers.set(browser, data.pageID);
         this.setTelemetryBucket(data.pageID);
 
         break;
       }
 
-      case "showHeartbeat": {
-        // Validate the input parameters.
-        if (typeof data.message !== "string" || data.message === "") {
-          log.error("showHeartbeat: Invalid message specified.");
-          return false;
-        }
-
-        if (typeof data.thankyouMessage !== "string" || data.thankyouMessage === "") {
-          log.error("showHeartbeat: Invalid thank you message specified.");
-          return false;
-        }
-
-        if (typeof data.flowId !== "string" || data.flowId === "") {
-          log.error("showHeartbeat: Invalid flowId specified.");
-          return false;
-        }
-
-        if (data.engagementButtonLabel && typeof data.engagementButtonLabel != "string") {
-          log.error("showHeartbeat: Invalid engagementButtonLabel specified");
-          return false;
-        }
-
-        let heartbeatWindow = window;
-        if (data.privateWindowsOnly && !PrivateBrowsingUtils.isWindowPrivate(heartbeatWindow)) {
-          heartbeatWindow = RecentWindow.getMostRecentBrowserWindow({ private: true });
-          if (!heartbeatWindow) {
-            log.debug("showHeartbeat: No private window found");
-            return false;
-          }
-        }
-
-        // Finally show the Heartbeat UI.
-        this.showHeartbeat(heartbeatWindow, data);
-        break;
-      }
-
       case "showHighlight": {
         let targetPromise = this.getTarget(window, data.target);
         targetPromise.then(target => {
           if (!target.node) {
             log.error("UITour: Target could not be resolved: " + data.target);
             return;
           }
           let effect = undefined;
@@ -1037,347 +999,16 @@ this.UITour = {
       LightweightThemeManager.previewTheme(data);
   },
 
   resetTheme() {
     LightweightThemeManager.resetPreview();
   },
 
   /**
-   * Show the Heartbeat UI to request user feedback. This function reports back to the
-   * caller using |notify|. The notification event name reflects the current status the UI
-   * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed",
-   * "Heartbeat:LearnMore", "Heartbeat:Engaged", "Heartbeat:Voted",
-   * "Heartbeat:SurveyExpired" or "Heartbeat:WindowClosed").
-   * When a "Heartbeat:Voted" event is notified
-   * the data payload contains a |score| field which holds the rating picked by the user.
-   * Please note that input parameters are already validated by the caller.
-   *
-   * @param aChromeWindow
-   *        The chrome window that the heartbeat notification is displayed in.
-   * @param {Object} aOptions Options object.
-   * @param {String} aOptions.message
-   *        The message, or question, to display on the notification.
-   * @param {String} aOptions.thankyouMessage
-   *        The thank you message to display after user votes.
-   * @param {String} aOptions.flowId
-   *        An identifier for this rating flow. Please note that this is only used to
-   *        identify the notification box.
-   * @param {String} [aOptions.engagementButtonLabel=null]
-   *        The text of the engagement button to use instad of stars. If this is null
-   *        or invalid, rating stars are used.
-   * @param {String} [aOptions.engagementURL=null]
-   *        The engagement URL to open in a new tab once user has engaged. If this is null
-   *        or invalid, no new tab is opened.
-   * @param {String} [aOptions.learnMoreLabel=null]
-   *        The label of the learn more link. No link will be shown if this is null.
-   * @param {String} [aOptions.learnMoreURL=null]
-   *        The learn more URL to open when clicking on the learn more link. No learn more
-   *        will be shown if this is an invalid URL.
-   * @param {boolean} [aOptions.privateWindowsOnly=false]
-   *        Whether the heartbeat UI should only be targeted at a private window (if one exists).
-   *        No notifications should be fired when this is true.
-   * @param {String} [aOptions.surveyId]
-   *        An ID for the survey, reflected in the Telemetry ping.
-   * @param {Number} [aOptions.surveyVersion]
-   *        Survey's version number, reflected in the Telemetry ping.
-   * @param {boolean} [aOptions.testing]
-   *        Whether this is a test survey, reflected in the Telemetry ping.
-   */
-  showHeartbeat(aChromeWindow, aOptions) {
-    // Initialize survey state
-    let pingSent = false;
-    let surveyResults = {};
-    let surveyEndTimer = null;
-
-    /**
-     * Accumulates survey events and submits to Telemetry after the survey ends.
-     *
-     * @param {String} aEventName
-     *        Heartbeat event name
-     * @param {Object} aParams
-     *        Additional parameters and their values
-     */
-    let maybeNotifyHeartbeat = (aEventName, aParams = {}) => {
-      // Return if event occurred after the ping was sent
-      if (pingSent) {
-        log.warn("maybeNotifyHeartbeat: event occurred after ping sent:", aEventName, aParams);
-        return;
-      }
-
-      // No Telemetry from private-window-only Heartbeats
-      if (aOptions.privateWindowsOnly) {
-        return;
-      }
-
-      let ts = Date.now();
-      let sendPing = false;
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered":
-          surveyResults.flowId = aOptions.flowId;
-          surveyResults.offeredTS = ts;
-          break;
-        case "Heartbeat:LearnMore":
-          // record only the first click
-          if (!surveyResults.learnMoreTS) {
-            surveyResults.learnMoreTS = ts;
-          }
-          break;
-        case "Heartbeat:Engaged":
-          surveyResults.engagedTS = ts;
-          break;
-        case "Heartbeat:Voted":
-          surveyResults.votedTS = ts;
-          surveyResults.score = aParams.score;
-          break;
-        case "Heartbeat:SurveyExpired":
-          surveyResults.expiredTS = ts;
-          break;
-        case "Heartbeat:NotificationClosed":
-          // this is the final event in most surveys
-          surveyResults.closedTS = ts;
-          sendPing = true;
-          break;
-        case "Heartbeat:WindowClosed":
-          surveyResults.windowClosedTS = ts;
-          sendPing = true;
-          break;
-        default:
-          log.error("maybeNotifyHeartbeat: unrecognized event:", aEventName);
-          break;
-      }
-
-      aParams.timestamp = ts;
-      aParams.flowId = aOptions.flowId;
-      this.notify(aEventName, aParams);
-
-      if (!sendPing) {
-        return;
-      }
-
-      // Send the ping to Telemetry
-      let payload = Object.assign({}, surveyResults);
-      payload.version = 1;
-      for (let meta of ["surveyId", "surveyVersion", "testing"]) {
-        if (aOptions.hasOwnProperty(meta)) {
-          payload[meta] = aOptions[meta];
-        }
-      }
-
-      log.debug("Sending payload to Telemetry: aEventName:", aEventName,
-                "payload:", payload);
-
-      TelemetryController.submitExternalPing("heartbeat", payload, {
-        addClientId: true,
-        addEnvironment: true,
-      });
-
-      // only for testing
-      this.notify("Heartbeat:TelemetrySent", payload);
-
-      // Survey is complete, clear out the expiry timer & survey configuration
-      if (surveyEndTimer) {
-        clearTimeout(surveyEndTimer);
-        surveyEndTimer = null;
-      }
-
-      pingSent = true;
-      surveyResults = {};
-    };
-
-    let nb = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
-    let buttons = null;
-
-    if (aOptions.engagementButtonLabel) {
-      buttons = [{
-        label: aOptions.engagementButtonLabel,
-        callback: () => {
-          // Let the consumer know user engaged.
-          maybeNotifyHeartbeat("Heartbeat:Engaged");
-
-          userEngaged(new Map([
-            ["type", "button"],
-            ["flowid", aOptions.flowId]
-          ]));
-
-          // Return true so that the notification bar doesn't close itself since
-          // we have a thank you message to show.
-          return true;
-        },
-      }];
-    }
-
-    let defaultIcon = "chrome://browser/skin/heartbeat-icon.svg";
-    let iconURL = defaultIcon;
-    try {
-      // Take the optional icon URL if specified
-      if (aOptions.iconURL) {
-        iconURL = new URL(aOptions.iconURL);
-        // For now, only allow chrome URIs.
-        if (iconURL.protocol != "chrome:") {
-          iconURL = defaultIcon;
-          throw new Error("Invalid protocol");
-        }
-      }
-    } catch (error) {
-      log.error("showHeartbeat: Invalid icon URL specified.");
-    }
-
-    // Create the notification. Prefix its ID to decrease the chances of collisions.
-    let notice = nb.appendNotification(aOptions.message, "heartbeat-" + aOptions.flowId,
-                                       iconURL,
-                                       nb.PRIORITY_INFO_HIGH, buttons,
-                                       (aEventType) => {
-                                         if (aEventType != "removed") {
-                                           return;
-                                         }
-                                         // Let the consumer know the notification bar was closed.
-                                         // This also happens after voting.
-                                         maybeNotifyHeartbeat("Heartbeat:NotificationClosed");
-                                       });
-
-    // Get the elements we need to style.
-    let messageImage =
-      aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageImage");
-    let messageText =
-      aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageText");
-
-    function userEngaged(aEngagementParams) {
-      // Make the heartbeat icon pulse twice.
-      notice.label = aOptions.thankyouMessage;
-      messageImage.classList.remove("pulse-onshow");
-      messageImage.classList.add("pulse-twice");
-
-      // Remove all the children of the notice (rating container
-      // and the flex).
-      while (notice.firstChild) {
-        notice.firstChild.remove();
-      }
-
-      // Make sure that we have a valid URL. If we haven't, do not open the engagement page.
-      let engagementURL = null;
-      try {
-        engagementURL = new URL(aOptions.engagementURL);
-      } catch (error) {
-        log.error("showHeartbeat: Invalid URL specified.");
-      }
-
-      // Just open the engagement tab if we have a valid engagement URL.
-      if (engagementURL) {
-        for (let [param, value] of aEngagementParams) {
-          engagementURL.searchParams.append(param, value);
-        }
-
-        // Open the engagement URL in a new tab.
-        aChromeWindow.gBrowser.selectedTab =
-          aChromeWindow.gBrowser.addTab(engagementURL.toString(), {
-            owner: aChromeWindow.gBrowser.selectedTab,
-            relatedToCurrent: true
-          });
-      }
-
-      // Remove the notification bar after 3 seconds.
-      aChromeWindow.setTimeout(() => {
-        nb.removeNotification(notice);
-      }, 3000);
-    }
-
-    // Create the fragment holding the rating UI.
-    let frag = aChromeWindow.document.createDocumentFragment();
-
-    // Build the Heartbeat star rating.
-    const numStars = aOptions.engagementButtonLabel ? 0 : 5;
-    let ratingContainer = aChromeWindow.document.createElement("hbox");
-    ratingContainer.id = "star-rating-container";
-
-    for (let i = 0; i < numStars; i++) {
-      // Create a star rating element.
-      let ratingElement = aChromeWindow.document.createElement("toolbarbutton");
-
-      // Style it.
-      let starIndex = numStars - i;
-      ratingElement.className = "plain star-x";
-      ratingElement.id = "star" + starIndex;
-      ratingElement.setAttribute("data-score", starIndex);
-
-      // Add the click handler.
-      ratingElement.addEventListener("click", function(evt) {
-        let rating = Number(evt.target.getAttribute("data-score"), 10);
-
-        // Let the consumer know user voted.
-        maybeNotifyHeartbeat("Heartbeat:Voted", { score: rating });
-
-        // Append the score data to the engagement URL.
-        userEngaged(new Map([
-          ["type", "stars"],
-          ["score", rating],
-          ["flowid", aOptions.flowId]
-        ]));
-      });
-
-      // Add it to the container.
-      ratingContainer.appendChild(ratingElement);
-    }
-
-    frag.appendChild(ratingContainer);
-
-    // Make sure the stars are not pushed to the right by the spacer.
-    let rightSpacer = aChromeWindow.document.createElement("spacer");
-    rightSpacer.flex = 20;
-    frag.appendChild(rightSpacer);
-
-    messageText.flex = 0; // Collapse the space before the stars.
-    let leftSpacer = messageText.nextSibling;
-    leftSpacer.flex = 0;
-
-    // Make sure that we have a valid learn more URL.
-    let learnMoreURL = null;
-    try {
-      learnMoreURL = new URL(aOptions.learnMoreURL);
-    } catch (error) {
-      log.error("showHeartbeat: Invalid learnMore URL specified.");
-    }
-
-    // Add the learn more link.
-    if (aOptions.learnMoreLabel && learnMoreURL) {
-      let learnMore = aChromeWindow.document.createElement("label");
-      learnMore.className = "text-link";
-      learnMore.href = learnMoreURL.toString();
-      learnMore.setAttribute("value", aOptions.learnMoreLabel);
-      learnMore.addEventListener("click", () => maybeNotifyHeartbeat("Heartbeat:LearnMore"));
-      frag.appendChild(learnMore);
-    }
-
-    // Append the fragment and apply the styling.
-    notice.appendChild(frag);
-    notice.classList.add("heartbeat");
-    messageImage.classList.add("heartbeat", "pulse-onshow");
-    messageText.classList.add("heartbeat");
-
-    // Let the consumer know the notification was shown.
-    maybeNotifyHeartbeat("Heartbeat:NotificationOffered");
-
-    // End the survey if the user quits, closes the window, or
-    // hasn't responded before expiration.
-    if (!aOptions.privateWindowsOnly) {
-      function handleWindowClosed(aTopic) {
-        maybeNotifyHeartbeat("Heartbeat:WindowClosed");
-        aChromeWindow.removeEventListener("SSWindowClosing", handleWindowClosed);
-      }
-      aChromeWindow.addEventListener("SSWindowClosing", handleWindowClosed);
-
-      let surveyDuration = Services.prefs.getIntPref(PREF_SURVEY_DURATION) * 1000;
-      surveyEndTimer = setTimeout(() => {
-        maybeNotifyHeartbeat("Heartbeat:SurveyExpired");
-        nb.removeNotification(notice);
-      }, surveyDuration);
-    }
-  },
-
-  /**
    * The node to which a highlight or notification(-popup) is anchored is sometimes
    * obscured because it may be inside an overflow menu. This function should figure
    * that out and offer the overflow chevron as an alternative.
    *
    * @param {Node} aAnchor The element that's supposed to be the anchor
    * @type {Node}
    */
   _correctAnchor(aAnchor) {
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -28,18 +28,16 @@ skip-if = os == "linux" # Intermittent f
 [browser_UITour2.js]
 [browser_UITour3.js]
 skip-if = os == "linux" # Linux: Bug 986760, Bug 989101.
 [browser_UITour_availableTargets.js]
 [browser_UITour_annotation_size_attributes.js]
 [browser_UITour_defaultBrowser.js]
 [browser_UITour_detach_tab.js]
 [browser_UITour_forceReaderMode.js]
-[browser_UITour_heartbeat.js]
-skip-if = os == "win" # Bug 1277107
 [browser_UITour_modalDialog.js]
 skip-if = os != "mac" # modal dialog disabling only working on OS X.
 [browser_UITour_observe.js]
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_pocket.js]
 skip-if = true # Disabled pending removal of pocket UI Tour
 [browser_UITour_registerPageID.js]
deleted file mode 100644
--- a/browser/components/uitour/test/browser_UITour_heartbeat.js
+++ /dev/null
@@ -1,753 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-var gTestTab;
-var gContentAPI;
-var gContentWindow;
-
-function getHeartbeatNotification(aId, aChromeWindow = window) {
-  let notificationBox = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
-  // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
-  return notificationBox.getNotificationWithValue("heartbeat-" + aId);
-}
-
-/**
- * Simulate a click on a rating element in the Heartbeat notification.
- *
- * @param aId
- *        The id of the notification box.
- * @param aScore
- *        The score related to the rating element we want to click on.
- */
-function simulateVote(aId, aScore) {
-  let notification = getHeartbeatNotification(aId);
-
-  let ratingContainer = notification.childNodes[0];
-  ok(ratingContainer, "The notification has a valid rating container.");
-
-  let ratingElement = ratingContainer.getElementsByAttribute("data-score", aScore);
-  ok(ratingElement[0], "The rating container contains the requested rating element.");
-
-  ratingElement[0].click();
-}
-
-/**
- * Simulate a click on the learn-more link.
- *
- * @param aId
- *        The id of the notification box.
- */
-function clickLearnMore(aId) {
-  let notification = getHeartbeatNotification(aId);
-
-  let learnMoreLabel = notification.childNodes[2];
-  ok(learnMoreLabel, "The notification has a valid learn more label.");
-
-  learnMoreLabel.click();
-}
-
-/**
- * Remove the notification box.
- *
- * @param aId
- *        The id of the notification box to remove.
- * @param [aChromeWindow=window]
- *        The chrome window the notification box is in.
- */
-function cleanUpNotification(aId, aChromeWindow = window) {
-  let notification = getHeartbeatNotification(aId, aChromeWindow);
-  notification.close();
-}
-
-/**
- * Check telemetry payload for proper format and expected content.
- *
- * @param aPayload
- *        The Telemetry payload to verify
- * @param aFlowId
- *        Expected value of the flowId field.
- * @param aExpectedFields
- *        Array of expected fields. No other fields are allowed.
- */
-function checkTelemetry(aPayload, aFlowId, aExpectedFields) {
-  // Basic payload format
-  is(aPayload.version, 1, "Telemetry ping must have heartbeat version=1");
-  is(aPayload.flowId, aFlowId, "Flow ID in the Telemetry ping must match");
-
-  // Check for superfluous fields
-  let extraKeys = new Set(Object.keys(aPayload));
-  extraKeys.delete("version");
-  extraKeys.delete("flowId");
-
-  // Check for expected fields
-  for (let field of aExpectedFields) {
-    ok(field in aPayload, "The payload should have the field '" + field + "'");
-    if (field.endsWith("TS")) {
-      let ts = aPayload[field];
-      ok(Number.isInteger(ts) && ts > 0, "Timestamp '" + field + "' must be a natural number");
-    }
-    extraKeys.delete(field);
-  }
-
-  is(extraKeys.size, 0, "No unexpected fields in the Telemetry payload");
-}
-
-/**
- * Waits for an UITour notification dispatched through |UITour.notify|. This should be
- * done with |gContentAPI.observe|. Unfortunately, in e10s, |gContentAPI.observe| doesn't
- * allow for multiple calls to the same callback, allowing to catch just the first
- * notification.
- *
- * @param aEventName
- *        The notification name to wait for.
- * @return {Promise} Resolved with the data that comes with the event.
- */
-function promiseWaitHeartbeatNotification(aEventName) {
-  return ContentTask.spawn(gTestTab.linkedBrowser, aEventName, (aContentEventName) => {
-        return new Promise(resolve => {
-          addEventListener("mozUITourNotification", function listener(event) {
-            if (event.detail.event !== aContentEventName) {
-              return;
-            }
-            removeEventListener("mozUITourNotification", listener, false);
-            resolve(event.detail.params);
-          }, false);
-        });
-      });
-}
-
-/**
- * Waits for UITour notifications dispatched through |UITour.notify|. This works like
- * |promiseWaitHeartbeatNotification|, but waits for all the passed notifications to
- * be received before resolving. If it receives an unaccounted notification, it rejects.
- *
- * @param events
- *        An array of expected notification names to wait for.
- * @return {Promise} Resolved with the data that comes with the event. Rejects with the
- *         name of an undesired notification if received.
- */
-function promiseWaitExpectedNotifications(events) {
-  return ContentTask.spawn(gTestTab.linkedBrowser, events, contentEvents => {
-        let stillToReceive = contentEvents;
-        return new Promise((res, rej) => {
-          addEventListener("mozUITourNotification", function listener(event) {
-            if (stillToReceive.includes(event.detail.event)) {
-              // Filter out the received event.
-              stillToReceive = stillToReceive.filter(x => x !== event.detail.event);
-            } else {
-              removeEventListener("mozUITourNotification", listener, false);
-              rej(event.detail.event);
-            }
-            // We still need to catch some notifications. Don't do anything.
-            if (stillToReceive.length > 0) {
-              return;
-            }
-            // We don't need to listen for other notifications. Resolve the promise.
-            removeEventListener("mozUITourNotification", listener, false);
-            res();
-          }, false);
-        });
-      });
-}
-
-function validateTimestamp(eventName, timestamp) {
-  info("'" + eventName + "' notification received (timestamp " + timestamp.toString() + ").");
-  ok(Number.isFinite(timestamp), "Timestamp must be a number.");
-}
-
-add_task(async function test_setup() {
-  await setup_UITourTest();
-  requestLongerTimeout(2);
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-  });
-});
-
-/**
- * Check that the "stars" heartbeat UI correctly shows and closes.
- */
-add_UITour_task(async function test_heartbeat_stars_show() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let engagementURL = "http://example.com";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Close the heartbeat notification.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Check that the heartbeat UI correctly takes optional icon URL.
- */
-add_UITour_task(async function test_heartbeat_take_optional_icon_URL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let engagementURL = "http://example.com";
-  let iconURL = "chrome://branding/content/icon48.png";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL, null, null, {
-    iconURL
-  });
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Check the icon URL
-  let notification = getHeartbeatNotification(flowId);
-  is(notification.image, iconURL, "The optional icon URL is not taken correctly");
-
-  // Close the heartbeat notification.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the heartbeat UI correctly works with null engagement URL.
- */
-add_UITour_task(async function test_heartbeat_null_engagementURL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 2);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 2, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the heartbeat UI correctly works with an invalid, but non null, engagement URL.
- */
-add_UITour_task(async function test_heartbeat_invalid_engagement_URL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  let invalidEngagementURL = "invalidEngagement";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, invalidEngagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 2);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 2, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the score is correctly reported.
- */
-add_UITour_task(async function test_heartbeat_stars_vote() {
-  const expectedScore = 4;
-  let originalTabCount = gBrowser.tabs.length;
-  let flowId = "ui-ratefirefox-" + Math.random();
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, expectedScore);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-  is(data.score, expectedScore, "Should report a score of " + expectedScore);
-
-  // Validate the closing timestamp and vote.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, expectedScore, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the engagement page is correctly opened when voting.
- */
-add_UITour_task(async function test_heartbeat_engagement_tab() {
-  let engagementURL = "http://example.com";
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 1);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp, vote and make sure the engagement page was opened.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 1, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the engagement button opens the engagement URL.
- */
-add_UITour_task(async function test_heartbeat_engagement_button() {
-  let engagementURL = "http://example.com";
-  let flowId = "ui-engagewithfirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Engaged", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL, null, null, {
-    engagementButtonLabel: "Engage Me",
-  });
-
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Engaged, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let engagedPromise = promiseWaitHeartbeatNotification("Heartbeat:Engaged");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // Simulate user engagement.
-  let notification = getHeartbeatNotification(flowId);
-  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
-  // The UI was just shown. We can simulate a click on the engagement button.
-  let engagementButton = notification.querySelector(".notification-button");
-  is(engagementButton.label, "Engage Me", "Check engagement button text");
-  engagementButton.doCommand();
-
-  data = await engagedPromise;
-  validateTimestamp("Heartbeat:Engaged", data.timestamp);
-
-  // Validate the closing timestamp, vote and make sure the engagement page was opened.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "engagedTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the learn more link is displayed and that the page is correctly opened when
- * clicking on it.
- */
-add_UITour_task(async function test_heartbeat_learnmore() {
-  let dummyURL = "http://example.com";
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:LearnMore", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, dummyURL,
-                            "What is this?", dummyURL);
-
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the LearnMore, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let learnMorePromise = promiseWaitHeartbeatNotification("Heartbeat:LearnMore");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. Simulate a click on the learn more link.
-  clickLearnMore(flowId);
-
-  data = await learnMorePromise;
-  validateTimestamp("Heartbeat:LearnMore", data.timestamp);
-  cleanUpNotification(flowId);
-
-  // The notification was closed.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Learn more URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "learnMoreTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-add_UITour_task(async function test_invalidEngagementButtonLabel() {
-  let engagementURL = "http://example.com";
-  let flowId = "invalidEngagementButtonLabel-" + Math.random();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: 42,
-                            });
-
-  await eventPromise;
-  ok(!isTourBrowser(gBrowser.selectedBrowser),
-     "Invalid engagementButtonLabel should prevent init");
-
-})
-
-add_UITour_task(async function test_privateWindowsOnly_noneOpen() {
-  let engagementURL = "http://example.com";
-  let flowId = "privateWindowsOnly_noneOpen-" + Math.random();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await eventPromise;
-  ok(!isTourBrowser(gBrowser.selectedBrowser),
-     "If there are no private windows opened, tour init should be prevented");
-})
-
-add_UITour_task(async function test_privateWindowsOnly_notMostRecent() {
-  let engagementURL = "http://example.com";
-  let flowId = "notMostRecent-" + Math.random();
-
-  let privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
-  let mostRecentWin = await BrowserTestUtils.openNewBrowserWindow();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await eventPromise;
-  is(getHeartbeatNotification(flowId, window), null,
-     "Heartbeat shouldn't appear in the default window");
-  is(!!getHeartbeatNotification(flowId, privateWin), true,
-     "Heartbeat should appear in the most recent private window");
-  is(getHeartbeatNotification(flowId, mostRecentWin), null,
-     "Heartbeat shouldn't appear in the most recent non-private window");
-
-  await BrowserTestUtils.closeWindow(mostRecentWin);
-  await BrowserTestUtils.closeWindow(privateWin);
-})
-
-add_UITour_task(async function test_privateWindowsOnly() {
-  let engagementURL = "http://example.com";
-  let learnMoreURL = "http://example.org/learnmore/";
-  let flowId = "ui-privateWindowsOnly-" + Math.random();
-
-  let privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
-
-  await new Promise((resolve) => {
-    gContentAPI.observe(function(aEventName, aData) {
-      info(aEventName + " notification received: " + JSON.stringify(aData, null, 2));
-      ok(false, "No heartbeat notifications should arrive for privateWindowsOnly");
-    }, resolve);
-  });
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            "Learn More", learnMoreURL, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await promisePageEvent();
-
-  ok(isTourBrowser(gBrowser.selectedBrowser), "UITour should have been init for the browser");
-
-  let notification = getHeartbeatNotification(flowId, privateWin);
-
-  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
-
-  info("Test the learn more link.");
-  let learnMoreLink = notification.querySelector(".text-link");
-  is(learnMoreLink.value, "Learn More", "Check learn more label");
-  let learnMoreTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-  learnMoreLink.click();
-  let learnMoreTab = await learnMoreTabPromise;
-  is(learnMoreTab.linkedBrowser.currentURI.host, "example.org", "Check learn more site opened");
-  ok(PrivateBrowsingUtils.isBrowserPrivate(learnMoreTab.linkedBrowser), "Ensure the learn more tab is private");
-  await BrowserTestUtils.removeTab(learnMoreTab);
-
-  info("Test the engagement button's new tab.");
-  let engagementButton = notification.querySelector(".notification-button");
-  is(engagementButton.label, "Yes!", "Check engagement button text");
-  let engagementTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-  engagementButton.doCommand();
-  let engagementTab = await engagementTabPromise;
-  is(engagementTab.linkedBrowser.currentURI.host, "example.com", "Check enagement site opened");
-  ok(PrivateBrowsingUtils.isBrowserPrivate(engagementTab.linkedBrowser), "Ensure the engagement tab is private");
-  await BrowserTestUtils.removeTab(engagementTab);
-
-  await BrowserTestUtils.closeWindow(privateWin);
-})
-
-/**
- * Test that the survey closes itself after a while and submits Telemetry
- */
-add_UITour_task(async function test_telemetry_surveyExpired() {
-  let flowId = "survey-expired-" + Math.random();
-  let engagementURL = "http://example.com";
-  let surveyDuration = 1; // 1 second (pref is in seconds)
-  Services.prefs.setIntPref("browser.uitour.surveyDuration", surveyDuration);
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:SurveyExpired", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  let expiredPromise = promiseWaitHeartbeatNotification("Heartbeat:SurveyExpired");
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  await Promise.all([shownPromise, expiredPromise, closedPromise]);
-  // Validate the ping data.
-  let data = await pingPromise;
-  checkTelemetry(data, flowId, ["offeredTS", "expiredTS", "closedTS"]);
-
-  Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Check that certain whitelisted experiment parameters get reflected in the
- * Telemetry ping
- */
-add_UITour_task(async function test_telemetry_params() {
-  let flowId = "telemetry-params-" + Math.random();
-  let engagementURL = "http://example.com";
-  let extraParams = {
-    "surveyId": "foo",
-    "surveyVersion": 1.5,
-    "testing": true,
-    "notWhitelisted": 123,
-  };
-  let expectedFields = ["surveyId", "surveyVersion", "testing"];
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!",
-                            flowId, engagementURL, null, null, extraParams);
-  await shownPromise;
-
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  // The notification was closed.
-  let data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  // Validate the data we send out.
-  data = await pingPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"].concat(expectedFields));
-  for (let param of expectedFields) {
-    is(data[param], extraParams[param],
-       "Whitelisted experiment configs should be copied into Telemetry pings");
-  }
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
--- a/browser/extensions/activity-stream/README.md
+++ b/browser/extensions/activity-stream/README.md
@@ -1,8 +1,10 @@
 # activity-stream
 
 This system add-on replaces the new tab page in Firefox with a new design and
 functionality as part of the Activity Stream project. It can be enabled (or disabled)
 via the browser.newtabpage.activity-stream.enabled pref.
 
 The files in this directory, including vendor dependencies, are imported from the
 system-addon directory in https://github.com/mozilla/activity-stream.
+
+Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon) for more detail.
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,32 +1,57 @@
 /* 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/. */
-/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
 "use strict";
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+  "resource://gre/modules/Preferences.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
-  "resource://activity-stream/lib/ActivityStream.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
 
+const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
 const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
-const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
+const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
 const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
-const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
+const RESOURCE_BASE = "resource://activity-stream";
 
 const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
 
 let activityStream;
+let modulesToUnload = new Set();
 let startupData;
 let startupReason;
+let waitingForBrowserReady = true;
+
+// Lazily load ActivityStream then find related modules to unload
+XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
+  "resource://activity-stream/lib/ActivityStream.jsm", null, null, () => {
+    // Helper to fetch a resource directory listing and call back with each item
+    const processListing = async(uri, cb) => (await (await fetch(uri)).text())
+      .split("\n").slice(2).forEach(line => cb(line.split(" ").slice(1)));
+
+    // Look for modules one level deeper than the top resource URI
+    processListing(RESOURCE_BASE, ([directory, , , type]) => {
+      if (type === "DIRECTORY") {
+        // Look into this directory for .jsm files
+        const subDir = `${RESOURCE_BASE}/${directory}`;
+        processListing(subDir, ([name]) => {
+          if (name && name.search(/\.jsm$/) !== -1) {
+            modulesToUnload.add(`${subDir}/${name}`);
+          }
+        });
+      }
+    });
+  });
 
 /**
  * init - Initializes an instance of ActivityStream. This could be called by
  *        the startup() function exposed by bootstrap.js, or it could be called
  *        when ACTIVITY_STREAM_ENABLED_PREF is changed from false to true.
  *
  * @param  {string} reason - Reason for initialization. Could be install, upgrade, or PREF_ON
  */
@@ -45,64 +70,94 @@ function init(reason) {
  *          called by the shutdown() function exposed by bootstrap.js, or it could
  *          be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
  *
  * @param  {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
  */
 function uninit(reason) {
   if (activityStream) {
     activityStream.uninit(reason);
-    activityStream = null;
   }
 }
 
 /**
  * onPrefChanged - handler for changes to ACTIVITY_STREAM_ENABLED_PREF
  *
  * @param  {bool} isEnabled Determines whether Activity Stream is enabled
  */
 function onPrefChanged(isEnabled) {
   if (isEnabled) {
     init(REASON_STARTUP_ON_PREF_CHANGE);
   } else {
     uninit(REASON_SHUTDOWN_ON_PREF_CHANGE);
   }
 }
 
+/**
+ * onBrowserReady - Continues startup of the add-on after browser is ready.
+ */
+function onBrowserReady() {
+  waitingForBrowserReady = false;
+
+  // Listen for changes to the pref that enables Activity Stream
+  Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+
+  // Only initialize if the pref is true
+  if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+    init(startupReason);
+  }
+}
+
+/**
+ * observe - nsIObserver callback to handle various browser notifications.
+ */
 function observe(subject, topic, data) {
   switch (topic) {
     case BROWSER_READY_NOTIFICATION:
-      // Listen for changes to the pref that enables Activity Stream
-      Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-      // Only initialize if the pref is true
-      if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
-        init(startupReason);
-        Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
-      }
+      Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+      onBrowserReady();
       break;
   }
 }
 
 // The functions below are required by bootstrap.js
 
 this.install = function install(data, reason) {};
 
 this.startup = function startup(data, reason) {
-  // Only start Activity Stream up when the browser UI is ready
-  Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
-
   // Cache startup data which contains stuff like the version number, etc.
   // so we can use it when we init
   startupData = data;
   startupReason = reason;
+
+  // Only start Activity Stream up when the browser UI is ready
+  if (Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).startingUp) {
+    Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Handle manual install or automatic install after manual uninstall
+    onBrowserReady();
+  }
 };
 
 this.shutdown = function shutdown(data, reason) {
   // Uninitialize Activity Stream
   startupData = null;
   startupReason = null;
   uninit(reason);
 
-  // Stop listening to the pref that enables Activity Stream
-  Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  // Stop waiting for browser to be ready
+  if (waitingForBrowserReady) {
+    Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Stop listening to the pref that enables Activity Stream
+    Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  }
+
+  // Unload any add-on modules that might might have been imported
+  modulesToUnload.forEach(Cu.unload);
 };
 
-this.uninstall = function uninstall(data, reason) {};
+this.uninstall = function uninstall(data, reason) {
+  if (activityStream) {
+    activityStream.uninstall(reason);
+    activityStream = null;
+  }
+};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -25,25 +25,23 @@ const actionTypes = [
   "INIT",
   "LOCALE_UPDATED",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
   "NEW_TAB_UNLOAD",
   "NEW_TAB_VISIBLE",
   "OPEN_NEW_WINDOW",
   "OPEN_PRIVATE_WINDOW",
-  "PERFORM_SEARCH",
   "PLACES_BOOKMARK_ADDED",
   "PLACES_BOOKMARK_CHANGED",
   "PLACES_BOOKMARK_REMOVED",
   "PLACES_HISTORY_CLEARED",
   "PLACES_LINK_BLOCKED",
   "PLACES_LINK_DELETED",
   "SCREENSHOT_UPDATED",
-  "SEARCH_STATE_UPDATED",
   "TELEMETRY_PERFORMANCE_EVENT",
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_UPDATED",
   "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -16,25 +16,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
       return Object.assign({}, action.data || {}, {initialized: true});
     case at.LOCALE_UPDATED: {
@@ -95,28 +86,12 @@ function TopSites(prevState = INITIAL_ST
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, {rows: newRows});
     default:
       return prevState;
   }
 }
 
-function Search(prevState = INITIAL_STATE.Search, action) {
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED: {
-      if (!action.data) {
-        return prevState;
-      }
-      let {currentEngine, engines} = action.data;
-      return Object.assign({}, prevState, {
-        currentEngine,
-        engines
-      });
-    }
-    default:
-      return prevState;
-  }
-}
 this.INITIAL_STATE = INITIAL_STATE;
-this.reducers = {TopSites, App, Search};
+this.reducers = {TopSites, App};
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -92,17 +92,17 @@ var BACKGROUND_PROCESS = 2;
  *                       Use this in action creators if you need different logic
  *                       for ui/background processes.
  */
 
 const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
 // Export for tests
 
 
-const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PERFORM_SEARCH", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
+const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 // It prevents accidentally adding a different key/value name.
 ].reduce((obj, type) => {
   obj[type] = type;return obj;
@@ -435,17 +435,17 @@ module.exports = class DetectUserSession
 
 /***/ }),
 /* 6 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-/* globals sendAsyncMessage, addMessageListener */
+/* eslint-env mozilla/frame-script */
 
 var _require = __webpack_require__(14);
 
 const createStore = _require.createStore,
       combineReducers = _require.combineReducers,
       applyMiddleware = _require.applyMiddleware;
 
 var _require2 = __webpack_require__(1);
@@ -539,25 +539,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App() {
   let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
   let action = arguments[1];
 
   switch (action.type) {
@@ -632,40 +623,17 @@ function TopSites() {
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, { rows: newRows });
     default:
       return prevState;
   }
 }
 
-function Search() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
-  let action = arguments[1];
-
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED:
-      {
-        if (!action.data) {
-          return prevState;
-        }
-        var _action$data3 = action.data;
-        let currentEngine = _action$data3.currentEngine,
-            engines = _action$data3.engines;
-
-        return Object.assign({}, prevState, {
-          currentEngine,
-          engines
-        });
-      }
-    default:
-      return prevState;
-  }
-}
-var reducers = { TopSites, App, Search };
+var reducers = { TopSites, App };
 module.exports = {
   reducers,
   INITIAL_STATE
 };
 
 /***/ }),
 /* 8 */
 /***/ (function(module, exports) {
@@ -768,79 +736,95 @@ var _require = __webpack_require__(3);
 
 const injectIntl = _require.injectIntl;
 
 const ContextMenu = __webpack_require__(9);
 
 var _require2 = __webpack_require__(1);
 
 const actionTypes = _require2.actionTypes,
-      actionCreators = _require2.actionCreators;
+      ac = _require2.actionCreators;
 
 
 class LinkMenu extends React.Component {
   getBookmarkStatus(site) {
     return site.bookmarkGuid ? {
       id: "menu_action_remove_bookmark",
       icon: "bookmark-remove",
       action: "DELETE_BOOKMARK_BY_ID",
-      data: site.bookmarkGuid
+      data: site.bookmarkGuid,
+      userEvent: "BOOKMARK_DELETE"
     } : {
       id: "menu_action_bookmark",
       icon: "bookmark",
       action: "BOOKMARK_URL",
-      data: site.url
+      data: site.url,
+      userEvent: "BOOKMARK_ADD"
     };
   }
   getDefaultContextMenu(site) {
     return [{
       id: "menu_action_open_new_window",
       icon: "new-window",
       action: "OPEN_NEW_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_NEW_WINDOW"
     }, {
       id: "menu_action_open_private_window",
       icon: "new-window-private",
       action: "OPEN_PRIVATE_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_PRIVATE_WINDOW"
     }];
   }
   getOptions() {
     var _props = this.props;
     const dispatch = _props.dispatch,
-          site = _props.site;
+          site = _props.site,
+          index = _props.index,
+          source = _props.source;
 
     // default top sites have a limited set of context menu options
 
     let options = this.getDefaultContextMenu(site);
 
     // all other top sites have all the following context menu options
     if (!site.isDefault) {
       options = [this.getBookmarkStatus(site), { type: "separator" }, ...options, { type: "separator" }, {
         id: "menu_action_dismiss",
         icon: "dismiss",
         action: "BLOCK_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "BLOCK"
       }, {
         id: "menu_action_delete",
         icon: "delete",
         action: "DELETE_HISTORY_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "DELETE"
       }];
     }
     options.forEach(option => {
-      let action = option.action,
-          data = option.data,
-          id = option.id,
-          type = option.type;
+      const action = option.action,
+            data = option.data,
+            id = option.id,
+            type = option.type,
+            userEvent = option.userEvent;
       // Convert message ids to localized labels and add onClick function
 
       if (!type && id) {
         option.label = this.props.intl.formatMessage(option);
-        option.onClick = () => dispatch(actionCreators.SendToMain({ type: actionTypes[action], data }));
+        option.onClick = () => {
+          dispatch(ac.SendToMain({ type: actionTypes[action], data }));
+          dispatch(ac.UserEvent({
+            event: userEvent,
+            source,
+            action_position: index
+          }));
+        };
       }
     });
 
     // this is for a11y - we want to know which item is the first and which item
     // is the last, so we can close the context menu accordingly
     options[0].first = true;
     options[options.length - 1].last = true;
     return options;
@@ -856,104 +840,98 @@ class LinkMenu extends React.Component {
 module.exports = injectIntl(LinkMenu);
 module.exports._unconnected = LinkMenu;
 
 /***/ }),
 /* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
+/* globals ContentSearchUIController */
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage,
       injectIntl = _require2.injectIntl;
 
 var _require3 = __webpack_require__(1);
 
-const actionTypes = _require3.actionTypes,
-      actionCreators = _require3.actionCreators;
+const ac = _require3.actionCreators;
 
 
 class Search extends React.Component {
   constructor(props) {
     super(props);
-    this.state = { searchString: "" };
     this.onClick = this.onClick.bind(this);
-    this.onChange = this.onChange.bind(this);
-  }
-
-  componentWillMount() {
-    // Trigger initialization of ContentSearch in case it hasn't happened yet
-    dispatchEvent(new CustomEvent("ContentSearchClient", { detail: {} }));
+    this.onInputMount = this.onInputMount.bind(this);
   }
 
-  performSearch(options) {
-    let searchData = {
-      engineName: options.engineName,
-      searchString: options.searchString,
-      searchPurpose: "newtab",
-      healthReportKey: "newtab"
-    };
-    this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+  handleEvent(event) {
+    // Also track search events with our own telemetry
+    if (event.detail.type === "Search") {
+      this.props.dispatch(ac.UserEvent({ event: "SEARCH" }));
+    }
   }
   onClick(event) {
-    const currentEngine = this.props.Search.currentEngine;
+    this.controller.search(event);
+  }
+  onInputMount(input) {
+    if (input) {
+      this.controller = new ContentSearchUIController(input, input.parentNode, "newtab", "activity");
+      addEventListener("ContentSearchClient", this);
+    } else {
+      this.controller = null;
+      removeEventListener("ContentSearchClient", this);
+    }
+  }
 
-    event.preventDefault();
-    this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
-  }
-  onChange(event) {
-    this.setState({ searchString: event.target.value });
-  }
   render() {
     return React.createElement(
       "form",
       { className: "search-wrapper" },
       React.createElement(
         "label",
         { htmlFor: "search-input", className: "search-label" },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_web_placeholder" })
         )
       ),
       React.createElement("input", {
         id: "search-input",
         maxLength: "256",
-        onChange: this.onChange,
         placeholder: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
+        ref: this.onInputMount,
         title: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
-        type: "search",
-        value: this.state.searchString }),
+        type: "search" }),
       React.createElement(
         "button",
         {
           className: "search-button",
           onClick: this.onClick,
           title: this.props.intl.formatMessage({ id: "search_button" }) },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_button" })
         )
       )
     );
   }
 }
 
-module.exports = connect(state => ({ Search: state.Search }))(injectIntl(Search));
+module.exports = connect()(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
 /* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -966,41 +944,54 @@ const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage;
 
 const shortURL = __webpack_require__(13);
 const LinkMenu = __webpack_require__(10);
 
+var _require3 = __webpack_require__(1);
+
+const ac = _require3.actionCreators;
+
+const TOP_SITES_SOURCE = "TOP_SITES";
+
 class TopSite extends React.Component {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeTile: null };
   }
   toggleContextMenu(event, index) {
     this.setState({ showContextMenu: true, activeTile: index });
   }
+  trackClick() {
+    this.props.dispatch(ac.UserEvent({
+      event: "CLICK",
+      source: TOP_SITES_SOURCE,
+      action_position: this.props.index
+    }));
+  }
   render() {
     var _props = this.props;
     const link = _props.link,
           index = _props.index,
           dispatch = _props.dispatch;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
     const title = shortURL(link);
     const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
     const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
     const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
     return React.createElement(
       "li",
       { className: topSiteOuterClassName, key: link.url },
       React.createElement(
         "a",
-        { href: link.url },
+        { onClick: () => this.trackClick(), href: link.url },
         React.createElement(
           "div",
           { className: "tile", "aria-hidden": true },
           React.createElement(
             "span",
             { className: "letter-fallback" },
             title[0]
           ),
@@ -1025,33 +1016,38 @@ class TopSite extends React.Component {
           `Open context menu for ${title}`
         )
       ),
       React.createElement(LinkMenu, {
         dispatch: dispatch,
         visible: isContextMenuOpen,
         onUpdate: val => this.setState({ showContextMenu: val }),
         site: link,
-        index: index })
+        index: index,
+        source: TOP_SITES_SOURCE })
     );
   }
 }
 
 const TopSites = props => React.createElement(
   "section",
   null,
   React.createElement(
     "h3",
     { className: "section-title" },
     React.createElement(FormattedMessage, { id: "header_top_sites" })
   ),
   React.createElement(
     "ul",
     { className: "top-sites-list" },
-    props.TopSites.rows.map((link, index) => React.createElement(TopSite, { dispatch: props.dispatch, key: link.url, link: link, index: index }))
+    props.TopSites.rows.map((link, index) => React.createElement(TopSite, {
+      key: link.url,
+      dispatch: props.dispatch,
+      link: link,
+      index: index }))
   )
 );
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
 module.exports._unconnected = TopSites;
 module.exports.TopSite = TopSite;
 
 /***/ }),
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -238,159 +238,75 @@ main {
 
 .search-wrapper {
   cursor: default;
   display: flex;
   position: relative;
   margin: 0 0 48px;
   width: 100%;
   height: 36px; }
-  .search-wrapper .search-container {
-    z-index: 1001;
-    background: #FFF;
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 100%;
-    margin-top: -2px;
-    border: 1px solid #BFBFBF;
-    font-size: 12px;
-    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
-    overflow: hidden; }
-    .search-wrapper .search-container .search-title {
-      color: #666;
-      padding: 5px 10px;
-      background-color: #F7F7F7;
-      display: flex;
-      align-items: center;
-      word-break: break-all; }
-      .search-wrapper .search-container .search-title p {
-        margin: 0; }
-      .search-wrapper .search-container .search-title #current-engine-icon {
-        margin-inline-end: 8px; }
-    .search-wrapper .search-container section {
-      border-bottom: 1px solid #EAEAEA;
-      margin-bottom: 0; }
-    .search-wrapper .search-container .search-suggestions ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-suggestions ul li a {
-        cursor: default;
-        color: #000;
-        display: block;
-        padding: 4px 36px; }
-        .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
-          background: #0996F8;
-          color: #FFF; }
-    .search-wrapper .search-container .history-search-suggestions {
-      border-bottom: 0; }
-      .search-wrapper .search-container .history-search-suggestions ul {
-        padding: 0;
-        margin: 0;
-        list-style: none; }
-        .search-wrapper .search-container .history-search-suggestions ul li a {
-          cursor: default;
-          color: #000;
-          display: block;
-          padding: 4px 10px; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
-            background: #0996F8;
-            color: #FFF; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
-          .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
-            background-image: url("assets/glyph-search-history.svg#search-history-active"); }
-    .search-wrapper .search-container .history-search-suggestions #historyIcon {
-      width: 16px;
-      height: 16px;
-      display: inline-block;
-      margin-inline-end: 10px;
-      margin-bottom: -3px;
-      background-image: url("assets/glyph-search-history.svg#search-history"); }
-    .search-wrapper .search-container .search-partners ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-partners ul li {
-        display: inline-block;
-        padding: 5px 0; }
-        .search-wrapper .search-container .search-partners ul li a {
-          display: block;
-          padding: 3px 16px;
-          border-right: 1px solid #BFBFBF; }
-        .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
-          background: #0996F8;
-          color: #FFF; }
-          .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
-            border-color: transparent; }
-    .search-wrapper .search-container .search-settings button {
-      color: #666;
-      margin: 0;
-      padding: 0;
-      height: 32px;
-      text-align: center;
-      width: 100%;
-      border-style: solid none none;
-      border-radius: 0;
-      background: #F7F7F7;
-      border-top: 0; }
-      .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
-        background: #EBEBEB;
-        box-shadow: none; }
   .search-wrapper input {
     border: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     flex-grow: 1;
     margin: 0;
     outline: none;
     padding: 0 12px 0 35px;
     height: 100%;
-    border-top-left-radius: 4px;
-    border-bottom-left-radius: 4px;
+    border-radius: 4px 0 0 4px;
     padding-inline-start: 35px; }
     .search-wrapper input:focus {
       border-color: #0996F8;
       box-shadow: 0 0 0 2px #0996F8;
       z-index: 1; }
-    .search-wrapper input:focus + button {
+    .search-wrapper input:focus + .search-button {
       z-index: 1;
       box-shadow: 0 0 0 2px #0996F8;
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
       color: #FFF; }
-  .search-wrapper input:dir(rtl) {
-    border-radius: 0 4px 4px 0; }
+    .search-wrapper input[aria-expanded="true"] {
+      border-radius: 4px 0 0 0; }
+    .search-wrapper input:dir(rtl) {
+      border-radius: 0 4px 4px 0; }
+      .search-wrapper input:dir(rtl)[aria-expanded="true"] {
+        border-radius: 0 4px 0 0; }
   .search-wrapper .search-label {
     background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
     position: absolute;
     top: 0;
     offset-inline-start: 0;
     height: 100%;
     width: 35px;
     display: flex;
     align-items: center;
     justify-content: center;
     z-index: 2; }
-  .search-wrapper button {
+  .search-wrapper .search-button {
     border-radius: 0 3px 3px 0;
     margin-inline-start: -1px;
     border: 0;
     width: 36px;
     padding: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
     background-size: 16px 16px; }
-    .search-wrapper button:hover {
+    .search-wrapper .search-button:hover {
       z-index: 1;
       box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
-      color: #FFF; }
-  .search-wrapper button:dir(rtl) {
-    transform: scaleX(-1); }
+      color: #FFF;
+      cursor: pointer; }
+    .search-wrapper .search-button:dir(rtl) {
+      transform: scaleX(-1); }
+  .search-wrapper .contentSearchSuggestionTable {
+    transform: translate(-2px, 2px); }
+    .search-wrapper .contentSearchSuggestionTable:dir(rtl) {
+      transform: translate(2px, 2px); }
 
 .context-menu {
   display: block;
   position: absolute;
   font-size: 14px;
   box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 0, 0, 0.2);
   top: 6.75px;
   offset-inline-start: 100%;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,17 +1,19 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title></title>
+    <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
+    <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
deleted file mode 100644
--- a/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
-  <style>
-    use:not(:target) {
-      display: none;
-    }
-    use {
-      fill: graytext;
-    }
-    use[id$="-active"] {
-      fill: HighlightText;
-    }
-  </style>
-  <defs>
-    <path id="search-history-glyph" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7 C15,4.1,11.9,1,8,1z M8,13.3c-2.9,0-5.3-2.4-5.3-5.3S5.1,2.7,8,2.7c2.9,0,5.3,2.4,5.3,5.3S10.9,13.3,8,13.3z M10.5,7H9V5 c0-0.6-0.4-1-1-1S7,4.4,7,5v3c0,0.6,0.4,1,1,1h2.5c0.6,0,1-0.4,1-1C11.5,7.4,11.1,7,10.5,7z"/>
-  </defs>
-  <use id="search-history" xlink:href="#search-history-glyph"/>
-  <use id="search-history-active" xlink:href="#search-history-glyph"/>
-</svg>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -125,26 +125,46 @@
     "menu_action_remove_bookmark": "Əlfəcini sil",
     "menu_action_copy_address": "Ünvanı köçür",
     "menu_action_email_link": "Keçidi e-poçt ilə göndər…",
     "menu_action_open_new_window": "Yeni Pəncərədə Aç",
     "menu_action_open_private_window": "Yeni Məxfi Pəncərədə Aç",
     "menu_action_dismiss": "Rədd et",
     "menu_action_delete": "Tarixçədən Sil",
     "search_for_something_with": "{search_term} üçün bununla axtar:",
+    "search_button": "Axtar",
     "search_header": "{search_engine_name} Axtarış",
     "search_web_placeholder": "İnternetdə Axtar",
     "search_settings": "Axtarış Tənzimləmələrini Dəyiş",
     "welcome_title": "Yeni vərəqə xoş gəldiniz",
     "welcome_body": "Firefox bu səhifədə ən uyğun əlfəcin, məqalə, video və son ziyarət etdiyiniz səhifələri göstərərək onları rahat tapmağınıza kömək edəcək.",
     "welcome_label": "Seçilmişləriniz təyin edilir",
     "time_label_less_than_minute": "<1d",
     "time_label_minute": "{number}d",
     "time_label_hour": "{number}s",
-    "time_label_day": "{number}g"
+    "time_label_day": "{number}g",
+    "settings_pane_button_label": "Yeni Vərəq səhifənizi özəlləşdirin",
+    "settings_pane_header": "Yeni Vərəq Nizamlamaları",
+    "settings_pane_body": "Yeni vərəq açdığınızda nə görəcəyinizi seçin.",
+    "settings_pane_search_header": "Axtar",
+    "settings_pane_search_body": "Yeni vərəqinizdən Web-də axtarış edin.",
+    "settings_pane_topsites_header": "Qabaqcıl Saytlar",
+    "settings_pane_topsites_body": "Ən çox ziyarət etdiyiniz saytları görün.",
+    "settings_pane_topsites_options_showmore": "İki sətir göstər",
+    "settings_pane_highlights_header": "Seçilmişlər",
+    "settings_pane_highlights_body": "Son gəzmə tarixçəniz və yeni yaradılar əlfəcinlərinizə göz gəzdirin.",
+    "settings_pane_done_button": "Oldu",
+    "edit_topsites_button_text": "Redaktə et",
+    "edit_topsites_button_label": "Qabaqcıl Saytlar bölümünüzü fərdiləşdirin",
+    "edit_topsites_showmore_button": "Daha çox göstər",
+    "edit_topsites_showless_button": "Daha az göstər",
+    "edit_topsites_done_button": "Oldu",
+    "edit_topsites_pin_button": "Bu saytı sabitlə",
+    "edit_topsites_edit_button": "Bu saytı düzəlt",
+    "edit_topsites_dismiss_button": "Bu saytı çıxart"
   },
   "be": {
     "newtab_page_title": "Новая картка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Папулярныя сайты",
     "header_highlights": "Выбранае",
     "type_label_visited": "Наведанае",
     "type_label_bookmarked": "У закладках",
@@ -370,17 +390,66 @@
     "edit_topsites_button_label": "Upravit oddíl Top stránek",
     "edit_topsites_showmore_button": "Zobrazit více",
     "edit_topsites_showless_button": "Zobrazit méně",
     "edit_topsites_done_button": "Hotovo",
     "edit_topsites_pin_button": "Připnout tuto stránku",
     "edit_topsites_edit_button": "Upravit tuto stránku",
     "edit_topsites_dismiss_button": "Skrýt tuto stránku"
   },
-  "cy": {},
+  "cy": {
+    "newtab_page_title": "Tab Newydd",
+    "default_label_loading": "Llwytho…",
+    "header_top_sites": "Hoff Wefannau",
+    "header_highlights": "Goreuon",
+    "type_label_visited": "Ymwelwyd",
+    "type_label_bookmarked": "Nod Tudalen",
+    "type_label_synced": "Cydweddwyd o ddyfais arall",
+    "type_label_open": "Ar Agor",
+    "type_label_topic": "Pwnc",
+    "menu_action_bookmark": "Nod Tudalen",
+    "menu_action_remove_bookmark": "Tynnu Nod Tudalen",
+    "menu_action_copy_address": "Copïo'r Cyfeiriad",
+    "menu_action_email_link": "Dolen E-bost…",
+    "menu_action_open_new_window": "Agor Ffenestr Newydd",
+    "menu_action_open_private_window": "Agor mewn Ffenestr Preifat Newydd",
+    "menu_action_dismiss": "Cau",
+    "menu_action_delete": "Dileu o'r Hanes",
+    "search_for_something_with": "Chwilio am {search_term} gyda:",
+    "search_button": "Chwilio",
+    "search_header": "{search_engine_name} Chwilio",
+    "search_web_placeholder": "Chwilio'r We",
+    "search_settings": "Newid y Gosodiadau Chwilio",
+    "welcome_title": "Croeso i dab newydd",
+    "welcome_body": "Bydd Firefox yn defnyddio'r gofod hwn i ddangos y nodau tudalen, erthyglau, fideos a thudalennau mwyaf perthnasol i chi, a thudalennau fuoch yn ymweld â nhw'n ddiweddar, fel bod modd i chi ddychwelydd atyn nhw'n hawdd.",
+    "welcome_label": "Adnabod eich Goreuon",
+    "time_label_less_than_minute": "<1m",
+    "time_label_minute": "{number}m",
+    "time_label_hour": "{number}a",
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Cyfaddasu eich tudalen Tab Newydd",
+    "settings_pane_header": "Dewisiadau Tab Newydd",
+    "settings_pane_body": "Dewis beth rydych yn ei weld pan fyddwch yn agor tab newydd.",
+    "settings_pane_search_header": "Chwilio",
+    "settings_pane_search_body": "Chwilio'r We o'ch tab newydd.",
+    "settings_pane_topsites_header": "Hoff Wefannau",
+    "settings_pane_topsites_body": "Cael mynediad at y gwefannau rydych yn ymweld â nhw amlaf.",
+    "settings_pane_topsites_options_showmore": "Dangos dwy res",
+    "settings_pane_highlights_header": "Goreuon",
+    "settings_pane_highlights_body": "Edrych nôl ar eich hanes pori a nodau tudalen diweddar.",
+    "settings_pane_done_button": "Gorffen",
+    "edit_topsites_button_text": "Golygu",
+    "edit_topsites_button_label": "Cyfaddasu eich adran Hoff Wefannau",
+    "edit_topsites_showmore_button": "Dangos rhagor",
+    "edit_topsites_showless_button": "Dangos llai",
+    "edit_topsites_done_button": "Gorffen",
+    "edit_topsites_pin_button": "Pinio'r wefan",
+    "edit_topsites_edit_button": "Golygu'r wefan",
+    "edit_topsites_dismiss_button": "Dileu'r wefan"
+  },
   "da": {
     "newtab_page_title": "Nyt faneblad",
     "default_label_loading": "Indlæser…",
     "header_top_sites": "Mest besøgte websider",
     "header_highlights": "Højdepunkter",
     "type_label_visited": "Besøgt",
     "type_label_bookmarked": "Bogmærket",
     "type_label_synced": "Synkroniseret fra en anden enhed",
@@ -390,26 +459,46 @@
     "menu_action_remove_bookmark": "Fjern bogmærke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send link…",
     "menu_action_open_new_window": "Åbn i et nyt vindue",
     "menu_action_open_private_window": "Åbn i et nyt privat vindue",
     "menu_action_dismiss": "Afvis",
     "menu_action_delete": "Slet fra historik",
     "search_for_something_with": "Søg efter {search_term} med:",
+    "search_button": "Søg",
     "search_header": "{search_engine_name}-søgning",
     "search_web_placeholder": "Søg på internettet",
     "search_settings": "Skift søgeindstillinger",
     "welcome_title": "Velkommen til nyt faneblad",
     "welcome_body": "Firefox vil bruge denne plads til at vise dine mest relevante bogmærker, artikler, videoer og sider, du har besøgt for nylig - så kan du nemmere finde dem.",
     "welcome_label": "Finder dine højdepunkter",
     "time_label_less_than_minute": "<1 m.",
     "time_label_minute": "{number} m.",
     "time_label_hour": "{number} t.",
-    "time_label_day": "{number} d."
+    "time_label_day": "{number} d.",
+    "settings_pane_button_label": "Tilpas siden Nyt faneblad",
+    "settings_pane_header": "Indstillinger for Nyt faneblad",
+    "settings_pane_body": "Vælg, hvad der vises, når du åbner et nyt faneblad.",
+    "settings_pane_search_header": "Søgning",
+    "settings_pane_search_body": "Søg på nettet fra Nyt faneblad.",
+    "settings_pane_topsites_header": "Mest besøgte websider",
+    "settings_pane_topsites_body": "Adgang til de websider, du besøger oftest.",
+    "settings_pane_topsites_options_showmore": "Vis to rækker",
+    "settings_pane_highlights_header": "Højdepunkter",
+    "settings_pane_highlights_body": "Se tilbage på din seneste browserhistorik og nyligt oprettede bogmærker.",
+    "settings_pane_done_button": "Færdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
+    "edit_topsites_showmore_button": "Vis flere",
+    "edit_topsites_showless_button": "Vis færre",
+    "edit_topsites_done_button": "Færdig",
+    "edit_topsites_pin_button": "Fastgør denne webside",
+    "edit_topsites_edit_button": "Rediger denne webside",
+    "edit_topsites_dismiss_button": "Afvis denne webside"
   },
   "de": {
     "newtab_page_title": "Neuer Tab",
     "default_label_loading": "Wird geladen…",
     "header_top_sites": "Meistbesuchte Seiten",
     "header_highlights": "Wichtige Seiten",
     "type_label_visited": "Besucht",
     "type_label_bookmarked": "Lesezeichen",
@@ -657,16 +746,25 @@
     "edit_topsites_button_label": "Customize your Top Sites section",
     "edit_topsites_showmore_button": "Show more",
     "edit_topsites_showless_button": "Show less",
     "edit_topsites_done_button": "Done",
     "edit_topsites_pin_button": "Pin this site",
     "edit_topsites_unpin_button": "Unpin this site",
     "edit_topsites_edit_button": "Edit this site",
     "edit_topsites_dismiss_button": "Dismiss this site",
+    "edit_topsites_add_button": "Add",
+    "topsites_form_add_header": "New Top Site",
+    "topsites_form_edit_header": "Edit Top Site",
+    "topsites_form_title_placeholder": "Enter a title",
+    "topsites_form_url_placeholder": "Type or paste a URL",
+    "topsites_form_add_button": "Add",
+    "topsites_form_save_button": "Save",
+    "topsites_form_cancel_button": "Cancel",
+    "topsites_form_url_validation": "Valid URL required",
     "pocket_read_more": "Popular Topics:",
     "pocket_read_even_more": "View More Stories",
     "pocket_feedback_header": "The best of the web, curated by over 25 million people.",
     "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
     "pocket_send_feedback": "Send Feedback"
   },
   "en-ZA": {},
   "eo": {
@@ -683,26 +781,28 @@
     "menu_action_remove_bookmark": "Forigi legosignon",
     "menu_action_copy_address": "Kopii adreson",
     "menu_action_email_link": "Sendi ligilon retpoŝte…",
     "menu_action_open_new_window": "Malfermi en nova fenestro",
     "menu_action_open_private_window": "Malfermi en nova privata fenestro",
     "menu_action_dismiss": "Ignori",
     "menu_action_delete": "Forigi el historio",
     "search_for_something_with": "Serĉi {search_term} per:",
+    "search_button": "Serĉi",
     "search_header": "Serĉo de {search_engine_name}",
     "search_web_placeholder": "Serĉi la Teksaĵon",
     "search_settings": "Modifi serĉajn agordojn",
     "welcome_title": "Bonvenon al nova langeto",
     "welcome_body": "Firefox uzos tiun ĉi spacon por montri al vi viaj plej gravajn legosignojn, artikolojn, filmetojn kaj paĝojn, kiujn vi vizitis antaŭ nelonge, tiel ke vi povos reiri al ili facile.",
     "welcome_label": "Elstaraĵoj identigataj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}t"
+    "time_label_day": "{number}t",
+    "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj"
   },
   "es-AR": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Más visitados",
     "header_highlights": "Destacados",
     "type_label_visited": "Visitados",
     "type_label_bookmarked": "Marcados",
@@ -913,26 +1013,46 @@
     "menu_action_remove_bookmark": "Eemalda järjehoidja",
     "menu_action_copy_address": "Kopeeri aadress",
     "menu_action_email_link": "Saada link e-postiga…",
     "menu_action_open_new_window": "Ava uues aknas",
     "menu_action_open_private_window": "Ava uues privaatses aknas",
     "menu_action_dismiss": "Peida",
     "menu_action_delete": "Kustuta ajaloost",
     "search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:",
+    "search_button": "Otsi",
     "search_header": "{search_engine_name}",
     "search_web_placeholder": "Otsi veebist",
     "search_settings": "Muuda otsingu sätteid",
     "welcome_title": "Tere tulemast uuele kaardile",
     "welcome_body": "Firefox kasutab seda lehte, et kuvada sulle kõige olulisemaid järjehoidjaid, artikleid, videoid ja lehti, mida oled hiljuti külastanud, nii et pääseksid kergelt nende juurde tagasi.",
     "welcome_label": "Esiletõstetava sisu tuvastamine",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}t",
-    "time_label_day": "{number}p"
+    "time_label_day": "{number}p",
+    "settings_pane_button_label": "Kohanda uue kaardi lehte",
+    "settings_pane_header": "Uue kaardi sätted",
+    "settings_pane_body": "Vali asjad, mida soovid uue kaardi avamisel näha.",
+    "settings_pane_search_header": "Otsi",
+    "settings_pane_search_body": "Veebis otsimine uuel kaardil.",
+    "settings_pane_topsites_header": "Top saidid",
+    "settings_pane_topsites_body": "Ligipääs enim külastatud veebilehtedele.",
+    "settings_pane_topsites_options_showmore": "Kuvatakse kahel real",
+    "settings_pane_highlights_header": "Esiletõstetud",
+    "settings_pane_highlights_body": "Tagasivaade hiljutisele lehitsemisajaloole ning lisatud järjehoidjatele.",
+    "settings_pane_done_button": "Valmis",
+    "edit_topsites_button_text": "Muuda",
+    "edit_topsites_button_label": "Kohanda top saitide osa",
+    "edit_topsites_showmore_button": "Kuva rohkem",
+    "edit_topsites_showless_button": "Näita vähem",
+    "edit_topsites_done_button": "Valmis",
+    "edit_topsites_pin_button": "Kinnita see sait",
+    "edit_topsites_edit_button": "Muuda seda saiti",
+    "edit_topsites_dismiss_button": "Peida see sait"
   },
   "eu": {},
   "fa": {
     "newtab_page_title": "زبانه جدید",
     "default_label_loading": "در حال بارگیری…",
     "header_top_sites": "سایت‌های برتر",
     "header_highlights": "برجسته‌ها",
     "type_label_visited": "مشاهده شده",
@@ -1217,27 +1337,97 @@
     "menu_action_remove_bookmark": "הסרת סימניה",
     "menu_action_copy_address": "העתקת כתובת",
     "menu_action_email_link": "שליחת קישור בדוא״ל…",
     "menu_action_open_new_window": "פתיחה בחלון חדש",
     "menu_action_open_private_window": "פתיחה בלשונית פרטית חדשה",
     "menu_action_dismiss": "ביטול",
     "menu_action_delete": "מחיקה מההיסטוריה",
     "search_for_something_with": "חיפוש אחר {search_term} עם:",
+    "search_button": "חיפוש",
     "search_header": "חיפוש ב־{search_engine_name}",
     "search_web_placeholder": "חיפוש ברשת",
     "search_settings": "שינוי הגדרות חיפוש",
     "welcome_title": "ברוכים הבאים לדף הלשונית החדשה",
     "welcome_body": "Firefox ישתמש באזור זה כדי להציג את הסימניות הרלוונטיות ביותר, מאמרים, סרטוני וידאו ודפים שביקרת בהם לאחרונה, כך שניתן יהיה לגשת אליהם שוב בקלות.",
+    "welcome_label": "תחומי העניין שלך מזוהים",
     "time_label_less_than_minute": "פחות מדקה",
     "time_label_minute": "{number} דקות",
     "time_label_hour": "{number} שעות",
-    "time_label_day": "{number} ימים"
+    "time_label_day": "{number} ימים",
+    "settings_pane_button_label": "התאמה אישית של דף הלשונית החדשה שלך",
+    "settings_pane_header": "העדפות לשונית חדשה",
+    "settings_pane_body": "ניתן לבחור מה יופיע בפניך בעת פתיחת לשונית חדשה.",
+    "settings_pane_search_header": "חיפוש",
+    "settings_pane_search_body": "חיפוש באינטרנט ישירות מהלשונית החדשה שלך.",
+    "settings_pane_topsites_header": "אתרים מובילים",
+    "settings_pane_topsites_body": "גישה לאתרים בהם ביקרת הכי הרבה.",
+    "settings_pane_topsites_options_showmore": "הצגת שתי שורות",
+    "settings_pane_highlights_header": "המלצות",
+    "settings_pane_highlights_body": "ניתן להסתכל על היסטוריית הגלישה העדכנית שלך ועל הסימניות האחרונות שנוצרו.",
+    "settings_pane_done_button": "סיום",
+    "edit_topsites_button_text": "עריכה",
+    "edit_topsites_button_label": "התאמת אגף האתרים המובילים שלך",
+    "edit_topsites_showmore_button": "להציג יותר",
+    "edit_topsites_showless_button": "להציג פחות",
+    "edit_topsites_done_button": "בוצע",
+    "edit_topsites_pin_button": "נעיצת אתר זה",
+    "edit_topsites_edit_button": "עריכת אתר זה",
+    "edit_topsites_dismiss_button": "התעלמות מאתר זה"
   },
-  "hi-IN": {},
+  "hi-IN": {
+    "newtab_page_title": "नया टैब",
+    "default_label_loading": "लोड हो रहा है…",
+    "header_top_sites": "सर्वोच्च साइटें",
+    "header_highlights": "प्रमुखताएँ",
+    "type_label_visited": "देखी गई",
+    "type_label_bookmarked": "पुस्तचिह्न लगाया हुआ",
+    "type_label_synced": "किसी अन्य उपकरण से समकालीन किया गया",
+    "type_label_open": "खोलें",
+    "type_label_topic": "विषय",
+    "menu_action_bookmark": "पुस्तचिह्न",
+    "menu_action_remove_bookmark": "पुस्तचिह्न हटाएँ",
+    "menu_action_copy_address": "पता कॉपी करें",
+    "menu_action_email_link": "ईमेल लिंक…",
+    "menu_action_open_new_window": "एक नई विंडो में खोलें",
+    "menu_action_open_private_window": "एक नई निजी विंडो में खोलें",
+    "menu_action_dismiss": "निरस्त करें",
+    "menu_action_delete": "इतिहास से मिटाएँ",
+    "search_for_something_with": "इस के साथ {search_term} के लिए खोजें:",
+    "search_button": "खोज",
+    "search_header": "{search_engine_name} खोज",
+    "search_web_placeholder": "वेब पर खोजें",
+    "search_settings": "खोज सेटिंग बदलें",
+    "welcome_title": "नए टैब में आपका स्वागत है",
+    "welcome_body": "Firefox यह जगह आपके सर्वाधिक प्रासंगिक पुस्तचिन्ह, लेख, वीडियो और पृष्ठों जिनका आपने हाल ही में दौरा किया है उनको दर्शाने के लिए करेगा, ताकि आप बाद में उन तक आसानी से वापस जा सकें.",
+    "welcome_label": "आपके प्रमुखताओं की पहचान की जा रही है",
+    "time_label_less_than_minute": "<1मि0",
+    "time_label_minute": "{number}मि0",
+    "time_label_hour": "{number}मि0",
+    "time_label_day": "{number}दिन",
+    "settings_pane_button_label": "अपने नए टैब पृष्ठ को अनुकूलित करें",
+    "settings_pane_header": "नयी टैब वरीयताएँ",
+    "settings_pane_body": "चयन करें कि नया टैब खोलने पर आप क्या देखें.",
+    "settings_pane_search_header": "खोज",
+    "settings_pane_search_body": "अपने नए टैब से वेब पर खोजें.",
+    "settings_pane_topsites_header": "सर्वोच्च साइटें",
+    "settings_pane_topsites_body": "आपके द्वारा सबसे ज्यादा खोजी जाने वाली वेबसाइट्स देखें.",
+    "settings_pane_topsites_options_showmore": "दो पंक्तियाँ दिखाएँ",
+    "settings_pane_highlights_header": "प्रमुखताएँ",
+    "settings_pane_highlights_body": "अपने हाल के ब्राउज़िंग इतिहास और नए बनाए गए पुस्तचिन्हों को वापस देखें.",
+    "settings_pane_done_button": "संपन्न",
+    "edit_topsites_button_text": "संपादित करें",
+    "edit_topsites_button_label": "अपने शीर्ष साइट्स अनुभाग को अनुकूलित करें",
+    "edit_topsites_showmore_button": "अधिक दिखाएँ",
+    "edit_topsites_showless_button": "कम दिखाएँ",
+    "edit_topsites_done_button": "पूर्ण",
+    "edit_topsites_pin_button": "इस साइट को पिन करें",
+    "edit_topsites_edit_button": "इस साइट को संपादित करें",
+    "edit_topsites_dismiss_button": "इस साइट को ख़ारिज करें"
+  },
   "hr": {
     "newtab_page_title": "Nova kartica",
     "default_label_loading": "Učitavanje…",
     "header_top_sites": "Najbolje stranice",
     "header_highlights": "Istaknuto",
     "type_label_visited": "Posjećeno",
     "type_label_bookmarked": "Zabilježeno",
     "type_label_synced": "Sinkronizirano s drugog uređaja",
@@ -1247,26 +1437,46 @@
     "menu_action_remove_bookmark": "Ukloni zabilješku",
     "menu_action_copy_address": "Kopiraj adresu",
     "menu_action_email_link": "Pošalji poveznicu e-poštom…",
     "menu_action_open_new_window": "Otvori u novom prozoru",
     "menu_action_open_private_window": "Otvori u novom privatnom prozoru",
     "menu_action_dismiss": "Odbaci",
     "menu_action_delete": "Obriši iz povijesti",
     "search_for_something_with": "Traži {search_term} s:",
+    "search_button": "Traži",
     "search_header": "{search_engine_name} pretraživanje",
     "search_web_placeholder": "Pretraži web",
     "search_settings": "Promijeni postavke pretraživanja",
     "welcome_title": "Dobro došli u novu karticu",
     "welcome_body": "Firefox će koristiti ovaj prostor kako bi vam pokazao najbitnije zabilješke, članke, video uratke i stranice koje ste nedavno posjetili, tako da se možete lako vratiti na njih.",
     "welcome_label": "Identificiranje istaknutog",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Prilagodite svoju početnu stranicu nove kartice",
+    "settings_pane_header": "Postavke nove kartice",
+    "settings_pane_body": "Odaberite što ćete vidjeti kada otvorite novu karticu.",
+    "settings_pane_search_header": "Traži",
+    "settings_pane_search_body": "Pretražite Web iz nove kartice.",
+    "settings_pane_topsites_header": "Najbolje stranice",
+    "settings_pane_topsites_body": "Pristupite stranicama koje najčešće posjećujete.",
+    "settings_pane_topsites_options_showmore": "Prikaži dva reda",
+    "settings_pane_highlights_header": "Istaknuto",
+    "settings_pane_highlights_body": "Osvrnite se na nedavno posjećene stranice i nove zabilješke.",
+    "settings_pane_done_button": "Gotovo",
+    "edit_topsites_button_text": "Uredi",
+    "edit_topsites_button_label": "Prilagodite odjel s najboljim stranicama",
+    "edit_topsites_showmore_button": "Prikaži više",
+    "edit_topsites_showless_button": "Prikaži manje",
+    "edit_topsites_done_button": "Gotovo",
+    "edit_topsites_pin_button": "Zakači stranicu",
+    "edit_topsites_edit_button": "Uredi ovu stranicu",
+    "edit_topsites_dismiss_button": "Odbaci stranicu"
   },
   "hsb": {
     "newtab_page_title": "Nowy rajtark",
     "default_label_loading": "Začituje so…",
     "header_top_sites": "Najhusćišo wopytane sydła",
     "header_highlights": "Wjerški",
     "type_label_visited": "Wopytany",
     "type_label_bookmarked": "Jako zapołožka składowany",
@@ -1690,26 +1900,46 @@
     "menu_action_remove_bookmark": "즐겨찾기 삭제",
     "menu_action_copy_address": "주소 복사",
     "menu_action_email_link": "메일로 링크 보내기…",
     "menu_action_open_new_window": "새 창에서 열기",
     "menu_action_open_private_window": "새 사생활 보호 창에서 열기",
     "menu_action_dismiss": "닫기",
     "menu_action_delete": "방문 기록에서 삭제",
     "search_for_something_with": "다음에서 {search_term} 검색:",
+    "search_button": "검색",
     "search_header": "{search_engine_name} 검색",
     "search_web_placeholder": "웹 검색",
     "search_settings": "검색 설정 바꾸기",
     "welcome_title": "새 탭을 소개합니다",
     "welcome_body": "최근에 방문한 관련있는 즐겨찾기나 글, 동영상, 페이지를 Firefox가 여기에 표시해서 쉽게 다시 찾아볼 수 있게 할 것입니다.",
     "welcome_label": "하이라이트 확인",
     "time_label_less_than_minute": "<1분",
     "time_label_minute": "{number}분",
     "time_label_hour": "{number}시",
-    "time_label_day": "{number}일"
+    "time_label_day": "{number}일",
+    "settings_pane_button_label": "새 탭 페이지 꾸미기",
+    "settings_pane_header": "새 탭 설정",
+    "settings_pane_body": "새 탭을 열 때 어떤 화면을 볼지 선택하세요.",
+    "settings_pane_search_header": "검색",
+    "settings_pane_search_body": "새 탭에서 웹을 검색하세요.",
+    "settings_pane_topsites_header": "상위 사이트",
+    "settings_pane_topsites_body": "가장 많이 방문한 웹 사이트에 접근하세요.",
+    "settings_pane_topsites_options_showmore": "두 줄로 보기",
+    "settings_pane_highlights_header": "하이라이트",
+    "settings_pane_highlights_body": "최근 방문 기록과 북마크를 살펴보세요.",
+    "settings_pane_done_button": "완료",
+    "edit_topsites_button_text": "수정",
+    "edit_topsites_button_label": "상위 사이트 영역 꾸미기",
+    "edit_topsites_showmore_button": "더보기",
+    "edit_topsites_showless_button": "줄이기",
+    "edit_topsites_done_button": "완료",
+    "edit_topsites_pin_button": "이 사이트 고정",
+    "edit_topsites_edit_button": "이 사이트 수정",
+    "edit_topsites_dismiss_button": "이 사이트 제거"
   },
   "ku": {},
   "lij": {
     "newtab_page_title": "Neuvo Feuggio",
     "default_label_loading": "Carego…",
     "header_top_sites": "I megio sciti",
     "header_highlights": "In evidensa",
     "type_label_visited": "Vixitou",
@@ -1771,25 +2001,46 @@
     "menu_action_remove_bookmark": "ລຶບບຸກມາກອອກ",
     "menu_action_copy_address": "ສຳເນົາທີ່ຢູ່",
     "menu_action_email_link": "ລີ້ງອີເມວ…",
     "menu_action_open_new_window": "ເປີດລີ້ງໃນຫນ້າຕ່າງໃຫມ່",
     "menu_action_open_private_window": "ເປີດໃນຫນ້າຕ່າງສ່ວນຕົວໃຫມ່",
     "menu_action_dismiss": "ຍົກເລີກ",
     "menu_action_delete": "ລຶບອອກຈາກປະຫວັດການນຳໃຊ້",
     "search_for_something_with": "ຄົ້ນຫາສໍາລັບ {search_term} ດ້ວຍ:",
+    "search_button": "ຊອກ​ຫາ",
     "search_header": "ຄົ້ນຫາ {search_engine_name}",
     "search_web_placeholder": "ຄົ້ນຫາເວັບ",
     "search_settings": "ປ່ຽນການຕັ້ງຄ່າການຄົ້ນຫາ",
     "welcome_title": "ຍິນດີຕອນຮັບເຂົ້າສູ່ແຖບໃຫມ່",
+    "welcome_body": "Firefox ຈະໃຊ້ພື້ນທີ່ນີ້ເພື່ອສະແດງໃຫ້ເຫັນບຸກມາກທີ່ກ່ຽວຂ້ອງທີ່ສຸດຂອງທ່ານ, ບົດຄວາມ, ວິດີໂອ, ແລະ ຫນ້າທີ່ທ່ານຫາກາໄດ້ເຂົ້າໄປເບິງ, ສະນັ້ນທ່ານຈຶ່ງສາມາດກັບໄປເບິງຄືນອີກໄດ້ຢ່າງງ່າຍດາຍ.",
     "welcome_label": "ກໍາລັງລະບຸລາຍການເດັ່ນຂອງທ່ານ",
     "time_label_less_than_minute": "<1 ນາທີ",
     "time_label_minute": "{number} ນາທີ",
     "time_label_hour": "{number} ຊົ່ວໂມງ",
-    "time_label_day": "{number} ມື້"
+    "time_label_day": "{number} ມື້",
+    "settings_pane_button_label": "ປັບແຕ່ງຫນ້າແທັບໃຫມ່ຂອງທ່ານ",
+    "settings_pane_header": "ການຕັ້ງຄ່າແທັບໃຫມ່",
+    "settings_pane_body": "ເລືອກສິ່ງທີ່ທ່ານເຫັນເມື່ອທ່ານເປີດແທັບໃຫມ່.",
+    "settings_pane_search_header": "ຊອກຫາ",
+    "settings_pane_search_body": "ຊອກຫາເວັບຈາກແທັບໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_topsites_header": "ເວັບໄຊທ໌ຍອດນິຍົມ",
+    "settings_pane_topsites_body": "ເຂົ້າເວັບໄຊທ໌ທີ່ທ່ານໄດ້ເຂົ້າໄປຫລາຍທີ່ສຸດ.",
+    "settings_pane_topsites_options_showmore": "ສະແດງເປັນສອງແຖວ",
+    "settings_pane_highlights_header": "ຈຸດເດັ່ນ",
+    "settings_pane_highlights_body": "ຍ້ອນຄືນກັບໄປເບິງປະຫວັດການທ່ອງເວັບທີ່ຫາກາເຂົ້າໄປ ແລະ ບຸກມາກທີ່ໄດ້ຮັບການສ້າງຂື້ນມາໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_done_button": "ສຳເລັດ",
+    "edit_topsites_button_text": "ແກ້ໄຂ",
+    "edit_topsites_button_label": "ປັບແຕ່ງພາກສ່ວນເວັບໄຊທ໌ຍອດນິຍົມຂອງທ່ານ",
+    "edit_topsites_showmore_button": "ສະແດງເພີ່ມເຕີມ",
+    "edit_topsites_showless_button": "ສະແດງນ້ອຍລົງ",
+    "edit_topsites_done_button": "ສຳເລັດ",
+    "edit_topsites_pin_button": "Pin ເວັບໄຊທ໌ນີ້",
+    "edit_topsites_edit_button": "ແກ້ໄຂເວັບໄຊທ໌ນີ້",
+    "edit_topsites_dismiss_button": "ຍົກເລີກເວັບໄຊທ໌ນີ້"
   },
   "lt": {
     "newtab_page_title": "Nauja kortelė",
     "default_label_loading": "Įkeliama…",
     "header_top_sites": "Lankomiausios svetainės",
     "header_highlights": "Akcentai",
     "type_label_visited": "Aplankyti",
     "type_label_bookmarked": "Adresyne",
@@ -1832,17 +2083,19 @@
     "edit_topsites_showmore_button": "Rodyti daugiau",
     "edit_topsites_showless_button": "Rodyti mažiau",
     "edit_topsites_done_button": "Atlikta",
     "edit_topsites_pin_button": "Įsegti šią svetainę",
     "edit_topsites_edit_button": "Redaguoti šią svetainę",
     "edit_topsites_dismiss_button": "Paslėpti šią svetainę"
   },
   "ltg": {},
-  "lv": {},
+  "lv": {
+    "newtab_page_title": "Jauna cilne"
+  },
   "mai": {},
   "mk": {},
   "ml": {},
   "mn": {},
   "mr": {},
   "ms": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuatkan…",
@@ -1888,17 +2141,66 @@
     "edit_topsites_button_label": "Sesuaikan bahagian Laman Teratas anda",
     "edit_topsites_showmore_button": "Papar selanjutnya",
     "edit_topsites_showless_button": "Papar minima",
     "edit_topsites_done_button": "Siap",
     "edit_topsites_pin_button": "Pin laman ini",
     "edit_topsites_edit_button": "Edit laman ini",
     "edit_topsites_dismiss_button": "Buang laman ini"
   },
-  "my": {},
+  "my": {
+    "newtab_page_title": "တပ်ဗ်အသစ်ဖွင့်",
+    "default_label_loading": "ရယူနေသှ်…",
+    "header_top_sites": "အများဆုံးသုံးဆိုက်များ",
+    "header_highlights": "အသားပေးဖော်ပြချက်များ",
+    "type_label_visited": "သွားလည်ခဲ့သော",
+    "type_label_bookmarked": "စာအမှတ်မှတ်ထားသော",
+    "type_label_synced": "အခြားပစ္စည်းတစ်ခုမှရယူထားသှ်",
+    "type_label_open": "ဖွင့်ပါ",
+    "type_label_topic": "အကြောင်းအရာ",
+    "menu_action_bookmark": "စာအမှတ်",
+    "menu_action_remove_bookmark": "စာအမှတ်အားဖယ်ပါ",
+    "menu_action_copy_address": "လိပ်စာအားကူးယူပါ",
+    "menu_action_email_link": "လင်ခ့်အားအီးမေလ်းဖြင့်ပို့ပါ…",
+    "menu_action_open_new_window": "အခြားဝင်းဒိုးတစ်ခုမှဖွင့်ပါ",
+    "menu_action_open_private_window": "အခြားတစ်ကိုယ်ရေသုံးဝင်းဒိုးတစ်ခုဖွင့်ပါ",
+    "menu_action_dismiss": "ပိတ်လိုက်ပါ",
+    "menu_action_delete": "မှတ်တမ်းမှ ဖျက်ပါ",
+    "search_for_something_with": "{search_term} အားရှာပါ -",
+    "search_button": "ရှာ",
+    "search_header": "{search_engine_name} ရှာဖွေမှု",
+    "search_web_placeholder": "ဝတ်ဘ်ပေါ်တွင် ရှာဖွေခြင်း",
+    "search_settings": "ရှာဖွေမှုအပြင်အဆင်အားပြောင်းလဲပါ",
+    "welcome_title": "တပ်ဗ်အသစ်တစ်ခုမှကြိုဆိုပါတယ်",
+    "welcome_body": "ယခုနေရာအား Firefox မှ အသင့်လျော်ဆုံး သင်သွားလည်ခဲ့ဖူးသော စာအမှတ်များ၊ ဆောင်းပါးများ၊ ရုပ်ရှင်များ နှင့် စာမျက်နှာများအား ပြသဖို့အသုံးပြုမည်ဖြစ်ပါတယ်။",
+    "welcome_label": "သင့် အသားပေးဖော်ပြချက်များကိုသတိထားမည်",
+    "time_label_less_than_minute": "<1မီတာ",
+    "time_label_minute": "{number}မီတာ",
+    "time_label_hour": "{number}အမြင့်",
+    "time_label_day": "{number}နေ့",
+    "settings_pane_button_label": "သင့်တပ်ဗ်အသစ်စာမျက်နှာအား ပြင်ဆင်မည်",
+    "settings_pane_header": "စာတပ်ဗ်အသစ်အပြင်အဆင်များ",
+    "settings_pane_body": "သင် တပ်ဗ်အသစ်ဖွင့်လိုက်ပါကမြင်ရမည့်အရာများကိုရွေးချယ်ပါ",
+    "settings_pane_search_header": "ရှာဖွေပါ",
+    "settings_pane_search_body": "ဝက်ဘ်ပေါ်တွင် သင့်တပ်ဗ်အသစ်မှရှာဖွေပါ",
+    "settings_pane_topsites_header": "ထိပ်တန်းဝတ်ဘ်ဆိုက်များ",
+    "settings_pane_topsites_body": "သင်အများဆုံးသွားလည်သော ဝတ်ဘ်ဆိုက်များကို ရယူပါ",
+    "settings_pane_topsites_options_showmore": "အတန်းနှစ်တန်းနှင့်ပြပါ",
+    "settings_pane_highlights_header": "အသားပေးဖော်ပြချက်များ",
+    "settings_pane_highlights_body": "သင်လတ်တလောသွားလည်ထားသော မှတ်တမ်းနှင့် အသစ်ဖန်တီးထားသော စာအမှတ်များအား ပြန်ကြည့်ပါ",
+    "settings_pane_done_button": "ပြီးပြီ",
+    "edit_topsites_button_text": "ပြင်ဆင်မည်",
+    "edit_topsites_button_label": "သင့်ထိပ်တန်းဆိုက် အမြင်အားပြင်ဆင်ပါ",
+    "edit_topsites_showmore_button": "ထပ်ပြပါ",
+    "edit_topsites_showless_button": "ချုံ့ပြရန်",
+    "edit_topsites_done_button": "ပြီးပြီ",
+    "edit_topsites_pin_button": "ဝတ်ဆိုဒ်အားpinလုပ်ထားမည်",
+    "edit_topsites_edit_button": "ဆိုက်အားပြင်မည်",
+    "edit_topsites_dismiss_button": "ဆိုက်အားဖျက်လိုက်မည်"
+  },
   "nb-NO": {
     "newtab_page_title": "Ny fane",
     "default_label_loading": "Laster …",
     "header_top_sites": "Mest besøkte nettsider",
     "header_highlights": "Høydepunkter",
     "type_label_visited": "Besøkt",
     "type_label_bookmarked": "Bokmerket",
     "type_label_synced": "Synkronisert fra annen enhet",
@@ -1908,26 +2210,46 @@
     "menu_action_remove_bookmark": "Fjern bokmerke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send lenke på e-post …",
     "menu_action_open_new_window": "Åpne i nytt vindu",
     "menu_action_open_private_window": "Åpne i nytt privat vindu",
     "menu_action_dismiss": "Avslå",
     "menu_action_delete": "Slett fra historikk",
     "search_for_something_with": "Søk etter {search_term} med:",
+    "search_button": "Søk",
     "search_header": "{search_engine_name}-søk",
     "search_web_placeholder": "Søk på nettet",
     "search_settings": "Endre søkeinnstillinger",
     "welcome_title": "Velkommen til ny fane",
     "welcome_body": "Firefox vil bruke denne plassen til å vise deg de mest relevante bokmerkene, artiklene, videoene og sidene du nettopp har besøkt, slik at du enkelt kan finne tilbake til de.",
     "welcome_label": "Identifiserer dine høydepunkter",
     "time_label_less_than_minute": "<1 m",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} t",
-    "time_label_day": "{number} d"
+    "time_label_day": "{number} d",
+    "settings_pane_button_label": "Tilpass siden for Ny fane",
+    "settings_pane_header": "Innstillinger for Ny fane",
+    "settings_pane_body": "Velg hva som vises når du åpner en ny fane.",
+    "settings_pane_search_header": "Søk",
+    "settings_pane_search_body": "Søk på nettet fra din nye fane.",
+    "settings_pane_topsites_header": "Mest besøkte",
+    "settings_pane_topsites_body": "Tilgang til nettsidene du besøker mest.",
+    "settings_pane_topsites_options_showmore": "Vis to rader",
+    "settings_pane_highlights_header": "Høydepunkter",
+    "settings_pane_highlights_body": "Se tilbake på din siste nettleserhistorikk og nyopprettede bokmerker.",
+    "settings_pane_done_button": "Ferdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpass seksjonen Mest besøkte",
+    "edit_topsites_showmore_button": "Vis mer",
+    "edit_topsites_showless_button": "Vis mindre",
+    "edit_topsites_done_button": "Ferdig",
+    "edit_topsites_pin_button": "Fest nettsiden",
+    "edit_topsites_edit_button": "Rediger denne nettsiden",
+    "edit_topsites_dismiss_button": "Avvis denne nettsiden"
   },
   "ne-NP": {
     "newtab_page_title": "नयाँ ट्याब",
     "default_label_loading": "लोड हुदैँछ...",
     "header_top_sites": "शीर्ष साइटहरु",
     "header_highlights": "विशेषताहरू",
     "type_label_visited": "भ्रमण गरिएको",
     "type_label_bookmarked": "पुस्तकचिनो लागाइएको",
@@ -2155,21 +2477,21 @@
     "edit_topsites_dismiss_button": "Odrzuć tę stronę"
   },
   "pt-BR": {
     "newtab_page_title": "Nova aba",
     "default_label_loading": "Carregando…",
     "header_top_sites": "Sites preferidos",
     "header_highlights": "Destaques",
     "type_label_visited": "Visitado",
-    "type_label_bookmarked": "Favorito",
+    "type_label_bookmarked": "Adicionado aos favoritos",
     "type_label_synced": "Sincronizado a partir de outro dispositivo",
     "type_label_open": "Abrir",
     "type_label_topic": "Tópico",
-    "menu_action_bookmark": "Favoritos",
+    "menu_action_bookmark": "Adicionar aos favoritos",
     "menu_action_remove_bookmark": "Remover favorito",
     "menu_action_copy_address": "Copiar endereço",
     "menu_action_email_link": "Enviar link por e-mail…",
     "menu_action_open_new_window": "Abrir em uma nova janela",
     "menu_action_open_private_window": "Abrir em uma nova janela privativa",
     "menu_action_dismiss": "Dispensar",
     "menu_action_delete": "Excluir do histórico",
     "search_for_something_with": "Pesquisar por {search_term} com:",
@@ -2500,26 +2822,46 @@
     "menu_action_remove_bookmark": "Hiqe Faqerojtësin",
     "menu_action_copy_address": "Kopjoje Adresën",
     "menu_action_email_link": "Dërgoni Lidhje me Email…",
     "menu_action_open_new_window": "Hape në Dritare të Re",
     "menu_action_open_private_window": "Hape në Dritare të Re Private",
     "menu_action_dismiss": "Hidhe tej",
     "menu_action_delete": "Fshije prej Historiku",
     "search_for_something_with": "Kërko për {search_term} me:",
+    "search_button": "Kërko",
     "search_header": "Kërkim me {search_engine_name}",
     "search_web_placeholder": "Kërkoni në Web",
     "search_settings": "Ndryshoji Rregullimet e Kërkimit",
     "welcome_title": "Mirë se vini te skedë e re",
     "welcome_body": "Firefox-i do ta përdorë këtë hapësirë për t’ju shfaqur faqerojtësit, artikujt, videot dhe faqet më me peshë që keni vizituar së fundi, që kështu të mund të ktheheni lehtë në to.",
     "welcome_label": "Po identifikohen Highlights tuaj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Personalizoni faqen tuaj Skedë e Re",
+    "settings_pane_header": "Parapëlqime për Skedë të Re",
+    "settings_pane_body": "Zgjidhni ç’doni të shihni kur hapni një skedë të re.",
+    "settings_pane_search_header": "Kërko",
+    "settings_pane_search_body": "Kërkoni në Web prej skedës tuaj të re.",
+    "settings_pane_topsites_header": "Sajte Kryesues",
+    "settings_pane_topsites_body": "Hyni te sajtet që vizitoni më shpesh.",
+    "settings_pane_topsites_options_showmore": "Shfaq dy rreshta",
+    "settings_pane_highlights_header": "Në Pah",
+    "settings_pane_highlights_body": "Rikthejuni historikut të shfletimeve të fundit dhe faqerojtësve të krijuar rishtas.",
+    "settings_pane_done_button": "U bë",
+    "edit_topsites_button_text": "Përpunoni",
+    "edit_topsites_button_label": "Personalizoni ndarjen tuaj Sajte Kryesues",
+    "edit_topsites_showmore_button": "Shfaq më tepër",
+    "edit_topsites_showless_button": "Shfaq më pak",
+    "edit_topsites_done_button": "U bë",
+    "edit_topsites_pin_button": "Fiksoje këtë sajt",
+    "edit_topsites_edit_button": "Përpunoni këtë sajt",
+    "edit_topsites_dismiss_button": "Hidhe tej këtë sajt"
   },
   "sr": {
     "newtab_page_title": "Нови језичак",
     "default_label_loading": "Учитавање…",
     "header_top_sites": "Популарни сајтови",
     "header_highlights": "Истакнути",
     "type_label_visited": "Посећене",
     "type_label_bookmarked": "Забележено",
@@ -2632,38 +2974,58 @@
     "menu_action_remove_bookmark": "ఇష్టాంశాన్ని తొలగించు",
     "menu_action_copy_address": "చిరునామా కాపీ చెయ్యండి",
     "menu_action_email_link": "ఈమెయిలు లింకు…",
     "menu_action_open_new_window": "కొత్త విండోలో తెరువు",
     "menu_action_open_private_window": "కొత్త వ్యక్తిగత విండోలో తెరువు",
     "menu_action_dismiss": "విస్మరించు",
     "menu_action_delete": "చరిత్ర నుంచి తీసివేయి",
     "search_for_something_with": "{search_term} కోసం దీని సాయంతో వెతుకు:",
+    "search_button": "వెతకండి",
     "search_header": "{search_engine_name} శోధన",
-    "search_web_placeholder": "వెబ్ లో వెతకండి",
+    "search_web_placeholder": "జాలంలో వెతకండి",
     "search_settings": "శోధన అమరికలు మార్చు",
     "welcome_title": "కొత్త ట్యాబుకు స్వాగతం",
     "welcome_body": "సముచితమైన మీ ఇష్టాంశాలను, వ్యాసాలను, వీడియోలను, ఇంకా మీరు ఇటీవలే చూసిన పేజీలను మీకు తేలిగ్గా అందుబాటులో ఉంచేందుకు Firefox ఈ జాగాని వాడుకుంటుంది.",
     "welcome_label": "మీ ముఖ్యాంశాలను గుర్తిస్తున్నది",
     "time_label_less_than_minute": "<1ని",
     "time_label_minute": "{number}ని",
     "time_label_hour": "{number}గం",
-    "time_label_day": "{number}రో"
+    "time_label_day": "{number}రో",
+    "settings_pane_button_label": "మీ కొత్త ట్యాబు పేజీని మలచుకోండి",
+    "settings_pane_header": "కొత్త ట్యాబు అభిరుచులు",
+    "settings_pane_body": "మీరు కొత్త ట్యాబు తెరిచినప్పుడు ఏం చూడాలో ఎంచుకోండి.",
+    "settings_pane_search_header": "వెతకడం",
+    "settings_pane_search_body": "కొత్త ట్యాబు నుండే జాలంలో వెతకండి.",
+    "settings_pane_topsites_header": "మేటి సైట్లు",
+    "settings_pane_topsites_body": "మీరు ఎక్కువగా చూసే వెబ్‌సైట్లను చూడండి.",
+    "settings_pane_topsites_options_showmore": "రెండు వరుసలు చూపించు",
+    "settings_pane_highlights_header": "విశేషాలు",
+    "settings_pane_highlights_body": "మీ ఇటీవలి విహరణ చరిత్రనూ కొత్త ఇష్టాంశాలను చూడండి.",
+    "settings_pane_done_button": "పూర్తయింది",
+    "edit_topsites_button_text": "మార్చు",
+    "edit_topsites_button_label": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి",
+    "edit_topsites_showmore_button": "ఇంకా చూపించు",
+    "edit_topsites_showless_button": "కొన్నే చూపించు",
+    "edit_topsites_done_button": "పూర్తయింది",
+    "edit_topsites_pin_button": "ఈ సైటును ఇక్కడ గుచ్చు",
+    "edit_topsites_edit_button": "ఈ సైటును మార్చు",
+    "edit_topsites_dismiss_button": "ఈ సైటుని తీసివేయి"
   },
   "th": {
     "newtab_page_title": "แท็บใหม่",
     "default_label_loading": "กำลังโหลด…",
     "header_top_sites": "ไซต์เด่น",
     "header_highlights": "รายการเด่น",
     "type_label_visited": "เยี่ยมชมแล้ว",
-    "type_label_bookmarked": "คั่นหน้าแล้ว",
+    "type_label_bookmarked": "มีที่คั่นหน้าแล้ว",
     "type_label_synced": "ซิงค์จากอุปกรณ์อื่น",
     "type_label_open": "เปิด",
     "type_label_topic": "หัวข้อ",
-    "menu_action_bookmark": "ที่คั่นหน้า",
+    "menu_action_bookmark": "เพิ่มที่คั่นหน้า",
     "menu_action_remove_bookmark": "เอาที่คั่นหน้าออก",
     "menu_action_copy_address": "คัดลอกที่อยู่",
     "menu_action_email_link": "ส่งอีเมลลิงก์…",
     "menu_action_open_new_window": "เปิดในหน้าต่างใหม่",
     "menu_action_open_private_window": "เปิดในหน้าต่างส่วนตัวใหม่",
     "menu_action_dismiss": "ยกเลิก",
     "menu_action_delete": "ลบออกจากประวัติ",
     "search_for_something_with": "ค้นหาสำหรับ {search_term} ด้วย:",
@@ -2682,16 +3044,17 @@
     "settings_pane_header": "ตั้งค่าแท็บใหม่",
     "settings_pane_body": "เลือกสิ่งที่คุณเห็นเมื่อคุณเปิดแท็บใหม่",
     "settings_pane_search_header": "ค้นหา",
     "settings_pane_search_body": "ค้นหาเว็บจากแท็บใหม่ของคุณ",
     "settings_pane_topsites_header": "ไซต์เด่น",
     "settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
     "settings_pane_topsites_options_showmore": "แสดงสองแถว",
     "settings_pane_highlights_header": "รายการเด่น",
+    "settings_pane_highlights_body": "มองย้อนกลับมาดูประวัติการท่องเว็บเมื่อเร็ว ๆ นี้และที่คั่นหน้าที่สร้างใหม่ของคุณ",
     "settings_pane_done_button": "เสร็จสิ้น",
     "edit_topsites_button_text": "แก้ไข",
     "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
     "edit_topsites_showmore_button": "แสดงเพิ่มเติม",
     "edit_topsites_showless_button": "แสดงน้อยลง",
     "edit_topsites_done_button": "เสร็จสิ้น",
     "edit_topsites_pin_button": "ปักหมุดไซต์นี้",
     "edit_topsites_edit_button": "แก้ไขไซต์นี้",
@@ -2873,16 +3236,17 @@
     "welcome_title": "نئے ٹیب میں خوش آمدید",
     "welcome_body": "اس جگہ کا استعمال کرنے ہوئے Firefox آپکی متعلقہ نشانیاں، عبارات، وڈیوز اور صفحات جن کا حال ہی میں ص آُپ نے دورہ کیا ہے دکھائے گا۔ تاکہ آپ ان تک واپس آسانی سے پہنچ سکیں۔",
     "welcome_label": "آپکی جھلکیوں کی نشاندہی کر رہا ہے",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں",
+    "settings_pane_header": "نئے َٹیب کی ترجیحات",
     "settings_pane_search_header": "تلاش",
     "settings_pane_search_body": "اپنے نئے ٹیب سے وہب پر تلاش کریں۔",
     "settings_pane_topsites_header": "بہترین سائٹیں",
     "settings_pane_topsites_options_showmore": "دو قطاریں دکھائیں",
     "settings_pane_highlights_header": "شہ سرخياں",
     "settings_pane_done_button": "ہوگیا",
     "edit_topsites_button_text": "تدوین",
     "edit_topsites_done_button": "ہوگیا",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,78 +1,136 @@
 /* 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/. */
-/* globals LocalizationFeed, NewTabInit, SearchFeed, TelemetryFeed, TopSitesFeed, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
+const REASON_ADDON_UNINSTALL = 6;
+
+XPCOMUtils.defineLazyModuleGetter(this, "DefaultPrefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
+
 // Feeds
 XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed",
   "resource://activity-stream/lib/LocalizationFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
   "resource://activity-stream/lib/NewTabInit.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesFeed",
   "resource://activity-stream/lib/PlacesFeed.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
-  "resource://activity-stream/lib/SearchFeed.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
   "resource://activity-stream/lib/TelemetryFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
   "resource://activity-stream/lib/TopSitesFeed.jsm");
 
-const feeds = {
-  // When you add a feed here:
-  // 1. The key in this object should directly refer to a pref, not including the
-  //    prefix (so "feeds.newtabinit" refers to the
-  //    "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
-  // 2. The value should be a function that returns a feed.
+const PREFS_CONFIG = [
+  // When you add a feed pref here:
+  // 1. The pref should be prefixed with "feeds."
+  // 2. The init property should be a function that instantiates your Feed
   // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
   //    so it isn't loaded until the feed is enabled.
-  "feeds.localization": () => new LocalizationFeed(),
-  "feeds.newtabinit": () => new NewTabInit(),
-  "feeds.places": () => new PlacesFeed(),
-  "feeds.search": () => new SearchFeed(),
-  "feeds.telemetry": () => new TelemetryFeed(),
-  "feeds.topsites": () => new TopSitesFeed()
-};
+  {
+    name: "feeds.localization",
+    title: "Initialize strings and detect locale for Activity Stream",
+    value: true,
+    init: () => new LocalizationFeed()
+  },
+  {
+    name: "feeds.newtabinit",
+    title: "Sends a copy of the state to each new tab that is opened",
+    value: true,
+    init: () => new NewTabInit()
+  },
+  {
+    name: "feeds.places",
+    title: "Listens for and relays various Places-related events",
+    value: true,
+    init: () => new PlacesFeed()
+  },
+  {
+    name: "feeds.telemetry",
+    title: "Relays telemetry-related actions to TelemetrySender",
+    value: true,
+    init: () => new TelemetryFeed()
+  },
+  {
+    name: "feeds.topsites",
+    title: "Queries places and gets metadata for Top Sites section",
+    value: true,
+    init: () => new TopSitesFeed()
+  },
+  // End feeds
+
+  {
+    name: "telemetry",
+    title: "Enable system error and usage data collection",
+    value: false
+  },
+  {
+    name: "telemetry.log",
+    title: "Log telemetry events in the console",
+    value: false
+  },
+  {
+    name: "telemetry.ping.endpoint",
+    title: "Telemetry server endpoint",
+    value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
+  }
+];
+
+const feeds = {};
+for (const pref of PREFS_CONFIG) {
+  if (pref.name.match(/^feeds\./)) {
+    feeds[pref.name] = pref.init;
+  }
+}
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
    * @param  {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
    * @param  {string} options.version Version of the add-on. e.g. "0.1.0"
    * @param  {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
    */
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = feeds;
+    this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
     this.initialized = true;
+    this._defaultPrefs.init();
     this.store.init(this.feeds);
     this.store.dispatch({
       type: at.INIT,
       data: {version: this.options.version}
     });
   }
   uninit() {
     this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
+
     this.initialized = false;
   }
+  uninstall(reason) {
+    if (reason === REASON_ADDON_UNINSTALL) {
+      // This resets all prefs in the config to their default values,
+      // so we DON'T want to do this on an upgrade/downgrade, only on a
+      // real uninstall
+      this._defaultPrefs.reset();
+    }
+  }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals AboutNewTab, RemotePages, XPCOMUtils */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+
+this.Prefs = class Prefs extends Preferences {
+
+  /**
+   * Prefs - A wrapper around Preferences that always sets the branch to
+   *         ACTIVITY_STREAM_PREF_BRANCH
+   */
+  constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    super({branch});
+    this._branchName = branch;
+  }
+  get branchName() {
+    return this._branchName;
+  }
+};
+
+this.DefaultPrefs = class DefaultPrefs {
+
+  /**
+   * DefaultPrefs - A helper for setting and resetting default prefs for the add-on
+   *
+   * @param  {Array} config An array of configuration objects with the following properties:
+   *         {string} .name The name of the pref
+   *         {string} .title (optional) A description of the pref
+   *         {bool|string|number} .value The default value for the pref
+   * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
+   */
+  constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    this._config = config;
+    this.branch = Services.prefs.getDefaultBranch(branch);
+  }
+
+  /**
+   * _setDefaultPref - Sets the default value (not user-defined) for a given pref
+   *
+   * @param  {string} key The name of the pref
+   * @param  {type} val The default value of the pref
+   */
+  _setDefaultPref(key, val) {
+    switch (typeof val) {
+      case "boolean":
+        this.branch.setBoolPref(key, val);
+        break;
+      case "number":
+        this.branch.setIntPref(key, val);
+        break;
+      case "string":
+        this.branch.setStringPref(key, val);
+        break;
+    }
+  }
+
+  /**
+   * init - Set default prefs for all prefs in the config
+   */
+  init() {
+    for (const pref of this._config) {
+      this._setDefaultPref(pref.name, pref.value);
+    }
+  }
+
+  /**
+   * reset - Resets all user-defined prefs for prefs in ._config to their defaults
+   */
+  reset() {
+    for (const pref of this._config) {
+      this.branch.clearUserPref(pref.name);
+    }
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["DefaultPrefs", "Prefs"];
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* globals Services, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* globals ContentSearch, XPCOMUtils, PlacesUtils, NewTabUtils, Services */
 "use strict";
 
 const {utils: Cu, interfaces: Ci} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
deleted file mode 100644
--- a/browser/extensions/activity-stream/lib/SearchFeed.jsm
+++ /dev/null
@@ -1,77 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* globals ContentSearch, XPCOMUtils, Services */
-"use strict";
-
-const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
-  "resource:///modules/ContentSearch.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-this.SearchFeed = class SearchFeed {
-  addObservers() {
-    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
-
-    // Notice when ContentSearch.init would be lazily loaded from nsBrowserGlue
-    this.contentSearch = new Promise(resolve => Services.mm.addMessageListener(
-      "ContentSearch", (this._onMessage = () => {
-        Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-        resolve(ContentSearch);
-      })));
-  }
-  removeObservers() {
-    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
-    Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-  }
-
-  observe(subject, topic, data) {
-    switch (topic) {
-      case SEARCH_ENGINE_TOPIC:
-        if (data !== "engine-default") {
-          this.getState();
-        }
-        break;
-    }
-  }
-
-  async getState() {
-    // Wait for ContentSearch to be lazily loaded before getting state
-    const state = await (await this.contentSearch).currentStateObj(true);
-    const engines = state.engines.map(engine => ({
-      name: engine.name,
-      icon: engine.iconBuffer
-    }));
-    const currentEngine = {
-      name: state.currentEngine.name,
-      icon: state.currentEngine.iconBuffer
-    };
-    const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
-    this.store.dispatch(ac.BroadcastToContent(action));
-  }
-  performSearch(browser, data) {
-    ContentSearch.performSearch({target: browser}, data);
-  }
-
-  async onAction(action) {
-    switch (action.type) {
-      case at.INIT:
-        this.addObservers();
-        await this.getState();
-        break;
-      case at.PERFORM_SEARCH:
-        this.performSearch(action._target.browser, action.data);
-        break;
-      case at.UNINIT:
-        this.removeObservers();
-        break;
-    }
-  }
-};
-this.EXPORTED_SYMBOLS = ["SearchFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,22 +1,22 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* global Preferences */
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const PREF_PREFIX = "browser.newtabpage.activity-stream.";
-Cu.import("resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
 
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         functionality to allow for routing of actions between the Main processes
  *         and child processes via a ActivityStreamMessageChannel.
  *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
@@ -32,16 +32,17 @@ this.Store = class Store {
     // store.dispatch() will call store._store.dispatch();
     ["dispatch", "getState", "subscribe"].forEach(method => {
       this[method] = function(...args) {
         return this._store[method](...args);
       }.bind(this);
     });
     this.feeds = new Map();
     this._feedFactories = null;
+    this._prefs = new Prefs();
     this._prefHandlers = new Map();
     this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
       redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
@@ -89,62 +90,56 @@ this.Store = class Store {
   /**
    * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
    *     feed off/on, and as long as that pref was not explicitly set to
    *     false, initialize the feed immediately.
    *
    * @param  {string} name The name of a feed, as defined in the object passed
    *                       to Store.init
    */
-  maybeStartFeedAndListenForPrefChanges(name) {
-    const prefName = PREF_PREFIX + name;
-
-    // If the pref was never set, set it to true by default.
-    if (!Preferences.has(prefName)) {
-      Preferences.set(prefName, true);
-    }
-
+  maybeStartFeedAndListenForPrefChanges(prefName) {
     // Create a listener that turns the feed off/on based on changes
     // to the pref, and cache it so we can unlisten on shut-down.
-    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(prefName) : this.uninitFeed(prefName));
     this._prefHandlers.set(prefName, onPrefChanged);
-    Preferences.observe(prefName, onPrefChanged);
+    this._prefs.observe(prefName, onPrefChanged);
 
     // TODO: This should propbably be done in a generic pref manager for Activity Stream.
     // If the pref is true, start the feed immediately.
-    if (Preferences.get(prefName)) {
-      this.initFeed(name);
+    if (this._prefs.get(prefName)) {
+      this.initFeed(prefName);
     }
   }
 
   /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
    *
-   * @param  {array} feeds An array of objects with an optional .onAction method
+   * @param  {array} feedConstructors An array of configuration objects for feeds
+   *                 each with .name (the name of the pref for the feed) and .init,
+   *                 a function that returns an instance of the feed
    */
   init(feedConstructors) {
     if (feedConstructors) {
       this._feedFactories = feedConstructors;
-      for (const name of Object.keys(feedConstructors)) {
-        this.maybeStartFeedAndListenForPrefChanges(name);
+      for (const pref of Object.keys(feedConstructors)) {
+        this.maybeStartFeedAndListenForPrefChanges(pref);
       }
     }
     this._messageChannel.createChannel();
   }
 
   /**
    * uninit -  Uninitalizes each feed, clears them, and destroys the message
    *           manager channel.
    *
    * @return {type}  description
    */
   uninit() {
     this.feeds.forEach(feed => this.uninitFeed(feed));
-    this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+    this._prefHandlers.forEach((handler, pref) => this._prefs.ignore(pref, handler));
     this._prefHandlers.clear();
     this._feedFactories = null;
     this.feeds.clear();
     this._messageChannel.destroyChannel();
   }
 };
 
-this.PREF_PREFIX = PREF_PREFIX;
-this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
+this.EXPORTED_SYMBOLS = ["Store"];
--- a/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals XPCOMUtils, gUUIDGenerator, ClientID */
 
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/ClientID.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
--- a/browser/extensions/activity-stream/lib/TelemetrySender.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetrySender.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals Preferences, Services, XPCOMUtils */
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Console.jsm"); // eslint-disable-line no-console
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -1,12 +1,11 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- /* globals NewTabUtils, PreviewProvider */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource:///modules/PreviewProvider.jsm");
 
--- a/browser/extensions/activity-stream/test/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -1,11 +1,12 @@
 module.exports = {
   "env": {
     "node": true,
     "es6": true,
     "mocha": true
   },
   "globals": {
     "assert": true,
-    "sinon": true
+    "sinon": true,
+    "chai": true
   }
 };
--- a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -26,16 +26,17 @@ module.exports = {
     "OpenBrowserWindow": false,
     "Preferences": false,
     "registerCleanupFunction": false,
     "requestLongerTimeout": false,
     "Services": false,
     "SimpleTest": false,
     "SpecialPowers": false,
     "TestUtils": false,
+    "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
     "todo": false,
     "todo_is": false,
     "todo_isnot": false,
     "waitForClipboard": false,
     "waitForExplicitFinish": false,
     "waitForFocus": false
   }
 };
--- a/browser/extensions/activity-stream/test/schemas/pings.js
+++ b/browser/extensions/activity-stream/test/schemas/pings.js
@@ -1,9 +1,10 @@
 const Joi = require("joi-browser");
+const {MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
 
 const baseKeys = {
   client_id: Joi.string().required(),
   addon_version: Joi.string().required(),
   locale: Joi.string().required(),
   session_id: Joi.string(),
   page: Joi.valid(["about:home", "about:newtab"])
 };
@@ -16,16 +17,39 @@ const UserEventPing = Joi.object().keys(
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_user_event").required(),
   metadata_source: Joi.string(),
   highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
   recommender_type: Joi.string()
 }));
 
+// Use this to validate actions generated from Redux
+const UserEventAction = Joi.object().keys({
+  type: Joi.string().required(),
+  data: Joi.object().keys({
+    event: Joi.valid([
+      "CLICK",
+      "SEARCH",
+      "BLOCK",
+      "DELETE",
+      "OPEN_NEW_WINDOW",
+      "OPEN_PRIVATE_WINDOW",
+      "BOOKMARK_DELETE",
+      "BOOKMARK_ADD"
+    ]).required(),
+    source: Joi.valid(["TOP_SITES"]),
+    action_position: Joi.number().integer()
+  }).required(),
+  meta: Joi.object().keys({
+    to: Joi.valid(MAIN_MESSAGE_TYPE).required(),
+    from: Joi.valid(CONTENT_MESSAGE_TYPE).required()
+  }).required()
+});
+
 const UndesiredPing = Joi.object().keys(Object.assign({}, baseKeys, {
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_undesired_event").required(),
   value: Joi.number().required()
 }));
 
 const PerfPing = Joi.object().keys(Object.assign({}, baseKeys, {
@@ -37,21 +61,53 @@ const PerfPing = Joi.object().keys(Objec
 
 const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
   session_id: baseKeys.session_id.required(),
   page: baseKeys.page.required(),
   session_duration: Joi.number().integer().required(),
   action: Joi.valid("activity_stream_session").required()
 }));
 
-function assertMatchesSchema(ping, schema) {
-  assert.isNull(Joi.validate(ping, schema).error);
+function chaiAssertions(_chai, utils) {
+  const {Assertion} = _chai;
+
+  Assertion.addMethod("validate", function(schema, schemaName) {
+    const {error} = Joi.validate(this._obj, schema);
+    this.assert(
+      !error,
+      `Expected to be ${schemaName ? `a valid ${schemaName}` : "valid"} but there were errors: ${error}`
+    );
+  });
+
+  const assertions = {
+    /**
+     * assert.validate - Validates an item given a Joi schema
+     *
+     * @param  {any} actual The item to validate
+     * @param  {obj} schema A Joi schema
+     */
+    validate(actual, schema, schemaName) {
+      new Assertion(actual).validate(schema, schemaName);
+    },
+
+    /**
+     * isUserEventAction - Passes if the item is a valid UserEvent action
+     *
+     * @param  {any} actual The item to validate
+     */
+    isUserEventAction(actual) {
+      new Assertion(actual).validate(UserEventAction, "UserEventAction");
+    }
+  };
+
+  Object.assign(_chai.assert, assertions);
 }
 
 module.exports = {
   baseKeys,
   BasePing,
   UndesiredPing,
   UserEventPing,
+  UserEventAction,
   PerfPing,
   SessionPing,
-  assertMatchesSchema
+  chaiAssertions
 };
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -1,10 +1,10 @@
 const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
-const {TopSites, Search, App} = reducers;
+const {TopSites, App} = reducers;
 const {actionTypes: at} = require("common/Actions.jsm");
 
 describe("Reducers", () => {
   describe("App", () => {
     it("should return the initial state", () => {
       const nextState = App(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.App);
     });
@@ -103,27 +103,9 @@ describe("Reducers", () => {
       events.forEach(event => {
         const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
         const action = {type: event, data: {url: "bar.com"}};
         const nextState = TopSites(oldState, action);
         assert.deepEqual(nextState.rows, [{url: "foo.com"}]);
       });
     });
   });
-  describe("Search", () => {
-    it("should return the initial state", () => {
-      const nextState = Search(undefined, {type: "FOO"});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should not update state for empty action.data on Search", () => {
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
-      const newEngine = {name: "Google", iconBuffer: "icon.ico"};
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
-      assert.equal(nextState.currentEngine.name, newEngine.name);
-      assert.equal(nextState.currentEngine.icon, newEngine.icon);
-      assert.equal(nextState.engines[0].name, newEngine.name);
-      assert.equal(nextState.engines[0].icon, newEngine.icon);
-    });
-  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,55 +1,60 @@
 const injector = require("inject!lib/ActivityStream.jsm");
 
+const REASON_ADDON_UNINSTALL = 6;
+
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
   function Fake() {}
-  before(() => {
+
+  beforeEach(() => {
     sandbox = sinon.sandbox.create();
     ({ActivityStream} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
-      "lib/SearchFeed.jsm": {SearchFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
       "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}
     }));
+    as = new ActivityStream();
+    sandbox.stub(as.store, "init");
+    sandbox.stub(as.store, "uninit");
+    sandbox.stub(as._defaultPrefs, "init");
+    sandbox.stub(as._defaultPrefs, "reset");
   });
 
   afterEach(() => sandbox.restore());
 
-  beforeEach(() => {
-    as = new ActivityStream();
-    sandbox.stub(as.store, "init");
-    sandbox.stub(as.store, "uninit");
-  });
-
   it("should exist", () => {
     assert.ok(ActivityStream);
   });
   it("should initialize with .initialized=false", () => {
     assert.isFalse(as.initialized, ".initialized");
   });
   describe("#init", () => {
     beforeEach(() => {
       as.init();
     });
+    it("should initialize default prefs", () => {
+      assert.calledOnce(as._defaultPrefs.init);
+    });
     it("should set .initialized to true", () => {
       assert.isTrue(as.initialized, ".initialized");
     });
     it("should call .store.init", () => {
       assert.calledOnce(as.store.init);
     });
     it("should emit an INIT event with the right version", () => {
       as = new ActivityStream({version: "1.2.3"});
       sandbox.stub(as.store, "init");
       sandbox.stub(as.store, "dispatch");
+      sandbox.stub(as._defaultPrefs, "init");
 
       as.init();
 
       assert.calledOnce(as.store.dispatch);
       const action = as.store.dispatch.firstCall.args[0];
       assert.propertyVal(action.data, "version", "1.2.3");
     });
   });
@@ -60,16 +65,26 @@ describe("ActivityStream", () => {
     });
     it("should set .initialized to false", () => {
       assert.isFalse(as.initialized, ".initialized");
     });
     it("should call .store.uninit", () => {
       assert.calledOnce(as.store.uninit);
     });
   });
+  describe("#uninstall", () => {
+    it("should reset default prefs if the reason is REASON_ADDON_UNINSTALL", () => {
+      as.uninstall(REASON_ADDON_UNINSTALL);
+      assert.calledOnce(as._defaultPrefs.reset);
+    });
+    it("should not reset default prefs if the reason is something else", () => {
+      as.uninstall("foo");
+      assert.notCalled(as._defaultPrefs.reset);
+    });
+  });
   describe("feeds", () => {
     it("should create a Localization feed", () => {
       const feed = as.feeds["feeds.localization"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a NewTabInit feed", () => {
       const feed = as.feeds["feeds.newtabinit"]();
       assert.instanceOf(feed, Fake);
@@ -81,14 +96,10 @@ describe("ActivityStream", () => {
     it("should create a TopSites feed", () => {
       const feed = as.feeds["feeds.topsites"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Telemetry feed", () => {
       const feed = as.feeds["feeds.telemetry"]();
       assert.instanceOf(feed, Fake);
     });
-    it("should create a Search feed", () => {
-      const feed = as.feeds["feeds.search"]();
-      assert.instanceOf(feed, Fake);
-    });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -4,38 +4,35 @@ const {createStore, applyMiddleware} = r
 const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
 
 const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
 
 describe("ActivityStreamMessageChannel", () => {
   let globals;
   let dispatch;
   let mm;
-  before(() => {
+  beforeEach(() => {
     function RP(url) {
       this.url = url;
       this.messagePorts = [];
       this.addMessageListener = globals.sandbox.spy();
       this.sendAsyncMessage = globals.sandbox.spy();
       this.destroy = globals.sandbox.spy();
     }
     globals = new GlobalOverrider();
     globals.set("AboutNewTab", {
       override: globals.sandbox.spy(),
       reset: globals.sandbox.spy()
     });
     globals.set("RemotePages", RP);
     dispatch = globals.sandbox.spy();
-  });
-  beforeEach(() => {
     mm = new ActivityStreamMessageChannel({dispatch});
   });
 
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
 
   it("should exist", () => {
     assert.ok(ActivityStreamMessageChannel);
   });
   it("should apply default options", () => {
     mm = new ActivityStreamMessageChannel();
     OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
   });
@@ -126,16 +123,22 @@ describe("ActivityStreamMessageChannel",
       it("should dispatch a NEW_TAB_UNLOAD action", () => {
         const t = {portID: "foo"};
         sinon.stub(mm, "onActionFromContent");
         mm.onNewTabUnload({target: t});
         assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
       });
     });
     describe("#onMessage", () => {
+      let sandbox;
+      beforeEach(() => {
+        sandbox = sinon.sandbox.create();
+        sandbox.spy(global.Components.utils, "reportError");
+      });
+      afterEach(() => sandbox.restore());
       it("should report an error if the msg.data is missing", () => {
         mm.onMessage({target: {portID: "foo"}});
         assert.calledOnce(global.Components.utils.reportError);
       });
       it("should report an error if the msg.data.type is missing", () => {
         mm.onMessage({target: {portID: "foo"}, data: "foo"});
         assert.calledOnce(global.Components.utils.reportError);
       });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
@@ -0,0 +1,63 @@
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm");
+
+const TEST_PREF_CONFIG = [
+  {name: "foo", value: true},
+  {name: "bar", value: "BAR"},
+  {name: "baz", value: 1}
+];
+
+describe("ActivityStreamPrefs", () => {
+  describe("Prefs", () => {
+    it("should have get, set, and observe methods", () => {
+      const p = new Prefs();
+      assert.property(p, "get");
+      assert.property(p, "set");
+      assert.property(p, "observe");
+    });
+    describe(".branchName", () => {
+      it("should return the activity stream branch by default", () => {
+        const p = new Prefs();
+        assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH);
+      });
+      it("should return the custom branch name if it was passed to the constructor", () => {
+        const p = new Prefs("foo");
+        assert.equal(p.branchName, "foo");
+      });
+    });
+  });
+
+  describe("DefaultPrefs", () => {
+    describe("#init", () => {
+      let defaultPrefs;
+      beforeEach(() => {
+        defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "setBoolPref");
+        sinon.spy(defaultPrefs.branch, "setStringPref");
+        sinon.spy(defaultPrefs.branch, "setIntPref");
+      });
+      it("should initialize a boolean pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true);
+      });
+      it("should initialize a string pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setStringPref, "bar", "BAR");
+      });
+      it("should initialize a integer pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setIntPref, "baz", 1);
+      });
+    });
+    describe("#reset", () => {
+      it("should clear user preferences for each pref in the config", () => {
+        const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "clearUserPref");
+        defaultPrefs.reset();
+        for (const pref of TEST_PREF_CONFIG) {
+          assert.calledWith(defaultPrefs.branch.clearUserPref, pref.name);
+        }
+      });
+    });
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
@@ -14,29 +14,32 @@ const TEST_STRINGS = {
     too: "Boo"
   },
   "ru": {foo: "Baz"}
 };
 
 describe("Localization Feed", () => {
   let feed;
   let globals;
-  before(() => {
+  let sandbox;
+  beforeEach(() => {
     globals = new GlobalOverrider();
-  });
-  beforeEach(() => {
+    sandbox = globals.sandbox;
     feed = new LocalizationFeed();
     feed.store = {dispatch: sinon.spy()};
+
+    sandbox.stub(global.Services.locale, "getRequestedLocale");
   });
   afterEach(() => {
     globals.restore();
   });
 
   it("should fetch strings on init", async () => {
-    sinon.stub(feed, "updateLocale");
+    sandbox.stub(feed, "updateLocale");
+    sandbox.stub(global, "fetch");
     fetch.returns(Promise.resolve({json() { return Promise.resolve(TEST_STRINGS); }}));
 
     await feed.init();
 
     assert.deepEqual(feed.allStrings, TEST_STRINGS);
     assert.calledOnce(feed.updateLocale);
   });
 
@@ -110,19 +113,23 @@ describe("Localization Feed", () => {
       feed.observe(null, "some-other-notification");
 
       assert.notCalled(feed.updateLocale);
     });
   });
 
   describe("#onAction", () => {
     it("should addObserver on INIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "addObserver");
+
       feed.onAction({type: at.INIT});
 
-      assert.calledOnce(global.Services.obs.addObserver);
+      assert.calledOnce(stub);
     });
     it("should removeObserver on UNINIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "removeObserver");
+
       feed.onAction({type: at.UNINIT});
 
-      assert.calledOnce(global.Services.obs.removeObserver);
+      assert.calledOnce(stub);
     });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -23,16 +23,19 @@ describe("PlacesFeed", () => {
         deleteHistoryEntry: sandbox.spy(),
         blockURL: sandbox.spy()
       }
     });
     globals.set("PlacesUtils", {
       history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
       bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
     });
+    sandbox.spy(global.Services.obs, "addObserver");
+    sandbox.spy(global.Services.obs, "removeObserver");
+    sandbox.spy(global.Components.utils, "reportError");
 
     feed = new PlacesFeed();
     feed.store = {dispatch: sinon.spy()};
   });
   afterEach(() => globals.restore());
 
   it("should have a HistoryObserver that dispatches to the store", () => {
     assert.instanceOf(feed.historyObserver, HistoryObserver);
deleted file mode 100644
--- a/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-"use strict";
-const {SearchFeed} = require("lib/SearchFeed.jsm");
-const {GlobalOverrider} = require("test/unit/utils");
-const {actionTypes: at} = require("common/Actions.jsm");
-const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
-describe("Search Feed", () => {
-  let feed;
-  let globals;
-  before(() => {
-    globals = new GlobalOverrider();
-    globals.set("ContentSearch", {
-      currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
-      performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
-    });
-  });
-  beforeEach(() => {
-    feed = new SearchFeed();
-    feed.store = {dispatch: sinon.spy()};
-  });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
-
-  it("should call get state (with true) from the content search provider on INIT", async() => {
-    await feed.onAction({type: at.INIT});
-
-    // calling currentStateObj with 'true' allows us to return a data uri for the
-    // icon, instead of an array buffer
-    assert.calledWith(global.ContentSearch.currentStateObj, true);
-  });
-  it("should get the the state on INIT", () => {
-    sinon.stub(feed, "getState");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.getState);
-  });
-  it("should add observers on INIT", () => {
-    sinon.stub(feed, "addObservers");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.addObservers);
-  });
-  it("should remove observers on UNINIT", () => {
-    sinon.stub(feed, "removeObservers");
-    feed.onAction({type: at.UNINIT});
-    assert.calledOnce(feed.removeObservers);
-  });
-  it("should add event handlers on INIT", () => {
-    feed.onAction({type: at.INIT});
-
-    assert.calledOnce(global.Services.obs.addObserver);
-    assert.calledOnce(global.Services.mm.addMessageListener);
-  });
-  it("should remove event handlers on UNINIT", () => {
-    feed.onAction({type: at.UNINIT});
-
-    assert.calledOnce(global.Services.obs.removeObserver);
-    assert.calledOnce(global.Services.mm.removeMessageListener);
-  });
-  it("should dispatch one event with the state", async() => {
-    feed.contentSearch = Promise.resolve(global.ContentSearch);
-
-    await feed.getState();
-
-    assert.calledOnce(feed.store.dispatch);
-  });
-  it("should perform a search on PERFORM_SEARCH", () => {
-    sinon.stub(feed, "performSearch");
-    feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
-    assert.calledOnce(feed.performSearch);
-  });
-  it("should call performSearch with an action", () => {
-    const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
-    feed.performSearch(action._target.browser, action.data);
-    assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
-  });
-  it("should get the state if we change the search engines", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "browser-search-engine-modified", "engine-current");
-    assert.calledOnce(feed.getState);
-  });
-  it("shouldn't get the state if it's not the right notification", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "some-other-notification", "engine-current");
-    assert.notCalled(feed.getState);
-  });
-});
--- a/browser/extensions/activity-stream/test/unit/lib/Store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -1,40 +1,34 @@
 const injector = require("inject!lib/Store.jsm");
 const {createStore} = require("redux");
 const {addNumberReducer} = require("test/unit/utils");
-const {GlobalOverrider} = require("test/unit/utils");
+const {FakePrefs} = require("test/unit/utils");
 describe("Store", () => {
   let Store;
-  let Preferences;
   let sandbox;
   let store;
-  let globals;
-  let PREF_PREFIX;
   beforeEach(() => {
-    globals = new GlobalOverrider();
-    sandbox = globals.sandbox;
-    Preferences = new Map();
-    Preferences.observe = sandbox.spy();
-    Preferences.ignore = sandbox.spy();
-    globals.set("Preferences", Preferences);
+    sandbox = sinon.sandbox.create();
     function ActivityStreamMessageChannel(options) {
       this.dispatch = options.dispatch;
       this.createChannel = sandbox.spy();
       this.destroyChannel = sandbox.spy();
       this.middleware = sandbox.spy(s => next => action => next(action));
     }
-    ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+    ({Store} = injector({
+      "lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel},
+      "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}
+    }));
     store = new Store();
   });
   afterEach(() => {
-    Preferences.clear();
-    globals.restore();
+    sandbox.restore();
   });
-  it("should have an .feeds property that is a Map", () => {
+  it("should have a .feeds property that is a Map", () => {
     assert.instanceOf(store.feeds, Map);
     assert.equal(store.feeds.size, 0, ".feeds.size");
   });
   it("should have a redux store at ._store", () => {
     assert.ok(store._store);
     assert.property(store, "dispatch");
     assert.property(store, "getState");
   });
@@ -44,17 +38,17 @@ describe("Store", () => {
   });
   it("should connect the ActivityStreamMessageChannel's middleware", () => {
     store.dispatch({type: "FOO"});
     assert.calledOnce(store._messageChannel.middleware);
   });
   describe("#initFeed", () => {
     it("should add an instance of the feed to .feeds", () => {
       class Foo {}
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.init({foo: () => new Foo()});
       store.initFeed("foo");
 
       assert.isTrue(store.feeds.has("foo"), "foo is set");
       assert.instanceOf(store.feeds.get("foo"), Foo);
     });
     it("should add a .store property to the feed", () => {
       class Foo {}
@@ -93,44 +87,36 @@ describe("Store", () => {
       assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
     });
   });
   describe("maybeStartFeedAndListenForPrefChanges", () => {
     beforeEach(() => {
       sinon.stub(store, "initFeed");
       sinon.stub(store, "uninitFeed");
     });
-    it("should set the new pref in Preferences to true, if it was never defined", () => {
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
-    });
-    it("should not override the pref if it was already set", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
-    });
     it("should initialize the feed if the Pref is set to true", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, true);
+      store._prefs.set("foo", true);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.calledWith(store.initFeed, "foo");
     });
     it("should not initialize the feed if the Pref is set to false", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.notCalled(store.initFeed);
     });
     it("should observe the pref", () => {
+      sinon.stub(store._prefs, "observe");
       store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+      assert.calledWith(store._prefs.observe, "foo", store._prefHandlers.get("foo"));
     });
     describe("handler", () => {
       let handler;
       beforeEach(() => {
         store.maybeStartFeedAndListenForPrefChanges("foo");
-        handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+        handler = store._prefHandlers.get("foo");
       });
       it("should initialize the feed if called with true", () => {
         handler(true);
         assert.calledWith(store.initFeed, "foo");
       });
       it("should uninitialize the feed if called with false", () => {
         handler(false);
         assert.calledWith(store.uninitFeed, "foo");
@@ -146,16 +132,19 @@ describe("Store", () => {
     });
     it("should initialize the ActivityStreamMessageChannel channel", () => {
       store.init();
       assert.calledOnce(store._messageChannel.createChannel);
     });
   });
   describe("#uninit", () => {
     it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+      store._prefs.set("a", true);
+      store._prefs.set("b", true);
+      store._prefs.set("c", true);
       store.init({
         a: () => ({}),
         b: () => ({}),
         c: () => ({})
       });
 
       store.uninit();
 
@@ -176,16 +165,17 @@ describe("Store", () => {
     });
   });
   describe("#dispatch", () => {
     it("should call .onAction of each feed", () => {
       const {dispatch} = store;
       const sub = {onAction: sinon.spy()};
       const action = {type: "FOO"};
 
+      store._prefs.set("sub", true);
       store.init({sub: () => sub});
 
       dispatch(action);
 
       assert.calledWith(sub.onAction, action);
     });
     it("should call the reducers", () => {
       const {dispatch} = store;
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
@@ -1,18 +1,17 @@
 const injector = require("inject!lib/TelemetryFeed.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {
   BasePing,
   UndesiredPing,
   UserEventPing,
   PerfPing,
-  SessionPing,
-  assertMatchesSchema
+  SessionPing
 } = require("test/schemas/pings");
 
 const FAKE_TELEMETRY_ID = "foo123";
 const FAKE_UUID = "{foo-123-foo}";
 
 describe("TelemetryFeed", () => {
   let globals;
   let sandbox;
@@ -98,107 +97,107 @@ describe("TelemetryFeed", () => {
       assert.calledWith(instance.sendEvent, instance.createSessionEndEvent.firstCall.returnValue);
     });
   });
   describe("ping creators", () => {
     beforeEach(async () => await instance.init());
     describe("#createPing", () => {
       it("should create a valid base ping without a session if no portID is supplied", () => {
         const ping = instance.createPing();
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
         assert.notProperty(ping, "session_id");
       });
       it("should create a valid base ping with session info if a portID is supplied", () => {
         // Add a session
         const portID = "foo";
         instance.addSession(portID);
         const sessionID = instance.sessions.get(portID).session_id;
 
         // Create a ping referencing the session
         const ping = instance.createPing(portID);
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
 
         // Make sure we added the right session-related stuff to the ping
         assert.propertyVal(ping, "session_id", sessionID);
         assert.propertyVal(ping, "page", "about:newtab");
       });
     });
     describe("#createUserEvent", () => {
       it("should create a valid event", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "CLICK"};
         const action = ac.SendToMain(ac.UserEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUserEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UserEventPing);
+        assert.validate(ping, UserEventPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
       });
     });
     describe("#createUndesiredEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10});
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
         const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
     });
     describe("#createPerformanceEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100});
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {event: "PAGE_LOADED", value: 100};
         const action = ac.SendToMain(ac.PerfEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
     });
     describe("#createSessionEndEvent", () => {
       it("should create a valid event", () => {
         const ping = instance.createSessionEndEvent({
           session_id: FAKE_UUID,
           page: "about:newtab",
           session_duration: 12345
         });
         // Is it valid?
-        assertMatchesSchema(ping, SessionPing);
+        assert.validate(ping, SessionPing);
         assert.propertyVal(ping, "session_id", FAKE_UUID);
         assert.propertyVal(ping, "page", "about:newtab");
         assert.propertyVal(ping, "session_duration", 12345);
       });
     });
   });
   describe("#sendEvent", () => {
     it("should call telemetrySender", async () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -13,41 +13,38 @@ let fakePrefs;
 const prefInitHook = function() {
   fakePrefs = this; // eslint-disable-line consistent-this
 };
 const tsArgs = {prefInitHook};
 
 describe("TelemetrySender", () => {
   let globals;
   let tSender;
+  let sandbox;
   let fetchStub;
   const fakeEndpointUrl = "http://127.0.0.1/stuff";
   const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
   const fakeFetchHttpErrorResponse = {ok: false, status: 400};
   const fakeFetchSuccessResponse = {ok: true, status: 200};
 
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
-
-    fetchStub = globals.sandbox.stub();
+    sandbox = globals.sandbox;
+    fetchStub = sandbox.stub();
 
     globals.set("Preferences", FakePrefs);
     globals.set("fetch", fetchStub);
-  });
-
-  beforeEach(() => {
+    sandbox.spy(global.Components.utils, "reportError");
   });
 
   afterEach(() => {
-    globals.reset();
+    globals.restore();
     FakePrefs.prototype.prefs = {};
   });
 
-  after(() => globals.restore());
-
   it("should construct the Prefs object", () => {
     globals.sandbox.spy(global, "Preferences");
 
     tSender = new TelemetrySender(tsArgs);
 
     assert.calledOnce(global.Preferences);
   });
 
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -7,21 +7,20 @@ const FAKE_LINKS = new Array(TOP_SITES_S
 const FAKE_SCREENSHOT = "data123";
 
 describe("Top Sites Feed", () => {
   let feed;
   let globals;
   let sandbox;
   let links;
   let clock;
-  before(() => {
+
+  beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
-  });
-  beforeEach(() => {
     globals.set("NewTabUtils", {activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))}});
     globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
     feed = new TopSitesFeed();
     feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
     links = FAKE_LINKS;
     clock = sinon.useFakeTimers();
   });
   afterEach(() => {
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -1,25 +1,22 @@
 const initStore = require("content-src/lib/init-store");
 const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
 const {actionCreators: ac} = require("common/Actions.jsm");
 
 describe("initStore", () => {
   let globals;
   let store;
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
     globals.set("sendAsyncMessage", globals.sandbox.spy());
     globals.set("addMessageListener", globals.sandbox.spy());
-  });
-  beforeEach(() => {
     store = initStore({number: addNumberReducer});
   });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
   it("should create a store with the provided reducers", () => {
     assert.ok(store);
     assert.property(store.getState(), "number");
   });
   it("should add a listener for incoming actions", () => {
     assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
     const callback = global.addMessageListener.firstCall.args[1];
     globals.sandbox.spy(store, "dispatch");
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -1,44 +1,59 @@
-const {GlobalOverrider} = require("test/unit/utils");
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {chaiAssertions} = require("test/schemas/pings");
 
-const req = require.context(".", true, /\.test\.js$/);
+const req = require.context(".", true, /\.test\.jsx?$/);
 const files = req.keys();
 
 // This exposes sinon assertions to chai.assert
 sinon.assert.expose(assert, {prefix: ""});
 
+chai.use(chaiAssertions);
+
 let overrider = new GlobalOverrider();
 overrider.set({
   Components: {
     interfaces: {},
     utils: {
-      import: overrider.sandbox.spy(),
-      importGlobalProperties: overrider.sandbox.spy(),
-      reportError: overrider.sandbox.spy(),
+      import() {},
+      importGlobalProperties() {},
+      reportError() {},
       now: () => window.performance.now()
     }
   },
+  // eslint-disable-next-line object-shorthand
+  ContentSearchUIController: function() {}, // NB: This is a function/constructor
+  dump() {},
+  fetch() {},
+  Preferences: FakePrefs,
+  Services: {
+    locale: {getRequestedLocale() {}},
+    mm: {
+      addMessageListener: (msg, cb) => cb(),
+      removeMessageListener() {}
+    },
+    obs: {
+      addObserver() {},
+      removeObserver() {}
+    },
+    prefs: {
+      getDefaultBranch() {
+        return {
+          setBoolPref() {},
+          setIntPref() {},
+          setStringPref() {},
+          clearUserPref() {}
+        };
+      }
+    }
+  },
   XPCOMUtils: {
-    defineLazyModuleGetter: overrider.sandbox.spy(),
-    defineLazyServiceGetter: overrider.sandbox.spy(),
-    generateQI: overrider.sandbox.stub().returns(() => {})
-  },
-  dump: overrider.sandbox.spy(),
-  fetch: overrider.sandbox.stub(),
-  Services: {
-    locale: {getRequestedLocale: overrider.sandbox.stub()},
-    mm: {
-      addMessageListener: overrider.sandbox.spy((msg, cb) => cb()),
-      removeMessageListener: overrider.sandbox.spy()
-    },
-    obs: {
-      addObserver: overrider.sandbox.spy(),
-      removeObserver: overrider.sandbox.spy()
-    }
+    defineLazyModuleGetter() {},
+    defineLazyServiceGetter() {},
+    generateQI() { return {}; }
   }
 });
 
 describe("activity-stream", () => {
-  afterEach(() => overrider.reset());
   after(() => overrider.restore());
   files.forEach(file => req(file));
 });
--- a/browser/extensions/activity-stream/test/unit/utils.js
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -49,16 +49,17 @@ class GlobalOverrider {
    */
   set(key, value) {
     if (!value && typeof key === "object") {
       const overrides = key;
       Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
     } else {
       this._override(key, value);
     }
+    return value;
   }
 
   /**
    * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
    *         You probably want to call this after each test.
    */
   reset() {
     this.sandbox.reset();
--- a/browser/extensions/moz.build
+++ b/browser/extensions/moz.build
@@ -28,9 +28,10 @@ if CONFIG['MOZ_MORTAR']:
     DIRS += [
         'mortar',
     ]
 
 # Nightly-only system add-ons
 if CONFIG['NIGHTLY_BUILD']:
     DIRS += [
         'activity-stream',
+        'onboarding',
     ]
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -0,0 +1,18 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+function install(aData, aReason) {}
+
+function uninstall(aData, aReason) {}
+
+function startup(aData, reason) {
+  Services.mm.loadFrameScript("resource://onboarding/onboarding.js", true);
+}
+
+function shutdown(aData, reason) {}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/content/img/overlay-icon.svg
@@ -0,0 +1,2 @@
+
+<svg width="36" height="29" viewBox="0 0 36 29" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>overlayfox</title><defs><path id="a" d="M.002.058h35.953V27.94H.002z"/><path id="c" d="M0 17.39V.42h35.957v16.97H0z"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 .55)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M35.953 16.593c.006-.19-.036-.386-.133-.562-1.02-1.884-2.052-3.65-3.17-5.243.773-.62 1.448-1.394 1.975-2.312 1.497-2.61 1.413-5.72.042-8.175-.063-.114-.176-.2-.294-.242.002.01-.006.016-.006.024-.008-.012-.018-.024-.03-.024-2.825-.03-5.558 1.46-7.112 4.09-.16.27-.3.572-.418.896-2.394-1.464-5.24-2.31-8.822-2.31-3.57 0-6.416.832-8.806 2.283v.007l-.02.014c-.12-.322-.257-.623-.416-.89C7.19 1.52 4.457.03 1.632.06c-.014 0-.028.014-.035.028 0-.007.007-.007.007-.014-.14.036-.267.12-.337.256-1.37 2.455-1.462 5.557.042 8.175.526.926 1.208 1.7 1.988 2.327-1.118 1.586-2.15 3.344-3.163 5.23-.092.166-.13.352-.13.533H.002c0 .007.002.013 0 .02v.016h.002c-.006.17.032.344.117.504 3.685 6.71 7.6 9.377 15 9.88.807.58 1.79.928 2.857.928 1.065 0 2.05-.347 2.857-.93 7.4-.5 11.323-3.168 15-9.878.09-.162.13-.35.12-.534v-.003-.004" fill="#F70" mask="url(#b)"/></g><g transform="translate(0 10.268)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M17.978 17.39c9.31 0 13.732-2.447 17.857-9.975.09-.163.133-.356.12-.54h-4.238V5.23h-.014c.007-.113.014-.234.014-.348 0-2.462-1.975-4.46-4.407-4.46-2.43 0-4.406 1.998-4.406 4.46 0 .12.008.235.014.35h-9.88c.007-.1.014-.208.014-.314 0-2.462-1.974-4.462-4.406-4.462-2.43 0-4.406 2-4.406 4.462 0 .106.007.206.014.313h-.007v1.644H.002c-.013.185.03.37.12.54 4.132 7.53 8.545 9.976 17.856 9.976" fill="#FFC899" mask="url(#d)"/></g><path d="M35.954 17.15c.007-.192-.035-.39-.134-.57-1.018-1.885-2.05-3.65-3.17-5.243.774-.62 1.45-1.395 1.976-2.312 1.497-2.61 1.413-5.72.042-8.175-.063-.114-.175-.2-.295-.242.007.02.007.043-.014.057-.746.37-3.943 2.15-5.756 6.19-2.74-2.242-6.1-3.572-10.62-3.572-3.568 0-6.414.832-8.804 2.284v.007c-.632.384-1.23.81-1.8 1.28-1.474-3.28-3.85-5.065-5.1-5.82-.008 0-.008-.006-.015-.006C2.175.97 2.09.92 2.013.878c-.008 0-.015-.007-.015-.007C1.963.85 1.935.836 1.9.82c-.007 0-.007-.006-.014-.006-.035-.02-.064-.035-.098-.05-.008 0-.008-.006-.015-.006-.02-.015-.05-.03-.07-.036-.007 0-.014-.008-.02-.008C1.66.7 1.632.694 1.612.68 1.604.68 1.604.67 1.597.67L1.59.665V.65c0-.007 0-.007.008-.014 0-.007.007-.007.007-.014-.14.036-.267.12-.338.256-1.37 2.455-1.46 5.557.042 8.175.527.925 1.208 1.7 1.988 2.327-1.117 1.587-2.15 3.344-3.162 5.23-.098.177-.14.377-.133.57H4.24v-1.645h.007c-.007-.1-.014-.207-.014-.313 0-2.462 1.975-4.46 4.406-4.46 2.43 0 4.405 1.998 4.405 4.46 0 .106-.007.206-.014.313h.015v7.96c0 2.755 2.207 4.996 4.933 4.996 2.72 0 4.933-2.233 4.933-4.994v-7.99h.01c0-.042-.01-.085-.01-.128v-.47c.128-2.347 2.047-4.21 4.4-4.21 2.432 0 4.407 1.998 4.407 4.46 0 .12-.007.235-.015.348h.015v1.644h4.237z" fill="#F70"/><path d="M16.453 19.832s.05 0 .134.008c.084.006.204.014.345.014.14.007.31.007.49.014.177 0 .374.007.563.007.19 0 .38 0 .563-.007.175 0 .344-.007.492-.014.14-.008.26-.014.344-.014.084-.008.133-.008.133-.008.598-.057 1.132.39 1.18.996.03.3-.07.584-.245.804l-.942 1.146-.035.035c-.02.022-.056.058-.098.093-.043.036-.092.078-.148.114-.057.043-.127.078-.197.12-.07.036-.148.072-.233.107-.084.03-.168.057-.26.08-.175.042-.372.056-.56.048-.1-.006-.19-.014-.29-.028-.05-.007-.09-.014-.14-.02-.05-.008-.092-.023-.134-.03-.042-.007-.09-.028-.133-.035-.042-.015-.084-.03-.127-.043-.042-.015-.084-.03-.12-.05-.034-.015-.077-.036-.112-.05-.035-.022-.07-.036-.105-.057-.036-.022-.064-.036-.092-.058-.057-.035-.106-.07-.148-.106-.042-.03-.077-.065-.098-.08l-.036-.035-.906-.946c-.45-.47-.436-1.21.02-1.666.254-.25.577-.356.893-.342" fill="#994C00"/><path d="M8.407 19.398c-.618 0-1.243-.135-1.82-.412-.28-.136-.4-.477-.267-.762.134-.284.47-.405.752-.27.87.42 1.884.406 2.77-.043.28-.14.617-.027.75.258.14.284.03.625-.252.76-.612.314-1.272.47-1.933.47M26.938 19.398c-.618 0-1.244-.135-1.82-.412-.28-.136-.4-.477-.267-.762.135-.284.472-.405.753-.27.87.42 1.883.406 2.77-.043.28-.14.617-.027.75.258.134.284.03.625-.252.76-.61.314-1.27.47-1.932.47" fill="#F70"/><path d="M10.91 15.926c-.008-.064-.03-.12-.057-.178-.45-1.024-1.363-1.657-2.39-1.657-1.025 0-1.94.634-2.39 1.658-.027.057-.04.12-.055.178-.14.3-.077.67.183.904.31.277.788.25 1.07-.064.3-.342.736-.54 1.194-.54.456 0 .9.198 1.194.54.148.17.358.256.57.256.175 0 .358-.064.498-.192.26-.235.323-.605.183-.904M29.44 15.926c-.007-.064-.028-.12-.057-.178-.45-1.024-1.363-1.657-2.39-1.657-1.024 0-1.938.634-2.388 1.658-.028.057-.042.12-.056.178-.142.3-.078.67.182.904.14.128.323.192.5.192.21 0 .413-.086.568-.256.302-.342.737-.54 1.194-.54.457 0 .9.198 1.195.54.273.313.75.348 1.067.064.26-.235.323-.605.183-.904" fill="#363959"/><path d="M17.978 27.438c-1.405 0-2.122-.718-2.473-1.323-.373-.648-.415-1.288-.415-1.31-.007-.092.042-.184.12-.234.077-.05.175-.056.26-.014.007.008.934.456 2.48.456 1.553 0 2.53-.456 2.544-.456.085-.042.183-.028.26.022.078.05.12.142.112.235 0 .028-.042.67-.414 1.31-.352.597-1.068 1.315-2.474 1.315" fill="#994C00"/><path d="M28.597 6.855c1.468-3.28 3.843-5.066 5.094-5.82.008 0 .008-.007.016-.007.09-.057.175-.107.252-.15.007 0 .015-.007.015-.007.034-.02.063-.034.098-.05.008 0 .008-.006.015-.006.035-.02.063-.035.098-.05.007 0 .007-.007.015-.007.02-.014.05-.028.07-.035.006 0 .014-.008.02-.008.022-.014.05-.02.07-.035.008 0 .008-.008.015-.008l.007-.008V.65c0-.007 0-.007-.007-.013-.007-.015-.02-.03-.035-.03C31.513.58 28.78 2.068 27.226 4.7c-.16.27-.302.575-.42.902.617.356 1.215.783 1.79 1.253M7.374 6.855C5.906 3.575 3.53 1.79 2.28 1.035c-.008 0-.008-.007-.015-.007C2.175.97 2.09.92 2.013.878c-.008 0-.015-.007-.015-.007C1.963.85 1.935.837 1.9.82c-.007 0-.007-.006-.014-.006-.035-.02-.064-.035-.098-.05-.008 0-.008-.007-.015-.007-.02-.014-.05-.028-.07-.035-.007 0-.014-.008-.02-.008C1.66.7 1.632.694 1.612.68 1.604.68 1.604.67 1.597.67L1.59.664V.65c0-.007 0-.007.008-.013.007-.015.02-.03.035-.03C4.458.58 7.19 2.068 8.745 4.7c.16.27.302.575.42.902-.624.356-1.22.783-1.79 1.253" fill="#FF9F4D"/></g></svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/content/onboarding.css
@@ -0,0 +1,88 @@
+/* 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/. */
+#onboarding-overlay * {
+  box-sizing: border-box;
+}
+
+#onboarding-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  /* Ensuring we can put the overlay over elements using
+     z-index on original page */
+  z-index: 999;
+  color: #4d4d4d;
+  background: rgb(54, 57, 89, 0.8); /* #363959, 0.8 opacity */
+  display: none;
+}
+
+#onboarding-overlay.opened {
+  display: block;
+}
+
+#onboarding-overlay-icon {
+  width: 52px;
+  height: 40px;
+  position: absolute;
+  cursor: pointer;
+  top: 30px;
+  offset-inline-start: 30px;
+  background: url("img/overlay-icon.svg") no-repeat;
+}
+
+#onboarding-overlay-dialog {
+  display: none;
+}
+
+#onboarding-tour-close-btn {
+  position: absolute;
+  top: 15px;
+  offset-inline-end: 15px;
+}
+
+#onboarding-overlay.opened > #onboarding-overlay-dialog {
+  width: 1200px;
+  height: 550px;
+  background: #f5f5f7;
+  border: 1px solid rgba(9, 6, 13, 0.1); /* #09060D, 0.1 opacity */
+  position: relative;
+  margin: 0 calc(50% - 600px);
+  top: calc(50% - 275px);
+  display: grid;
+  grid-template-rows: [dialog-start] 76px [page-start] 1fr [footer-start] 40px [dialog-end];
+  grid-template-columns: [dialog-start] 240px [page-start] 1fr [dialog-end];
+}
+
+@media (max-height: 550px) {
+  #onboarding-overlay.opened > #onboarding-overlay-dialog {
+    top: 0;
+  }
+}
+
+#onboarding-overlay-dialog > header {
+  grid-row: dialog-start / page-start;
+  grid-column: dialog-start / tour-end;
+  margin-top: 36px;
+  margin-bottom: 0;
+  margin-inline-end: 0;
+  margin-inline-start: 36px;
+  font-size: 22px;
+}
+
+#onboarding-overlay-dialog > nav {
+  grid-row: dialog-start / footer-start;
+  grid-column-start: dialog-start;
+  margin-top: 40px;
+  margin-bottom: 0;
+  margin-inline-end: 0;
+  margin-inline-start: 0;
+  padding: 0;
+}
+
+#onboarding-overlay-dialog > footer {
+  grid-row: footer-start;
+  grid-column: dialog-start / tour-end;
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -0,0 +1,113 @@
+/* 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/. */
+
+/* global content */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+
+const ONBOARDING_CSS_URL = "resource://onboarding/onboarding.css";
+const ABOUT_NEWTAB_URL = "about:newtab";
+
+/**
+ * The script won't be initialized if we turned off onboarding by
+ * setting "browser.onboarding.disabled" to true.
+ */
+class Onboarding {
+  constructor(contentWindow) {
+    this.init(contentWindow);
+  }
+
+  async init(contentWindow) {
+    this._window = contentWindow;
+    // We want to create and append elements after CSS is loaded so
+    // no flash of style changes and no additional reflow.
+    await this._loadCSS();
+    this._overlayIcon = this._renderOverlayIcon();
+    this._overlay = this._renderOverlay();
+    this._window.document.body.appendChild(this._overlayIcon);
+    this._window.document.body.appendChild(this._overlay);
+
+    this._overlayIcon.addEventListener("click", this);
+    this._overlay.addEventListener("click", this);
+    // Destroy on unload. This is to ensure we remove all the stuff we left.
+    // No any leak out there.
+    this._window.addEventListener("unload", () => this.destroy());
+  }
+
+  handleEvent(evt) {
+    switch (evt.target.id) {
+      case "onboarding-overlay-icon":
+      case "onboarding-tour-close-btn":
+      // If the clicking target is directly on the outer-most overlay,
+      // that means clicking outside the tour content area.
+      // Let's toggle the overlay.
+      case "onboarding-overlay":
+        this.toggleOverlay();
+      break;
+    }
+  }
+
+  destroy() {
+    this._overlayIcon.remove();
+    this._overlay.remove();
+  }
+
+  toggleOverlay() {
+    this._overlay.classList.toggle("opened");
+  }
+
+  _renderOverlay() {
+    let div = this._window.document.createElement("div");
+    div.id = "onboarding-overlay";
+    // Here we use `innerHTML` is for more friendly reading.
+    // The security should be fine because this is not from an external input.
+    // We're not shipping yet so l10n strings is going to be closed for now.
+    div.innerHTML = `
+      <div id="onboarding-overlay-dialog">
+        <button id="onboarding-tour-close-btn">X</button>
+        <header>Getting started?</header>
+        <nav>
+          <ul></ul>
+        </nav>
+        <footer>
+        </footer>
+      </div>
+    `;
+    return div;
+  }
+
+  _renderOverlayIcon() {
+    let img = this._window.document.createElement("div");
+    img.id = "onboarding-overlay-icon";
+    return img;
+  }
+
+  _loadCSS() {
+    // Returning a Promise so we can inform caller of loading complete
+    // by resolving it.
+    return new Promise(resolve => {
+      let doc = this._window.document;
+      let link = doc.createElement("link");
+      link.rel = "stylesheet";
+      link.type = "text/css";
+      link.href = ONBOARDING_CSS_URL;
+      link.addEventListener("load", resolve);
+      doc.head.appendChild(link);
+    });
+  }
+}
+
+addEventListener("load", function(evt) {
+  // Load onboarding module only when we enable it.
+  if (content.location.href == ABOUT_NEWTAB_URL &&
+      !Services.prefs.getBoolPref("browser.onboarding.disabled")) {
+
+    content.requestIdleCallback(() => {
+      new Onboarding(content);
+    });
+  }
+}, true);
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/install.rdf.in
@@ -0,0 +1,28 @@
+<?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/. -->
+
+#filter substitution
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>onboarding@mozilla.org</em:id>
+    <em:name>Photon onboarding</em:name>
+    <em:description>Photon onboarding</em:description>
+    <em:version>0.1</em:version>
+    <em:bootstrap>true</em:bootstrap>
+    <em:type>2</em:type>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!--Firefox-->
+        <em:minVersion>@MOZ_APP_VERSION@</em:minVersion>
+        <em:maxVersion>@MOZ_APP_MAXVERSION@</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+    <em:strictCompatibility>false</em:strictCompatibility>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/jar.mn
@@ -0,0 +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/.
+
+[features/onboarding@mozilla.org] chrome.jar:
+% resource onboarding %content/
+  content/ (content/*)
new file mode 100644
--- /dev/null
+++ b/browser/extensions/onboarding/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_PP_FILES.features['onboarding@mozilla.org'] += [
+  'install.rdf.in'
+]
+
+FINAL_TARGET_FILES.features['onboarding@mozilla.org'] += [
+  'bootstrap.js',
+]
+
+JAR_MANIFESTS += ['jar.mn']
--- a/browser/extensions/pdfjs/content/PdfJs.jsm
+++ b/browser/extensions/pdfjs/content/PdfJs.jsm
@@ -62,20 +62,20 @@ function getIntPref(aPref, aDefaultValue
   try {
     return Services.prefs.getIntPref(aPref);
   } catch (ex) {
     return aDefaultValue;
   }
 }
 
 function isDefaultHandler() {
- if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
-   return PdfjsContentUtils.isDefaultHandlerApp();
- }
- return PdfjsChromeUtils.isDefaultHandlerApp();
+  if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+    throw new Error("isDefaultHandler should only get called in the parent process.");
+  }
+  return PdfjsChromeUtils.isDefaultHandlerApp();
 }
 
 function initializeDefaultPreferences() {
   var DEFAULT_PREFERENCES =
 {
   "showPreviousViewOnLoad": true,
   "defaultZoomValue": "",
   "sidebarViewOnLoad": 0,
@@ -264,23 +264,24 @@ var PdfJs = {
     categoryManager.getService(Ci.nsICategoryManager).
                     deleteCategoryEntry("Gecko-Content-Viewers",
                                         PDF_CONTENT_TYPE,
                                         false);
   },
 
   // nsIObserver
   observe: function observe(aSubject, aTopic, aData) {
+    if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+      throw new Error("Only the parent process should be observing PDF handler changes.");
+    }
+
     this.updateRegistration();
-    if (Services.appinfo.processType ===
-        Services.appinfo.PROCESS_TYPE_DEFAULT) {
-      let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm";
-      let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils;
-      PdfjsChromeUtils.notifyChildOfSettingsChange();
-    }
+    let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm";
+    let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils;
+    PdfjsChromeUtils.notifyChildOfSettingsChange(this.enabled);
   },
 
   /**
    * pdf.js is only enabled if it is both selected as the pdf viewer and if the
    * global switch enabling it is true.
    * @return {boolean} Whether or not it's enabled.
    */
   get enabled() {
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -119,25 +119,25 @@ var PdfjsChromeUtils = {
   },
 
   /*
    * Called by the main module when preference changes are picked up
    * in the parent process. Observers don't propagate so we need to
    * instruct the child to refresh its configuration and (possibly)
    * the module's registration.
    */
-  notifyChildOfSettingsChange() {
+  notifyChildOfSettingsChange(enabled) {
     if (Services.appinfo.processType ===
         Services.appinfo.PROCESS_TYPE_DEFAULT && this._ppmm) {
       // XXX kinda bad, we want to get the parent process mm associated
       // with the content process. _ppmm is currently the global process
       // manager, which means this is going to fire to every child process
       // we have open. Unfortunately I can't find a way to get at that
       // process specific mm from js.
-      this._ppmm.broadcastAsyncMessage("PDFJS:Child:refreshSettings", {});
+      this._ppmm.broadcastAsyncMessage("PDFJS:Child:updateSettings", {enabled});
     }
   },
 
   /*
    * Events
    */
 
   observe(aSubject, aTopic, aData) {
--- a/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsContentUtils.jsm
@@ -38,25 +38,25 @@ var PdfjsContentUtils = {
   },
 
   init() {
     // child *process* mm, or when loaded into the parent for in-content
     // support the psuedo child process mm 'child PPMM'.
     if (!this._mm) {
       this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].
         getService(Ci.nsISyncMessageSender);
-      this._mm.addMessageListener("PDFJS:Child:refreshSettings", this);
+      this._mm.addMessageListener("PDFJS:Child:updateSettings", this);
 
       Services.obs.addObserver(this, "quit-application");
     }
   },
 
   uninit() {
     if (this._mm) {
-      this._mm.removeMessageListener("PDFJS:Child:refreshSettings", this);
+      this._mm.removeMessageListener("PDFJS:Child:updateSettings", this);
       Services.obs.removeObserver(this, "quit-application");
     }
     this._mm = null;
   },
 
   /*
    * prefs utilities - the child does not have write access to prefs.
    * note, the pref names here are cross-checked against a list of
@@ -93,24 +93,16 @@ var PdfjsContentUtils = {
   setStringPref(aPrefName, aPrefValue) {
     this._mm.sendSyncMessage("PDFJS:Parent:setStringPref", {
       name: aPrefName,
       value: aPrefValue
     });
   },
 
   /*
-   * Forwards default app query to the parent where we check various
-   * handler app settings only available in the parent process.
-   */
-  isDefaultHandlerApp() {
-    return this._mm.sendSyncMessage("PDFJS:Parent:isDefaultHandlerApp")[0];
-  },
-
-  /*
    * Request the display of a notification warning in the associated window
    * when the renderer isn't sure a pdf displayed correctly.
    */
   displayWarning(aWindow, aMessage, aLabel, aAccessKey) {
     // the child's dom frame mm associated with the window.
     let winmm = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                        .getInterface(Ci.nsIDocShell)
                        .QueryInterface(Ci.nsIInterfaceRequestor)
@@ -129,21 +121,25 @@ var PdfjsContentUtils = {
   observe(aSubject, aTopic, aData) {
     if (aTopic === "quit-application") {
       this.uninit();
     }
   },
 
   receiveMessage(aMsg) {
     switch (aMsg.name) {
-      case "PDFJS:Child:refreshSettings":
+      case "PDFJS:Child:updateSettings":
         // Only react to this if we are remote.
         if (Services.appinfo.processType ===
             Services.appinfo.PROCESS_TYPE_CONTENT) {
           let jsm = "resource://pdf.js/PdfJs.jsm";
           let pdfjs = Components.utils.import(jsm, {}).PdfJs;
-          pdfjs.updateRegistration();
+          if (aMsg.data.enabled) {
+            pdfjs.ensureRegistered();
+          } else {
+            pdfjs.ensureUnregistered();
+          }
         }
         break;
     }
   }
 };
 
--- a/browser/extensions/pocket/content/main.js
+++ b/browser/extensions/pocket/content/main.js
@@ -546,17 +546,17 @@ var pktUI = (function() {
             frame.setAttribute("type", "content");
             frameParent.appendChild(frame);
         }
         return frame;
     }
 
     function getSubview() {
         var view = document.getElementById("PanelUI-pocketView");
-        if (view && view.getAttribute("current") == "true")
+        if (view && view.getAttribute("current") == "true" && !view.getAttribute("mainview"))
             return view;
         return null;
     }
 
     function isInOverflowMenu() {
         var subview = getSubview();
         return !!subview;
     }
--- a/browser/extensions/shield-recipe-client/jar.mn
+++ b/browser/extensions/shield-recipe-client/jar.mn
@@ -1,9 +1,9 @@
 # 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/.
 
 [features/shield-recipe-client@mozilla.org] chrome.jar:
 % resource shield-recipe-client %content/
   content/lib/ (./lib/*)
-  content/data/ (./data/*)
   content/node_modules/jexl/ (./node_modules/jexl/*)
+  content/skin/  (skin/*)
--- a/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
+++ b/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
@@ -1,30 +1,55 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {utils: Cu} = Components;
+const {utils: Cu, interfaces: Ci} = Components;
 
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
 Cu.import("resource://shield-recipe-client/lib/EventEmitter.jsm");
 Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
 
 Cu.importGlobalProperties(["URL"]); /* globals URL */
 
 this.EXPORTED_SYMBOLS = ["Heartbeat"];
 
-const log = LogManager.getLogger("heartbeat");
 const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
 const NOTIFICATION_TIME = 3000;
+const HEARTBEAT_CSS_URI = Services.io.newURI("resource://shield-recipe-client/skin/shared/Heartbeat.css");
+const HEARTBEAT_CSS_URI_OSX = Services.io.newURI("resource://shield-recipe-client/skin/osx/Heartbeat.css");
+
+const log = LogManager.getLogger("heartbeat");
+const windowsWithInjectedCss = new WeakSet();
+let anyWindowsWithInjectedCss = false;
+
+// Add cleanup handler for CSS injected into windows by Heartbeat
+CleanupManager.addCleanupHandler(() => {
+  if (anyWindowsWithInjectedCss) {
+    const windowEnumerator = Services.wm.getEnumerator("navigator:browser");
+    while (windowEnumerator.hasMoreElements()) {
+      const window = windowEnumerator.getNext();
+      if (windowsWithInjectedCss.has(window)) {
+        const utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+        utils.removeSheet(HEARTBEAT_CSS_URI, window.AGENT_SHEET);
+        if (AppConstants.platform === "macosx") {
+          utils.removeSheet(HEARTBEAT_CSS_URI_OSX, window.AGENT_SHEET);
+        }
+        windowsWithInjectedCss.delete(window);
+      }
+    }
+  }
+});
 
 /**
  * Show the Heartbeat UI to request user feedback.
  *
  * @param chromeWindow
  *        The chrome window that the heartbeat notification is displayed in.
  * @param sandboxManager
  *        The manager for the sandbox this was called from. Heartbeat will
@@ -92,16 +117,26 @@ this.Heartbeat = class {
 
     this.chromeWindow = chromeWindow;
     this.eventEmitter = new EventEmitter(sandboxManager);
     this.sandboxManager = sandboxManager;
     this.options = options;
     this.surveyResults = {};
     this.buttons = null;
 
+    if (!windowsWithInjectedCss.has(chromeWindow)) {
+      windowsWithInjectedCss.add(chromeWindow);
+      const utils = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      utils.loadSheet(HEARTBEAT_CSS_URI, chromeWindow.AGENT_SHEET);
+      if (AppConstants.platform === "macosx") {
+        utils.loadSheet(HEARTBEAT_CSS_URI_OSX, chromeWindow.AGENT_SHEET);
+      }
+      anyWindowsWithInjectedCss = true;
+    }
+
     // so event handlers are consistent
     this.handleWindowClosed = this.handleWindowClosed.bind(this);
     this.close = this.close.bind(this);
 
     if (this.options.engagementButtonLabel) {
       this.buttons = [{
         label: this.options.engagementButtonLabel,
         callback: () => {
@@ -119,17 +154,17 @@ this.Heartbeat = class {
         },
       }];
     }
 
     this.notificationBox = this.chromeWindow.document.querySelector("#high-priority-global-notificationbox");
     this.notice = this.notificationBox.appendNotification(
       this.options.message,
       "heartbeat-" + this.options.flowId,
-      "chrome://browser/skin/heartbeat-icon.svg",
+      "resource://shield-recipe-client/skin/shared/heartbeat-icon.svg",
       this.notificationBox.PRIORITY_INFO_HIGH,
       this.buttons,
       eventType => {
         if (eventType !== "removed") {
           return;
         }
         this.maybeNotifyHeartbeat("NotificationClosed");
       }
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/osx/Heartbeat.css
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/* Notification overrides for Heartbeat UI */
+
+notification.heartbeat {
+  background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%);
+  border-bottom: 1px solid #C1C1C1;
+  height: 40px;
+}
+
+/* In themes/osx/global/notification.css the close icon is inverted because notifications
+   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
+
+notification.heartbeat[type="info"] .close-icon:not(:hover) {
+  -moz-image-region: rect(0, 16px, 16px, 0) !important;
+}
+
+@media (min-resolution: 2dppx) {
+  notification.heartbeat[type="info"] .close-icon:not(:hover) {
+    -moz-image-region: rect(0, 32px, 32px, 0) !important;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/Heartbeat.css
@@ -0,0 +1,130 @@
+/* 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/. */
+
+/* Notification overrides for Heartbeat UI */
+
+notification.heartbeat {
+  background-color: #F1F1F1;
+  border-bottom: 1px solid #C1C1C1;
+  height: 40px;
+}
+
+/* In themes/osx/global/notification.css the close icon is inverted because notifications
+   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
+
+@keyframes pulse-onshow {
+  0% {
+    opacity: 0;
+    transform: scale(1);
+  }
+
+  25% {
+    opacity: 1;
+    transform: scale(1.1);
+  }
+
+  50% {
+    transform: scale(1);
+  }
+
+  75% {
+    transform: scale(1.1);
+  }
+
+  100% {
+    transform: scale(1);
+  }
+}
+
+@keyframes pulse-twice {
+  0% {
+    transform: scale(1.1);
+  }
+
+  50% {
+    transform: scale(0.8);
+  }
+
+  100% {
+    transform: scale(1);
+  }
+}
+
+.messageText.heartbeat {
+  color: #333;
+  margin-inline-end: 12px !important; /* The !important is required to override OSX default style. */
+  margin-inline-start: 0;
+  text-shadow: none;
+}
+
+.messageImage.heartbeat {
+  height: 24px;
+  margin-inline-end: 8px;
+  margin-inline-start: 8px;
+  width: 24px;
+}
+
+.messageImage.heartbeat.pulse-onshow {
+  animation-duration: 1.5s;
+  animation-iteration-count: 1;
+  animation-name: pulse-onshow;
+  animation-timing-function: cubic-bezier(0.7, 1.8, 0.9, 1.1);
+}
+
+.messageImage.heartbeat.pulse-twice {
+  animation-duration: 1s;
+  animation-iteration-count: 2;
+  animation-name: pulse-twice;
+  animation-timing-function: linear;
+}
+
+/* Learn More link styles */
+.heartbeat > .text-link {
+  color: #0095DD;
+  margin-inline-start: 0;
+}
+
+.heartbeat > .text-link:hover {
+  color: #008ACB;
+  text-decoration: none;
+}
+
+.heartbeat > .text-link:hover:active {
+  color: #006B9D;
+}
+
+/* Heartbeat UI Rating Star Classes */
+.heartbeat > #star-rating-container {
+  display: -moz-box;
+  margin-bottom: 4px;
+}
+
+.heartbeat > #star-rating-container > #star5 {
+  -moz-box-ordinal-group: 5;
+}
+
+.heartbeat > #star-rating-container > #star4 {
+  -moz-box-ordinal-group: 4;
+}
+
+.heartbeat > #star-rating-container > #star3 {
+  -moz-box-ordinal-group: 3;
+}
+
+.heartbeat > #star-rating-container > #star2 {
+  -moz-box-ordinal-group: 2;
+}
+
+.heartbeat > #star-rating-container > .star-x {
+  background: url("resource://shield-recipe-client/skin/shared/heartbeat-star-off.svg");
+  cursor: pointer;
+  height: 16px;
+  margin-inline-end: 4px !important; /* Overrides the margin-inline-end for all platforms defined in the .plain class */
+  width: 16px;
+}
+
+.heartbeat > #star-rating-container > .star-x:hover,
+.heartbeat > #star-rating-container > .star-x:hover ~ .star-x {
+  background: url("resource://shield-recipe-client/skin/shared/heartbeat-star-lit.svg");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-icon.svg
@@ -0,0 +1,13 @@
+<?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/. -->
+<svg width="288px" height="248px" viewBox="0 0 288 248" xmlns="http://www.w3.org/2000/svg">
+  <path fill="#d74345" d="M144,248.571429 C141.214272,248.571429 138.857152,247.607152 136.928571,245.678571 L36.6428571,148.928571 C35.5714232,148.071424 34.0982237,146.678581 32.2232143,144.75 C30.3482049,142.821419 27.3750204,139.312525 23.3035714,134.223214 C19.2321225,129.133903 15.5893018,123.910741 12.375,118.553571 C9.16069821,113.196402 6.29465545,106.714324 3.77678571,99.1071429 C1.25891598,91.499962 0,84.1071788 0,76.9285714 C0,53.357025 6.80350339,34.9286379 20.4107143,21.6428571 C34.0179252,8.35707643 52.8213086,1.71428571 76.8214286,1.71428571 C83.4643189,1.71428571 90.2410369,2.86605991 97.1517857,5.16964286 C104.062535,7.4732258 110.491042,10.5803376 116.4375,14.4910714 C122.383958,18.4018053 127.499979,22.0714114 131.785714,25.5 C136.07145,28.9285886 140.142838,32.5714093 144,36.4285714 C147.857162,32.5714093 151.92855,28.9285886 156.214286,25.5 C160.500021,22.0714114 165.616042,18.4018053 171.5625,14.4910714 C177.508958,10.5803376 183.937465,7.4732258 190.848214,5.16964286 C197.758963,2.86605991 204.535681,1.71428571 211.178571,1.71428571 C235.178691,1.71428571 253.982075,8.35707643 267.589286,21.6428571 C281.196497,34.9286379 288,53.357025 288,76.9285714 C288,100.607261 275.732266,124.714163 251.196429,149.25 L151.071429,245.678571 C149.142847,247.607152 146.785728,248.571429 144,248.571429 L144,248.571429 Z" transform="translate(0,-1)"/>
+  <g transform="translate(0,-0.29)">
+    <mask id="mask" fill="#fff">
+      <path d="M144,246.857143 C141.214272,246.857143 138.857152,245.892867 136.928571,243.964286 L36.6428571,147.214286 C35.5714232,146.357139 34.0982237,144.964295 32.2232143,143.035714 C30.3482049,141.107133 27.3750204,137.59824 23.3035714,132.508929 C19.2321225,127.419617 15.5893018,122.196455 12.375,116.839286 C9.16069821,111.482116 6.29465545,105.000038 3.77678571,97.3928571 C1.25891598,89.7856763 0,82.392893 0,75.2142857 C0,51.6427393 6.80350339,33.2143521 20.4107143,19.9285714 C34.0179252,6.64279071 52.8213086,0 76.8214286,0 C83.4643189,0 90.2410369,1.1517742 97.1517857,3.45535714 C104.062535,5.75894009 110.491042,8.86605187 116.4375,12.7767857 C122.383958,16.6875196 127.499979,20.3571257 131.785714,23.7857143 C136.07145,27.2143029 140.142838,30.8571236 144,34.7142857 C147.857162,30.8571236 151.92855,27.2143029 156.214286,23.7857143 C160.500021,20.3571257 165.616042,16.6875196 171.5625,12.7767857 C177.508958,8.86605187 183.937465,5.75894009 190.848214,3.45535714 C197.758963,1.1517742 204.535681,0 211.178571,0 C235.178691,0 253.982075,6.64279071 267.589286,19.9285714 C281.196497,33.2143521 288,51.6427393 288,75.2142857 C288,98.8929755 275.732266,122.999877 251.196429,147.535714 L151.071429,243.964286 C149.142847,245.892867 146.785728,246.857143 144,246.857143 L144,246.857143 Z"/>
+    </mask>
+    <path fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" mask="url(#mask)" d="M-166,115.135254 C-166,115.135254 0.595052083,115.135254 2.9765625,115.135254 L91.9101562,115.135254 L97.9638977,100.101562 L105.430695,115.135254 L114.893585,115.135254 L131.129913,189.53125 L148.161163,57 L165.348663,131.027344 L172.272491,115.135254 L250.84967,115.135254 L428.259813,115.135254"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-star-lit.svg
@@ -0,0 +1,7 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="100%" height="100%">
+  <path fill="#0095dd" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-star-off.svg
@@ -0,0 +1,7 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="100%" height="100%">
+  <path fill="#c0c0c0" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
+</svg>
--- a/browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
+++ b/browser/extensions/webcompat-reporter/content/WebCompatReporter.jsm
@@ -116,17 +116,17 @@ let WebCompatReporter = {
   openWebCompatTab([gBrowser, tabData]) {
     const SCREENSHOT_MESSAGE = "WebCompat:SendScreenshot";
     const FRAMESCRIPT = "chrome://webcompat-reporter/content/wc-frame.js";
     let win = Services.wm.getMostRecentWindow("navigator:browser");
     const WEBCOMPAT_ORIGIN = new win.URL(WebCompatReporter.endpoint).origin;
 
     let tab = gBrowser.loadOneTab(
       `${WebCompatReporter.endpoint}?url=${encodeURIComponent(tabData.url)}&src=desktop-reporter`,
-      {inBackground: false});
+      {inBackground: false, triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal()});
 
     // If we successfully got a screenshot blob, add a listener to know when
     // the new tab is loaded before sending it over.
     if (tabData && tabData.blob) {
       let browser = gBrowser.getBrowserForTab(tab);
       let loadedListener = {
         QueryInterface: XPCOMUtils.generateQI(["nsIWebProgressListener",
           "nsISupportsWeakReference"]),
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -563,16 +563,21 @@
 #endif
 
 #if defined(ENABLE_TESTS) && defined(MOZ_DEBUG)
 @RESPATH@/components/TestInterfaceJS.js
 @RESPATH@/components/TestInterfaceJS.manifest
 @RESPATH@/components/TestInterfaceJSMaplike.js
 #endif
 
+#if defined(MOZ_DEBUG) || defined(NIGHTLY_BUILD)
+@RESPATH@/browser/components/testComponents.manifest
+@RESPATH@/browser/components/startupRecorder.js
+#endif
+
 ; [Extensions]
 @RESPATH@/components/extensions-toolkit.manifest
 @RESPATH@/browser/components/extensions-browser.manifest
 
 ; Modules
 @RESPATH@/browser/modules/*
 @RESPATH@/modules/*
 
@@ -741,19 +746,19 @@
 @RESPATH@/chrome/pippki.manifest
 @RESPATH@/components/pipnss.xpt
 @RESPATH@/components/pippki.xpt
 
 ; For process sandboxing
 #if defined(MOZ_SANDBOX)
 #if defined(XP_LINUX)
 @BINPATH@/@DLL_PREFIX@mozsandbox@DLL_SUFFIX@
+#endif
 @RESPATH@/components/sandbox.xpt
 #endif
-#endif
 
 ; for Solaris SPARC
 #ifdef SOLARIS
 bin/libfreebl_32fpu_3.so
 bin/libfreebl_32int_3.so
 bin/libfreebl_32int64_3.so
 #endif
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -683,18 +683,17 @@ you can use these alternative items. Oth
 <!ENTITY quitApplicationCmdWin2.label       "Exit">
 <!ENTITY quitApplicationCmdWin2.accesskey   "x">
 <!ENTITY quitApplicationCmdWin2.tooltip     "Exit &brandShorterName;">
 <!ENTITY goBackCmd.commandKey "[">
 <!ENTITY goForwardCmd.commandKey "]">
 <!ENTITY quitApplicationCmd.label       "Quit"> 
 <!ENTITY quitApplicationCmd.accesskey   "Q">
 <!ENTITY quitApplicationCmdMac2.label   "Quit &brandShorterName;">
-<!-- LOCALIZATION NOTE(quitApplicationCmdUnix.key): This keyboard shortcut is used by both Linux and OSX builds. -->
-<!ENTITY quitApplicationCmdUnix.key     "Q">
+<!ENTITY quitApplicationCmd.key         "Q">
 
 <!ENTITY closeCmd.label                 "Close">  
 <!ENTITY closeCmd.key                   "W">  
 <!ENTITY closeCmd.accesskey             "C">
 
 <!ENTITY toggleMuteCmd.key              "M">
 
 <!ENTITY pageStyleMenu.label "Page Style">
--- a/browser/locales/en-US/chrome/browser/migration/migration.properties
+++ b/browser/locales/en-US/chrome/browser/migration/migration.properties
@@ -67,21 +67,21 @@ 64_edge=Other Data
 64_safari=Other Data
 64_chrome=Other Data
 64_firefox_other=Other Data
 64_360se=Other Data
 
 128_firefox=Windows and Tabs
 
 # Automigration undo notification.
-# %1$S will be replaced with the name of the browser we imported from, %2$S will be replaced with brandShortName
-automigration.undo.message.all              = Pick up where you left off. We’ve imported these sites and your bookmarks, history and passwords from %1$S into %2$S.
-automigration.undo.message.bookmarks        = Pick up where you left off. We’ve imported these sites and your bookmarks from %1$S into %2$S.
-automigration.undo.message.bookmarks.logins = Pick up where you left off. We’ve imported these sites and your bookmarks and passwords from %1$S into %2$S.
-automigration.undo.message.bookmarks.visits = Pick up where you left off. We’ve imported these sites and your bookmarks and history from %1$S into %2$S.
-automigration.undo.message.logins           = Pick up where you left off. We’ve imported your passwords from %1$S into %2$S.
-automigration.undo.message.logins.visits    = Pick up where you left off. We’ve imported these sites and your history and passwords from %1$S into %2$S.
-automigration.undo.message.visits           = Pick up where you left off. We’ve imported these sites and your history from %1$S into %2$S.
+# %1$S will be replaced with brandShortName, %2$S will be replaced with the name of the browser we imported from
+automigration.undo.message2.all              = Dive right into %1$S! Import your favorite sites, bookmarks, history and passwords from %2$S.
+automigration.undo.message2.bookmarks        = Dive right into %1$S! Import your favorite sites and bookmarks from %2$S.
+automigration.undo.message2.bookmarks.logins = Dive right into %1$S! Import your favorite sites, bookmarks and passwords from %2$S.
+automigration.undo.message2.bookmarks.visits = Dive right into %1$S! Import your favorite sites, bookmarks and history from %2$S.
+automigration.undo.message2.logins           = Dive right into %1$S! Import your passwords from %2$S.
+automigration.undo.message2.logins.visits    = Dive right into %1$S! Import your favorite sites, history and passwords from %2$S.
+automigration.undo.message2.visits           = Dive right into %1$S! Import your favorite sites and history from %2$S.
 automigration.undo.keep2.label            = OK, Got it
 automigration.undo.keep2.accesskey        = O
 automigration.undo.dontkeep2.label        = No Thanks
 automigration.undo.dontkeep2.accesskey    = N
 automigration.undo.unknownbrowser         = Unknown Browser