Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 01 May 2015 10:27:52 -0400
changeset 273250 60b269fed8cfad8c7028a0292721aa241f94a361
parent 273184 401d86355848e02184f61144bcd32052e54c5ce5 (current diff)
parent 273249 f27feb597c05ce0dea505f25ad54730487519d88 (diff)
child 273354 e70555ac58d8a6b15efe667bb419726849b1cfc3
push id863
push userraliiev@mozilla.com
push dateMon, 03 Aug 2015 13:22:43 +0000
treeherdermozilla-release@f6321b14228d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
browser/components/loop/CardDavImporter.jsm
browser/components/loop/GoogleImporter.jsm
browser/components/loop/LoopCalls.jsm
browser/components/loop/LoopContacts.jsm
browser/components/loop/LoopRooms.jsm
browser/components/loop/LoopStorage.jsm
browser/components/loop/MozLoopAPI.jsm
browser/components/loop/MozLoopPushHandler.jsm
browser/components/loop/MozLoopService.jsm
browser/components/loop/MozLoopWorker.js
browser/components/loop/content/shared/.eslintrc
browser/components/loop/standalone/.eslintrc
browser/devtools/performance/test/browser_timeline_blueprint.js
browser/devtools/performance/test/browser_timeline_filters.js
browser/devtools/timeline/test/browser.ini
browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js
browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
browser/devtools/timeline/test/browser_timeline_overview-theme.js
browser/devtools/timeline/test/browser_timeline_overview-update.js
browser/devtools/timeline/test/browser_timeline_panels.js
browser/devtools/timeline/test/browser_timeline_recording-without-memory.js
browser/devtools/timeline/test/browser_timeline_recording.js
browser/devtools/timeline/test/browser_timeline_waterfall-background.js
browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
browser/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
browser/devtools/timeline/test/doc_simple-test.html
browser/devtools/timeline/test/head.js
toolkit/themes/windows/mozapps/extensions/navigation.png
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1322,18 +1322,18 @@ pref("services.sync.prefs.sync.security.
 pref("services.sync.prefs.sync.security.tls.version.max", true);
 pref("services.sync.prefs.sync.signon.rememberSignons", true);
 pref("services.sync.prefs.sync.spellchecker.dictionary", true);
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 #endif
 
 // Developer edition preferences
 #ifdef MOZ_DEV_EDITION
-pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
-pref("browser.devedition.theme.enabled", true);
+sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org");
+sticky_pref("browser.devedition.theme.enabled", true);
 #endif
 
 // Developer edition promo preferences
 pref("devtools.devedition.promo.shown", false);
 pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer/?utm_source=firefox-dev-tools&utm_medium=firefox-browser&utm_content=betadoorhanger");
 
 // Only potentially show in beta release
 #if MOZ_UPDATE_CHANNEL == beta
@@ -1438,16 +1438,25 @@ pref("devtools.performance.profiler.samp
 pref("devtools.performance.ui.invert-call-tree", true);
 pref("devtools.performance.ui.invert-flame-graph", false);
 pref("devtools.performance.ui.flatten-tree-recursion", true);
 pref("devtools.performance.ui.show-platform-data", false);
 pref("devtools.performance.ui.show-idle-blocks", true);
 pref("devtools.performance.ui.enable-memory", false);
 pref("devtools.performance.ui.enable-framerate", true);
 pref("devtools.performance.ui.show-jit-optimizations", false);
+// If in aurora (40.0, will revert for 40.1), set default
+// to retro mode.
+// TODO bug 1160313
+#if MOZ_UPDATE_CHANNEL == aurora
+  pref("devtools.performance.ui.retro-mode", true);
+#else
+  pref("devtools.performance.ui.retro-mode", false);
+#endif
+
 
 // The default cache UI setting
 pref("devtools.cache.disabled", false);
 
 // The default service workers UI setting
 pref("devtools.serviceWorkers.testing.enabled", false);
 
 // Enable the Network Monitor
@@ -1502,19 +1511,19 @@ pref("devtools.canvasdebugger.enabled", 
 // Enable the Web Audio Editor
 pref("devtools.webaudioeditor.enabled", false);
 
 // Web Audio Editor Inspector Width should be a preference
 pref("devtools.webaudioeditor.inspectorWidth", 300);
 
 // Default theme ("dark" or "light")
 #ifdef MOZ_DEV_EDITION
-pref("devtools.theme", "dark");
+sticky_pref("devtools.theme", "dark");
 #else
-pref("devtools.theme", "light");
+sticky_pref("devtools.theme", "light");
 #endif
 
 // Display the introductory text
 pref("devtools.gcli.hideIntro", false);
 
 // How eager are we to show help: never=1, sometimes=2, always=3
 pref("devtools.gcli.eagerHelper", 2);
 
@@ -1641,20 +1650,20 @@ pref("browser.newtabpage.enabled", true)
 
 // number of rows of newtab grid
 pref("browser.newtabpage.rows", 3);
 
 // number of columns of newtab grid
 pref("browser.newtabpage.columns", 5);
 
 // directory tiles download URL
-pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v2/links/fetch/%LOCALE%");
+pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%");
 
 // endpoint to send newtab click and view pings
-pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v2/links/");
+pref("browser.newtabpage.directory.ping", "https://tiles.services.mozilla.com/v3/links/");
 
 // Enable the DOM fullscreen API.
 pref("full-screen-api.enabled", true);
 
 // True if the fullscreen API requires approval upon a domain entering fullscreen.
 // Domains that have already had fullscreen permission granted won't re-request
 // approval.
 pref("full-screen-api.approval-required", true);
@@ -1858,25 +1867,25 @@ pref("media.gmp-provider.enabled", true)
 
 pref("browser.apps.URL", "https://marketplace.firefox.com/discovery/");
 
 #ifdef NIGHTLY_BUILD
 pref("browser.polaris.enabled", false);
 pref("privacy.trackingprotection.ui.enabled", false);
 #endif
 
-#ifdef NIGHTLY_BUILD
+#ifdef E10S_TESTING_ONLY
 // At the moment, autostart.2 is used, while autostart.1 is unused.
 // We leave it here set to false to reset users' defaults and allow
 // us to change everybody to true in the future, when desired.
 pref("browser.tabs.remote.autostart.1", false);
 pref("browser.tabs.remote.autostart.2", true);
 #endif
 
-#ifdef NIGHTLY_BUILD
+#ifdef E10S_TESTING_ONLY
 // Enable e10s add-on interposition by default.
 pref("extensions.interposition.enabled", true);
 pref("extensions.interposition.prefetching", true);
 #endif
 
 pref("browser.defaultbrowser.notificationbar", false);
 
 // How often to check for CPOW timeouts. CPOWs are only timed out by
@@ -1895,16 +1904,17 @@ pref("dom.ipc.reportProcessHangs", false
 pref("dom.ipc.reportProcessHangs", true);
 #endif
 
 pref("browser.readinglist.enabled", false);
 pref("browser.readinglist.sidebarEverOpened", false);
 pref("readinglist.scheduler.enabled", false);
 pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
 
+pref("browser.reader.detectedFirstArticle", false);
 // Don't limit how many nodes we care about on desktop:
 pref("reader.parse-node-limit", 0);
 
 // Enable Service workers for desktop on non-release builds
 #ifdef NIGHTLY_BUILD
 pref("dom.serviceWorkers.enabled", true);
 #endif
 
--- a/browser/base/content/newtab/sites.js
+++ b/browser/base/content/newtab/sites.js
@@ -128,17 +128,17 @@ Site.prototype = {
     let link = this._querySelector(".newtab-link");
     link.setAttribute("title", tooltip);
     link.setAttribute("href", url);
     this._querySelector(".newtab-title").textContent = title;
     this.node.setAttribute("type", this.link.type);
 
     if (this.link.targetedSite) {
       this.node.setAttribute("suggested", true);
-      let targetedSite = `<strong> ${this.link.targetedSite} </strong>`;
+      let targetedSite = `<strong> ${this.link.targetedName} </strong>`;
       this._querySelector(".newtab-suggested").innerHTML =
         `<div class='newtab-suggested-bounds'> ${newTabString("suggested.button", [targetedSite])} </div>`;
     }
 
     if (this.isPinned())
       this._updateAttributes(true);
     // Capture the page if the thumbnail is missing, which will cause page.js
     // to be notified and call our refreshThumbnail() method.
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -550,17 +550,19 @@ function gKeywordURIFixup(fixupInfo) {
   sendAsyncMessage("Browser:URIFixup", data);
 }
 Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false);
 addEventListener("unload", () => {
   Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
 }, false);
 
 addMessageListener("Browser:AppTab", function(message) {
-  docShell.isAppTab = message.data.isAppTab;
+  if (docShell) {
+    docShell.isAppTab = message.data.isAppTab;
+  }
 });
 
 let WebBrowserChrome = {
   onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
     return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
   },
 
   // Check whether this URI should load in the current process
--- a/browser/base/content/test/chat/browser_chatwindow.js
+++ b/browser/base/content/test/chat/browser_chatwindow.js
@@ -1,40 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 let chatbar = document.getElementById("pinnedchats");
 
-function waitForCondition(condition, errorMsg) {
-  let deferred = Promise.defer();
-  var tries = 0;
-  var interval = setInterval(function() {
-    if (tries >= 30) {
-      ok(false, errorMsg);
-      moveOn();
-    }
-    var conditionPassed;
-    try {
-      conditionPassed = condition();
-    } catch (e) {
-      ok(false, e + "\n" + e.stack);
-      conditionPassed = false;
-    }
-    if (conditionPassed) {
-      moveOn();
-    }
-    tries++;
-  }, 100);
-  var moveOn = function() { clearInterval(interval); deferred.resolve(); };
-  return deferred.promise;
-}
-
 add_chat_task(function* testOpenCloseChat() {
   let chatbox = yield promiseOpenChat("http://example.com");
   Assert.strictEqual(chatbox, chatbar.selectedChat);
   // we requested a "normal" chat, so shouldn't be minimized
   Assert.ok(!chatbox.minimized, "chat is not minimized");
   Assert.equal(chatbar.childNodes.length, 1, "should be 1 chat open");
 
 
@@ -110,56 +86,48 @@ add_chat_task(function* testSecondTopLev
   yield promiseOneEvent(secondWindow, "load");
   yield promiseOpenChat(chatUrl);
   // the chat was created - let's make sure it was created in the second window.
   Assert.equal(numChatsInWindow(window), 0, "main window has no chats");
   Assert.equal(numChatsInWindow(secondWindow), 1, "second window has 1 chat");
   secondWindow.close();
 });
 
-// Test that chats are created in the correct window.
+// Test that findChromeWindowForChats() returns the expected window.
 add_chat_task(function* testChatWindowChooser() {
   let chat = yield promiseOpenChat("http://example.com");
   Assert.equal(numChatsInWindow(window), 1, "first window has the chat");
   // create a second window - this will be the "most recent" and will
   // therefore be the window that hosts the new chat (see bug 835111)
   let secondWindow = OpenBrowserWindow();
   yield promiseOneEvent(secondWindow, "load");
   Assert.equal(secondWindow, Chat.findChromeWindowForChats(null), "Second window is the preferred chat window");
-  Assert.equal(numChatsInWindow(secondWindow), 0, "second window starts with no chats");
-  yield promiseOpenChat("http://example.com#2");
-  Assert.equal(numChatsInWindow(secondWindow), 1, "second window now has chats");
-  Assert.equal(numChatsInWindow(window), 1, "first window still has 1 chat");
-  chat.close();
 
-  // a bit heavy handed, but handy fixing bug 1090633
-  yield waitForCondition(function () !chat.parentNode, "chat has been detached");
-  Assert.equal(numChatsInWindow(window), 0, "first window now has no chats");
-  // now open another chat - it should still open in the second.
-  yield promiseOpenChat("http://example.com#3");
-  Assert.equal(numChatsInWindow(window), 0, "first window still has no chats");
-  Assert.equal(numChatsInWindow(secondWindow), 2, "second window has both chats");
+  // focus the first window, and check it will be selected for future chats.
+  // Bug 1090633 - there are lots of issues around focus, especially when the
+  // browser itself doesn't have focus. We can end up with
+  // Services.wm.getMostRecentWindow("navigator:browser") returning a different
+  // window than, say, Services.focus.activeWindow. But the focus manager isn't
+  // the point of this test.
+  // So we simply keep focusing the first window until it is reported as the
+  // most recent.
+  do {
+    dump("trying to force window to become the most recent.\n");
+    secondWindow.focus();
+    window.focus();
+    yield promiseWaitForFocus();
+  } while (Services.wm.getMostRecentWindow("navigator:browser") != window)
 
-  // focus the first window, and open yet another chat - it
-  // should open in the first window.
-  window.focus();
-  yield promiseWaitForFocus();
-  chat = yield promiseOpenChat("http://example.com#4");
-  Assert.equal(numChatsInWindow(window), 1, "first window got new chat");
-  chat.close();
-  Assert.equal(numChatsInWindow(window), 0, "first window has no chats");
+  Assert.equal(window, Chat.findChromeWindowForChats(null), "First window now the preferred chat window");
 
   let privateWindow = OpenBrowserWindow({private: true});
   yield promiseOneEvent(privateWindow, "load")
 
-  // open a last chat - the focused window can't accept
-  // chats (it's a private window), so the chat should open
-  // in the window that was selected before. This is known
-  // to be broken on Linux.
-  chat = yield promiseOpenChat("http://example.com#5");
-  let os = Services.appinfo.OS;
-  const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin";
-  let fn = BROKEN_WM_Z_ORDER ? todo : ok;
-  fn(numChatsInWindow(window) == 1, "first window got the chat");
-  chat.close();
+  // The focused window can't accept chats (it's a private window), so the
+  // chat should open in the window that was selected before. This will be
+  // either window or secondWindow (linux may choose a different one) but the
+  // point is that the window is *not* the private one.
+  Assert.ok(Chat.findChromeWindowForChats(null) == window ||
+            Chat.findChromeWindowForChats(null) == secondWindow,
+            "Private window isn't selected for new chats.");
   privateWindow.close();
   secondWindow.close();
 });
--- a/browser/base/content/test/chat/browser_focus.js
+++ b/browser/base/content/test/chat/browser_focus.js
@@ -5,22 +5,26 @@
 // Tests the focus functionality.
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 const CHAT_URL = "https://example.com/browser/browser/base/content/test/chat/chat.html";
 
 // Is the currently opened tab focused?
 function isTabFocused() {
   let tabb = gBrowser.getBrowserForTab(gBrowser.selectedTab);
-  return Services.focus.focusedWindow == tabb.contentWindow;
+  // focus sucks in tests - our window may have lost focus.
+  let elt = Services.focus.getFocusedElementForWindow(window, false, {});
+  return elt == tabb;
 }
 
 // Is the specified chat focused?
 function isChatFocused(chat) {
-  return chat.chatbar._isChatFocused(chat);
+  // focus sucks in tests - our window may have lost focus.
+  let elt = Services.focus.getFocusedElementForWindow(window, false, {});
+  return elt == chat.content;
 }
 
 let chatbar = document.getElementById("pinnedchats");
 
 function* setUp() {
   // Note that (probably) due to bug 604289, if a tab is focused but the
   // focused element is null, our chat windows can "steal" focus.  This is
   // avoided if we explicitly focus an element in the tab.
--- a/browser/base/content/test/general/browser_readerMode.js
+++ b/browser/base/content/test/general/browser_readerMode.js
@@ -27,29 +27,39 @@ add_task(function* test_reader_button() 
       gBrowser.removeCurrentTab();
     }
   });
 
   // Set required test prefs.
   TEST_PREFS.forEach(([name, value]) => {
     Services.prefs.setBoolPref(name, value);
   });
+  Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", false);
 
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
+  ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"),
+     "Info panel shouldn't appear without the reader mode button");
+  ok(!Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
+     "Shouldn't have detected the first article");
 
   // Point tab to a test page that is reader-able.
   let url = TEST_PATH + "readerModeArticle.html";
   yield promiseTabLoadEvent(tab, url);
   yield promiseWaitForCondition(() => !readerButton.hidden);
   is_element_visible(readerButton, "Reader mode button is present on a reader-able page");
+  ok(UITour.isInfoOnTarget(window, "readerMode-urlBar"),
+     "Info panel should be anchored at the reader mode button");
+  ok(Services.prefs.getBoolPref("browser.reader.detectedFirstArticle"),
+     "Should have detected the first article");
 
   // Switch page into reader mode.
   readerButton.click();
   yield promiseTabLoadEvent(tab);
+  ok(!UITour.isInfoOnTarget(window, "readerMode-urlBar"), "Info panel should have closed");
 
   let readerUrl = gBrowser.selectedBrowser.currentURI.spec;
   ok(readerUrl.startsWith("about:reader"), "about:reader loaded after clicking reader mode button");
   is_element_visible(readerButton, "Reader mode button is present on about:reader");
 
   is(gURLBar.value, readerUrl, "gURLBar value is about:reader URL");
   is(gURLBar.textValue, url.substring("http://".length), "gURLBar is displaying original article URL");
 
--- a/browser/base/content/test/newtab/browser_newtab_block.js
+++ b/browser/base/content/test/newtab/browser_newtab_block.js
@@ -13,16 +13,18 @@ gDirectorySource = "data:application/jso
     imageURI: "data:image/png;base64,helloWORLD3",
     title: "title",
     type: "affiliate",
     frecent_sites: ["example0.com"]
   }]
 });
 
 function runTests() {
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = (site) => false;
 
   // we remove sites and expect the gaps to be filled as long as there still
   // are some sites available
   yield setLinks("0,1,2,3,4,5,6,7,8,9");
   setPinnedLinks("");
 
@@ -79,9 +81,10 @@ function runTests() {
   yield setLinks("0,1,2,3,4,5,6,7,8,9");
   yield addNewTabPageTab();
   yield customizeNewTabPage("enhanced");
   checkGrid("http://suggested.com/,0,1,2,3,4,5,6,7,8,9");
 
   yield blockCell(1);
   yield addNewTabPageTab();
   checkGrid("1,2,3,4,5,6,7,8,9");
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
 }
--- a/browser/base/content/test/newtab/browser_newtab_enhanced.js
+++ b/browser/base/content/test/newtab/browser_newtab_enhanced.js
@@ -21,18 +21,21 @@ gDirectorySource = "data:application/jso
     imageURI: "data:image/png;base64,helloWORLD3",
     title: "title2",
     type: "affiliate",
     frecent_sites: ["test.com"]
   }]
 });
 
 function runTests() {
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origEnhanced = NewTabUtils.allPages.enhanced;
   registerCleanupFunction(() => {
+    DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
     Services.prefs.clearUserPref(PRELOAD_PREF);
     NewTabUtils.allPages.enhanced = origEnhanced;
   });
 
   Services.prefs.setBoolPref(PRELOAD_PREF, false);
 
   function getData(cellNum) {
     let cell = getCell(cellNum);
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -1,15 +1,14 @@
-# We still need to fix warnings in this file.
-MozLoopWorker.js
-# This file currently uses es7 features
-MozLoopAPI.jsm
+# This file currently uses a non-standard (and not on a standards track)
+# if statement within catch.
+modules/MozLoopWorker.js
+# This file currently uses es7 features eslint issue:
+# https://github.com/eslint/espree/issues/125
+modules/MozLoopAPI.jsm
 # Libs we don't need to check
 content/libs
 content/shared/libs
 standalone/content/libs
 standalone/node_modules
-# We should look at turning these on when we fix the warnings
-test/xpcshell
-test/mochitest
 # Libs we don't need to check
 test/shared/vendor
 
--- a/browser/components/loop/.eslintrc
+++ b/browser/components/loop/.eslintrc
@@ -1,27 +1,17 @@
+// Note: there are extra allowances for files used solely in Firefox desktop,
+// see content/js/.eslintrc and modules/.eslintrc
 {
   "plugins": [
     "react"
   ],
   "ecmaFeatures": {
     "forOf": true,
     "jsx": true,
-    // These are on for this directory for .jsm and content/js files.
-    // If adding more items here, consider turning them off for the following
-    // files if they aren't supported by all browsers.
-    // content/shared/.eslintrc
-    // content/standalone/.eslintrc
-    "blockBindings": true,
-    "arrowFunctions": true,
-    "destructuring": true,
-    "generators": true,
-    "spread": true,
-    "restParams": true,
-    "objectLiteralShorthandMethods": true
   },
   "env": {
     "browser": true,
     "mocha": true
   },
   "globals": {
     "_": false,
     "$": false,
@@ -38,69 +28,71 @@
   },
   "rules": {
     // turn off all kinds of stuff that we actually do want, because
     // right now, we're bootstrapping the linting infrastructure.  We'll
     // want to audit these rules, and start turning them on and fixing the
     // problems they find, one at a time.
 
     // Eslint built-in rules are documented at <http://eslint.org/docs/rules/>
-    "camelcase": 0,
-    "comma-dangle": 0,
-    "comma-spacing": 0,
-    "consistent-return": 0,
-    "curly": 0,
-    "dot-notation": 0,
-    "eol-last": 0,
-    "eqeqeq": 0,
-    "global-strict": 0,
-    "key-spacing": 0,
-    "new-cap": 0,
-    "no-catch-shadow": 0,
-    "no-console": 0,
-    "no-empty": 0,
-    "no-extra-bind": 0,
-    "no-extra-boolean-cast": 0,
-    "no-extra-semi": 0,
-    "no-multi-spaces": 0,
-    "no-new": 0,
-    "no-redeclare": 0,
-    "no-return-assign": 0,
-    "no-shadow": 0,
-    "no-spaced-func": 0,
-    "no-trailing-spaces": 0,
-    "no-undef": 0,
-    "no-underscore-dangle": 0,
-    "no-unused-expressions": 0,
-    "no-unused-vars": 0,
-    "no-use-before-define": 0,
-    "no-wrap-func": 0,
-    "quotes": 0,
-    "semi": 0,
-    "semi-spacing": 0,
-    "space-infix-ops": 0,
-    "space-return-throw-case": 0,
-    "strict": 0,
-    "yoda": 0,
+    "camelcase": 0,               // TODO: Remove (use default)
+    "comma-dangle": 0,            // TODO: Remove (use default)
+    "comma-spacing": 0,           // TODO: Remove (use default)
+    "consistent-return": 0,       // TODO: Remove (use default)
+    "curly": 0,                   // TODO: Remove (use default)
+    "dot-notation": 0,            // TODO: Remove (use default)
+    "eol-last": 0,                // TODO: Remove (use default)
+    "eqeqeq": 0,                  // TBD. Might need to be separate for content & chrome
+    "global-strict": 0,           // Leave as zero (this will be unsupported in eslint 1.0.0)
+    "key-spacing": 0,             // TODO: Remove (use default)
+    "new-cap": 0,                 // TODO: Remove (use default)
+    "no-catch-shadow": 0,         // TODO: Remove (use default)
+    "no-console": 0,              // Leave as 0. We use console logging in content code.
+    "no-empty": 0,                // TODO: Remove (use default)
+    "no-extra-bind": 0,           // Leave as 0
+    "no-extra-boolean-cast": 0,   // TODO: Remove (use default)
+    "no-extra-semi": 0,           // TODO: Remove (use default)
+    "no-multi-spaces": 0,         // TBD.
+    "no-new": 0,                  // TODO: Remove (use default)
+    "no-redeclare": 0,            // TODO: Remove (use default)
+    "no-return-assign": 0,        // TODO: Remove (use default)
+    "no-shadow": 0,               // TODO: Remove (use default)
+    "no-spaced-func": 0,          // TODO: Remove (use default)
+    "no-trailing-spaces": 0,      // TODO: Remove (use default)
+    "no-undef": 0,                // TODO: Remove (use default)
+    "no-underscore-dangle": 0,    // Leave as 0. Commonly used for private variables.
+    "no-unused-expressions": 0,   // TODO: Remove (use default)
+    "no-unused-vars": 0,          // TODO: Remove (use default)
+    "no-use-before-define": 0,    // TODO: Remove (use default)
+    "no-wrap-func": 0,            // TODO: Remove (use default)
+    "quotes": 0,                  // [2, "double", "avoid-escape"],
+    "semi": 0,                    // TODO: Remove (use default)
+    "semi-spacing": 0,            // TODO: Remove (use default)
+    "space-infix-ops": 0,         // TODO: Remove (use default)
+    "space-return-throw-case": 0, // TODO: Remove (use default)
+    "strict": 0,                  // [2, "function"],
+    "yoda": 0,                    // [2, "never"],
     // eslint-plugin-react rules. These are documented at
     // <https://github.com/yannickcr/eslint-plugin-react#list-of-supported-rules>
     "react/jsx-quotes": [2, "double", "avoid-escape"],
     "react/jsx-no-undef": 2,
     // Need to fix instances where this is failing.
     "react/jsx-sort-props": 0,
+    "react/jsx-sort-prop-types": 0,
     "react/jsx-uses-vars": 2,
     // Need to fix the couple of instances which don't
     // currently pass this rule.
     "react/no-did-mount-set-state": 0,
     "react/no-did-update-set-state": 2,
     "react/no-unknown-property": 2,
     // Need to fix instances where this is currently failing
     "react/prop-types": 0,
     "react/self-closing-comp": 2,
     "react/wrap-multilines": 2,
     // Not worth it: React is defined globally
     "react/jsx-uses-react": 0,
     "react/react-in-jsx-scope": 0,
     // These ones we don't want to ever enable
     "react/display-name": 0,
+    "react/jsx-boolean-value": 0,
     "react/no-multi-comp": 0
   }
 }
--- a/browser/components/loop/README.txt
+++ b/browser/components/loop/README.txt
@@ -49,17 +49,17 @@ you've installed the dependencies by typ
 
 If you install eslint and the react plugin globally:
 
   npm install -g eslint
   npm install -g eslint-plugin-react
 
 You can also run it by hand in the browser/components/loop directory:
 
-  eslint -ext .js -ext .jsx --ext .jsm .
+  eslint --ext .js --ext .jsx --ext .jsm .
 
 Front-End Unit Tests
 ====================
 The unit tests for Loop reside in three directories:
 
 - test/desktop-local
 - test/shared
 - test/standalone
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/.eslintrc
@@ -0,0 +1,15 @@
+{
+  "ecmaFeatures": {
+   // These are on for this directory for .jsm and content/js files.
+    "blockBindings": true,
+    "arrowFunctions": true,
+    "destructuring": true,
+    "generators": true,
+    "spread": true,
+    "restParams": true,
+    "objectLiteralShorthandMethods": true
+  },
+  "rules": {
+    "generator-star-spacing": [2, "after"]
+  }
+}
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -217,17 +217,17 @@ loop.contacts = (function(_, mozL10n) {
               onClick: this.onItemClick, "data-action": blockAction}, 
             React.createElement("i", {className: "icon icon-" + blockAction}), 
             mozL10n.get(blockLabel)
           ), 
           React.createElement("li", {className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, "data-action": "remove"}, 
             React.createElement("i", {className: "icon icon-remove"}), 
-            mozL10n.get("remove_contact_menu_button")
+            mozL10n.get("remove_contact_menu_button2")
           )
         )
       );
     }
   });
 
   const ContactDetail = React.createClass({displayName: "ContactDetail",
     getInitialState: function() {
@@ -582,17 +582,17 @@ loop.contacts = (function(_, mozL10n) {
       }
 
       return (
         React.createElement("div", null, 
           React.createElement("div", {className: "content-area"}, 
             React.createElement(ButtonGroup, null, 
               React.createElement(Button, {caption: this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
-                               : mozL10n.get("import_contacts_button"), 
+                               : mozL10n.get("import_contacts_button2"), 
                       disabled: this.state.importBusy, 
                       onClick: this.handleImportButtonClick}, 
                 React.createElement("div", {className: cx({"contact-import-spinner": true,
                                     spinner: true,
                                     busy: this.state.importBusy})})
               ), 
               React.createElement(Button, {caption: mozL10n.get("new_contact_button"), 
                       onClick: this.handleAddContactButtonClick})
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -217,17 +217,17 @@ loop.contacts = (function(_, mozL10n) {
               onClick={this.onItemClick} data-action={blockAction}>
             <i className={"icon icon-" + blockAction} />
             {mozL10n.get(blockLabel)}
           </li>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="remove">
             <i className="icon icon-remove" />
-            {mozL10n.get("remove_contact_menu_button")}
+            {mozL10n.get("remove_contact_menu_button2")}
           </li>
         </ul>
       );
     }
   });
 
   const ContactDetail = React.createClass({
     getInitialState: function() {
@@ -582,17 +582,17 @@ loop.contacts = (function(_, mozL10n) {
       }
 
       return (
         <div>
           <div className="content-area">
             <ButtonGroup>
               <Button caption={this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
-                               : mozL10n.get("import_contacts_button")}
+                               : mozL10n.get("import_contacts_button2")}
                       disabled={this.state.importBusy}
                       onClick={this.handleImportButtonClick}>
                 <div className={cx({"contact-import-spinner": true,
                                     spinner: true,
                                     busy: this.state.importBusy})} />
               </Button>
               <Button caption={mozL10n.get("new_contact_button")}
                       onClick={this.handleAddContactButtonClick} />
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -6,20 +6,21 @@
 
 /* jshint newcap:false */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
+  var ROOM_STATES = loop.store.ROOM_STATES;
+  var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
-  var ROOM_STATES = loop.store.ROOM_STATES;
-  var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
+  var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   /**
    * ActiveRoomStore mixin.
    * @type {Object}
    */
   var ActiveRoomStoreMixin = {
     mixins: [Backbone.Events],
@@ -307,23 +308,35 @@ loop.roomViews = (function(mozL10n) {
     render: function() {
       if (!this.state.show)
         return null;
 
       var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
       var thumbnail = URL && URL.thumbnail || "";
       var URLDescription = URL && URL.description || "";
       var location = URL && URL.location || "";
+      var locationData = null;
+      if (location) {
+        locationData = sharedUtils.formatURL(location);
+      }
+
+      if (!locationData) {
+        return null;
+      }
+
       return (
         React.createElement("div", {className: "room-context"}, 
           React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}), 
           React.createElement("div", {className: "room-context-content"}, 
             React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")), 
             React.createElement("div", {className: "room-context-description"}, URLDescription), 
-            React.createElement("a", {className: "room-context-url", href: location, target: "_blank"}, location), 
+            React.createElement("a", {className: "room-context-url", 
+               href: location, 
+               target: "_blank", 
+               title: locationData.location}, locationData.hostname), 
             this.props.roomData.roomDescription ?
               React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
               null, 
             React.createElement("button", {className: "room-context-btn-close", 
                     onClick: this.handleCloseClick})
           )
         )
       );
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -6,20 +6,21 @@
 
 /* jshint newcap:false */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
+  var ROOM_STATES = loop.store.ROOM_STATES;
+  var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
-  var ROOM_STATES = loop.store.ROOM_STATES;
-  var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
+  var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   /**
    * ActiveRoomStore mixin.
    * @type {Object}
    */
   var ActiveRoomStoreMixin = {
     mixins: [Backbone.Events],
@@ -307,23 +308,35 @@ loop.roomViews = (function(mozL10n) {
     render: function() {
       if (!this.state.show)
         return null;
 
       var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
       var thumbnail = URL && URL.thumbnail || "";
       var URLDescription = URL && URL.description || "";
       var location = URL && URL.location || "";
+      var locationData = null;
+      if (location) {
+        locationData = sharedUtils.formatURL(location);
+      }
+
+      if (!locationData) {
+        return null;
+      }
+
       return (
         <div className="room-context">
           <img className="room-context-thumbnail" src={thumbnail}/>
           <div className="room-context-content">
             <div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
             <div className="room-context-description">{URLDescription}</div>
-            <a className="room-context-url" href={location} target="_blank">{location}</a>
+            <a className="room-context-url"
+               href={location}
+               target="_blank"
+               title={locationData.location}>{locationData.hostname}</a>
             {this.props.roomData.roomDescription ?
               <div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
               null}
             <button className="room-context-btn-close"
                     onClick={this.handleCloseClick}/>
           </div>
         </div>
       );
deleted file mode 100644
--- a/browser/components/loop/content/shared/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "ecmaFeatures": {
-    // Turn off top-level items needed for the jsm files, but not wanted
-    // for shared code as we can't support them.
-    "blockBindings": false,
-    "arrowFunctions": false,
-    "destructuring": false,
-    "forOf": true,
-    "generators": false,
-    "spread": false,
-    "restParams": false,
-    "objectLiteralShorthandMethods": false
-  }
-}
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -784,17 +784,17 @@ loop.OTSdkDriver = (function() {
       if (callLengthSeconds >= 10 && callLengthSeconds <= 30) {
         bucket = buckets.BETWEEN_10S_AND_30S;
       } else if (callLengthSeconds > 30 && callLengthSeconds <= 300) {
         bucket = buckets.BETWEEN_30S_AND_5M;
       } else if (callLengthSeconds > 300) {
         bucket = buckets.MORE_THAN_5M;
       }
 
-      this.mozLoop.telemetryAddValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH", bucket);
+      this.mozLoop.telemetryAddValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1", bucket);
       this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_ALREADY_NOTED);
 
       this._connectionLengthNotedCalls++;
       if (this._debugTwoWayMediaTelemetry) {
         console.log('Loop Telemetry: noted two-way media connection ' +
           'in bucket: ', bucket);
       }
     },
@@ -852,15 +852,15 @@ loop.OTSdkDriver = (function() {
 
       var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
         (enabled ? "ENABLED" : "DISABLED")];
       if (!bucket) {
         console.error("No sharing state bucket found for '" + type + "'");
         return;
       }
 
-      this.mozLoop.telemetryAddValue("LOOP_SHARING_STATE_CHANGE", bucket);
+      this.mozLoop.telemetryAddValue("LOOP_SHARING_STATE_CHANGE_1", bucket);
     }
   };
 
   return OTSdkDriver;
 
 })();
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -267,16 +267,43 @@ var inChrome = typeof Components != "und
   function locationData() {
     return {
       hash: window.location.hash,
       pathname: window.location.pathname
     };
   }
 
   /**
+   * Formats a url for display purposes. This includes converting the
+   * domain to punycode, and then decoding the url.
+   *
+   * @param {String} url The url to format.
+   * @return {Object}    An object containing the hostname and full location.
+   */
+  function formatURL(url) {
+    // We're using new URL to pass this through the browser's ACE/punycode
+    // processing system. If the browser considers a url to need to be
+    // punycode encoded for it to be displayed, then new URL will do that for
+    // us. This saves us needing our own punycode library.
+    var urlObject;
+    try {
+      urlObject = new URL(url);
+    } catch (ex) {
+      console.error("Error occurred whilst parsing URL:", ex);
+      return null;
+    }
+
+    // Finally, ensure we look good.
+    return {
+      hostname: urlObject.hostname,
+      location: decodeURI(urlObject.href)
+    };
+  }
+
+  /**
    * Generates and opens a mailto: url with call URL information prefilled.
    * Note: This only works for Desktop.
    *
    * @param  {String} callUrl   The call URL.
    * @param  {String} recipient The recipient email address (optional).
    */
   function composeCallUrlEmail(callUrl, recipient) {
     if (typeof navigator.mozLoop === "undefined") {
@@ -531,16 +558,17 @@ var inChrome = typeof Components != "und
     FAILURE_DETAILS: FAILURE_DETAILS,
     REST_ERRNOS: REST_ERRNOS,
     WEBSOCKET_REASONS: WEBSOCKET_REASONS,
     STREAM_PROPERTIES: STREAM_PROPERTIES,
     SCREEN_SHARE_STATES: SCREEN_SHARE_STATES,
     ROOM_INFO_FAILURES: ROOM_INFO_FAILURES,
     composeCallUrlEmail: composeCallUrlEmail,
     formatDate: formatDate,
+    formatURL: formatURL,
     getBoolPreference: getBoolPreference,
     getOS: getOS,
     getOSVersion: getOSVersion,
     isChrome: isChrome,
     isFirefox: isFirefox,
     isFirefoxOS: isFirefoxOS,
     isOpera: isOpera,
     getUnsupportedPlatform: getUnsupportedPlatform,
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/modules/.eslintrc
@@ -0,0 +1,17 @@
+{
+  "ecmaFeatures": {
+    "arrowFunctions": true,
+    "blockBindings": true,
+    "destructuring": true,
+    "generators": true,
+    "restParams": true,
+    "spread": true,
+    "objectLiteralShorthandMethods": true,
+  },
+  "rules": {
+    "generator-star-spacing": [2, "after"],
+    // We should fix the errors and enable this (set to 2)
+    "no-var": 0,
+    "strict": [2, "global"]
+  }
+}
rename from browser/components/loop/CardDavImporter.jsm
rename to browser/components/loop/modules/CardDavImporter.jsm
rename from browser/components/loop/GoogleImporter.jsm
rename to browser/components/loop/modules/GoogleImporter.jsm
rename from browser/components/loop/LoopCalls.jsm
rename to browser/components/loop/modules/LoopCalls.jsm
rename from browser/components/loop/LoopContacts.jsm
rename to browser/components/loop/modules/LoopContacts.jsm
rename from browser/components/loop/LoopRooms.jsm
rename to browser/components/loop/modules/LoopRooms.jsm
rename from browser/components/loop/LoopStorage.jsm
rename to browser/components/loop/modules/LoopStorage.jsm
rename from browser/components/loop/MozLoopAPI.jsm
rename to browser/components/loop/modules/MozLoopAPI.jsm
rename from browser/components/loop/MozLoopPushHandler.jsm
rename to browser/components/loop/modules/MozLoopPushHandler.jsm
rename from browser/components/loop/MozLoopService.jsm
rename to browser/components/loop/modules/MozLoopService.jsm
rename from browser/components/loop/MozLoopWorker.js
rename to browser/components/loop/modules/MozLoopWorker.js
--- a/browser/components/loop/moz.build
+++ b/browser/components/loop/moz.build
@@ -8,24 +8,24 @@ JAR_MANIFESTS += ['jar.mn']
 
 XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
 
 BROWSER_CHROME_MANIFESTS += [
     'test/mochitest/browser.ini',
 ]
 
 EXTRA_JS_MODULES.loop += [
-    'CardDavImporter.jsm',
     'content/shared/js/crypto.js',
     'content/shared/js/utils.js',
-    'GoogleImporter.jsm',
-    'LoopCalls.jsm',
-    'LoopContacts.jsm',
-    'LoopRooms.jsm',
-    'LoopStorage.jsm',
-    'MozLoopAPI.jsm',
-    'MozLoopPushHandler.jsm',
-    'MozLoopService.jsm',
-    'MozLoopWorker.js',
+    'modules/CardDavImporter.jsm',
+    'modules/GoogleImporter.jsm',
+    'modules/LoopCalls.jsm',
+    'modules/LoopContacts.jsm',
+    'modules/LoopRooms.jsm',
+    'modules/LoopStorage.jsm',
+    'modules/MozLoopAPI.jsm',
+    'modules/MozLoopPushHandler.jsm',
+    'modules/MozLoopService.jsm',
+    'modules/MozLoopWorker.js',
 ]
 
 with Files('**'):
     BUG_COMPONENT = ('Loop', 'Client')
deleted file mode 100644
--- a/browser/components/loop/standalone/.eslintrc
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "ecmaFeatures": {
-    // Turn off top-level items needed for the jsm files, but not wanted
-    // for shared code as we can't support them.
-    "blockBindings": false,
-    "arrowFunctions": false,
-    "destructuring": false,
-    "forOf": true,
-    "generators": false,
-    "spread": false,
-    "restParams": false,
-    "objectLiteralShorthandMethods": false
-  }
-}
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -11,16 +11,17 @@ var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
+  var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({displayName: "StandaloneRoomInfoArea",
     propTypes: {
       isFirefox: React.PropTypes.bool.isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
@@ -243,33 +244,37 @@ loop.standaloneRoomViews = (function(moz
     },
 
     render: function() {
       if (!this.props.roomContextUrl ||
           !this.props.roomContextUrl.location) {
         return null;
       }
 
-      var location = this.props.roomContextUrl.location;
+      var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
+      if (!locationInfo) {
+        return null;
+      }
 
       var cx = React.addons.classSet;
 
       var classes = cx({
         "standalone-context-url": true,
         "screen-share-active": this.props.receivingScreenShare
       });
 
       return (
         React.createElement("div", {className: classes}, 
             React.createElement("img", {src: this.props.roomContextUrl.thumbnail}), 
           React.createElement("div", {className: "standalone-context-url-description-wrapper"}, 
             this.props.roomContextUrl.description, 
-            React.createElement("br", null), React.createElement("a", {href: location, 
+            React.createElement("br", null), React.createElement("a", {href: locationInfo.location, 
                      onClick: this.recordClick, 
-                     target: "_blank"}, location)
+                     target: "_blank", 
+                     title: locationInfo.location}, locationInfo.hostname)
           )
         )
       );
     }
   });
 
   var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
     propTypes: {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -11,16 +11,17 @@ var loop = loop || {};
 loop.standaloneRoomViews = (function(mozL10n) {
   "use strict";
 
   var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
   var ROOM_STATES = loop.store.ROOM_STATES;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
+  var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   var StandaloneRoomInfoArea = React.createClass({
     propTypes: {
       isFirefox: React.PropTypes.bool.isRequired,
       activeRoomStore: React.PropTypes.oneOfType([
         React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
         React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
@@ -243,33 +244,37 @@ loop.standaloneRoomViews = (function(moz
     },
 
     render: function() {
       if (!this.props.roomContextUrl ||
           !this.props.roomContextUrl.location) {
         return null;
       }
 
-      var location = this.props.roomContextUrl.location;
+      var locationInfo = sharedUtils.formatURL(this.props.roomContextUrl.location);
+      if (!locationInfo) {
+        return null;
+      }
 
       var cx = React.addons.classSet;
 
       var classes = cx({
         "standalone-context-url": true,
         "screen-share-active": this.props.receivingScreenShare
       });
 
       return (
         <div className={classes}>
             <img src={this.props.roomContextUrl.thumbnail} />
           <div className="standalone-context-url-description-wrapper">
             {this.props.roomContextUrl.description}
-            <br /><a href={location}
+            <br /><a href={locationInfo.location}
                      onClick={this.recordClick}
-                     target="_blank">{location}</a>
+                     target="_blank"
+                     title={locationInfo.location}>{locationInfo.hostname}</a>
           </div>
         </div>
       );
     }
   });
 
   var StandaloneRoomContextView = React.createClass({
     propTypes: {
--- a/browser/components/loop/standalone/package.json
+++ b/browser/components/loop/standalone/package.json
@@ -7,18 +7,18 @@
     "url": "git@github.com:mozilla/loop-client.git"
   },
   "engines": {
     "node": "0.10.x",
     "npm": "1.3.x"
   },
   "dependencies": {},
   "devDependencies": {
-    "eslint": "0.18.x",
-    "eslint-plugin-react": "2.0.x",
+    "eslint": "0.20.x",
+    "eslint-plugin-react": "2.2.x",
     "express": "3.x"
   },
   "scripts": {
     "test": "make test",
     "start": "make runserver"
   },
   "license": "MPL-2.0"
 }
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/.eslintrc
@@ -0,0 +1,7 @@
+{
+  "rules": {
+    // This is useful for some of the tests, e.g.
+    // expect(new Foo()).to.Throw(/error/)
+    "no-new": 0
+  }
+}
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -237,16 +237,33 @@ describe("loop.roomViews", function () {
           showContext: true,
           roomData: {
             roomContextUrls: [fakeContextURL]
           }
         });
 
         expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
       });
+
+      it("should format the context url for display", function() {
+        sandbox.stub(sharedUtils, "formatURL").returns({
+          location: "location",
+          hostname: "hostname"
+        });
+
+        view = mountTestComponent({
+          showContext: true,
+          roomData: {
+            roomContextUrls: [fakeContextURL]
+          }
+        });
+
+        expect(view.getDOMNode().querySelector(".room-context-url").textContent)
+          .eql("hostname");
+      });
     });
   });
 
   describe("DesktopRoomConversationView", function() {
     var view;
 
     beforeEach(function() {
       loop.store.StoreMixin.register({
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/mochitest/.eslintrc
@@ -0,0 +1,17 @@
+{
+  "ecmaFeatures": {
+    "arrowFunctions": true,
+    "blockBindings": true,
+    "destructuring": true,
+    "generators": true,
+    "restParams": true,
+    "spread": true,
+    "objectLiteralShorthandMethods": true,
+  },
+  "rules": {
+    "generator-star-spacing": [2, "after"],
+    // We should fix the errors and enable this (set to 2)
+    "no-var": 0,
+    "strict": [2, "global"]
+  }
+}
--- a/browser/components/loop/test/mochitest/browser_CardDavImporter.js
+++ b/browser/components/loop/test/mochitest/browser_CardDavImporter.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const {CardDavImporter} = Cu.import("resource:///modules/loop/CardDavImporter.jsm", {});
 
 const kAuth = {
   "method": "basic",
   "user": "username",
   "password": "p455w0rd"
 }
 
--- a/browser/components/loop/test/mochitest/browser_GoogleImporter.js
+++ b/browser/components/loop/test/mochitest/browser_GoogleImporter.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const {GoogleImporter} = Cu.import("resource:///modules/loop/GoogleImporter.jsm", {});
 
 let importer = new GoogleImporter();
 
 function promiseImport() {
   return new Promise(function(resolve, reject) {
     importer.startImport({}, function(err, stats) {
       if (err) {
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ b/browser/components/loop/test/mochitest/browser_LoopContacts.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const {LoopContacts} = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
 const {LoopStorage} = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 const kContacts = [{
@@ -216,18 +218,23 @@ add_task(function* () {
 
   info("Get a couple of contacts.");
   yield new Promise((resolve, reject) => {
     let toRetrieve = [contacts[0], contacts[2], contacts[3]];
     LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
       Assert.ok(!err, "There shouldn't be an error");
       Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
                    "size as the list of items to retrieve");
+
+      function resultFilter(c) {
+        return c._guid == this._guid;
+      }
+
       for (let contact of toRetrieve) {
-        let found = result.filter(c => c._guid == contact._guid);
+        let found = result.filter(resultFilter.bind(contact));
         Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
         compareContacts(found[0], contact);
       }
       resolve();
     });
   });
 
   info("Get all contacts.");
--- a/browser/components/loop/test/mochitest/browser_mozLoop_appVersionInfo.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_appVersionInfo.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * This is an integration test from navigator.mozLoop through to the end
  * effects - rather than just testing MozLoopAPI alone.
  */
 
+"use strict";
+
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_appVersionInfo() {
   Assert.ok(gMozLoopAPI, "mozLoop should exist");
 
   let appVersionInfo = gMozLoopAPI.appVersionInfo;
--- a/browser/components/loop/test/mochitest/browser_mozLoop_doNotDisturb.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_doNotDisturb.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * This is an integration test from navigator.mozLoop through to the end
  * effects - rather than just testing MozLoopAPI alone.
  */
 
+"use strict";
+
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_doNotDisturb() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("loop.do_not_disturb");
   });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
@@ -1,21 +1,23 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * This is an integration test from navigator.mozLoop through to the end
- * effects - rather than just testing MozLoopAPI alone.
- */
-
-Components.utils.import("resource://gre/modules/Promise.jsm", this);
-
-add_task(loadLoopPanel);
-
-add_task(function* test_mozLoop_pluralStrings() {
-  Assert.ok(gMozLoopAPI, "mozLoop should exist");
-
-  var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
-  Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
-               "This window will close in {{countdown}} seconds");
-  Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
-               "This window will close in {{countdown}} second");
-});
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This is an integration test from navigator.mozLoop through to the end
+ * effects - rather than just testing MozLoopAPI alone.
+ */
+
+"use strict";
+
+Components.utils.import("resource://gre/modules/Promise.jsm", this);
+
+add_task(loadLoopPanel);
+
+add_task(function* test_mozLoop_pluralStrings() {
+  Assert.ok(gMozLoopAPI, "mozLoop should exist");
+
+  var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
+  Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
+               "This window will close in {{countdown}} seconds");
+  Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
+               "This window will close in {{countdown}} second");
+});
--- a/browser/components/loop/test/mochitest/browser_mozLoop_prefs.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_prefs.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * This is an integration test from navigator.mozLoop through to the end
  * effects - rather than just testing MozLoopAPI alone.
  */
 
+"use strict";
+
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 add_task(function* test_mozLoop_charPref() {
   registerCleanupFunction(function () {
     Services.prefs.clearUserPref("loop.test");
   });
--- a/browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_socialShare.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * This is an integration test from navigator.mozLoop through to the end
  * effects - rather than just testing MozLoopAPI alone.
  */
 
+"use strict";
+
 Cu.import("resource://gre/modules/Promise.jsm");
 const {SocialService} = Cu.import("resource://gre/modules/SocialService.jsm", {});
 
 add_task(loadLoopPanel);
 
 const kShareWidgetId = "social-share-button";
 const kShareProvider = {
   name: "provider 1",
--- a/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_telemetry.js
@@ -1,14 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /*
  * This file contains tests for the mozLoop telemetry API.
  */
+
+"use strict";
+
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
 
 add_task(loadLoopPanel);
 
 /**
  * Enable local telemetry recording for the duration of the tests.
  */
 add_task(function* test_initialize() {
@@ -18,17 +21,17 @@ add_task(function* test_initialize() {
     Services.telemetry.canRecordExtended = oldCanRecord;
   });
 });
 
 /**
  * Tests that enumerated bucket histograms exist and can be updated.
  */
 add_task(function* test_mozLoop_telemetryAdd_buckets() {
-  let histogramId = "LOOP_TWO_WAY_MEDIA_CONN_LENGTH";
+  let histogramId = "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1";
   let histogram = Services.telemetry.getHistogramById(histogramId);
   let CONN_LENGTH = gMozLoopAPI.TWO_WAY_MEDIA_CONN_LENGTH;
 
   histogram.clear();
   for (let value of [CONN_LENGTH.SHORTER_THAN_10S,
                      CONN_LENGTH.BETWEEN_10S_AND_30S,
                      CONN_LENGTH.BETWEEN_10S_AND_30S,
                      CONN_LENGTH.BETWEEN_30S_AND_5M,
@@ -44,17 +47,17 @@ add_task(function* test_mozLoop_telemetr
   let snapshot = histogram.snapshot();
   is(snapshot.counts[CONN_LENGTH.SHORTER_THAN_10S], 1, "TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S");
   is(snapshot.counts[CONN_LENGTH.BETWEEN_10S_AND_30S], 2, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S");
   is(snapshot.counts[CONN_LENGTH.BETWEEN_30S_AND_5M], 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M");
   is(snapshot.counts[CONN_LENGTH.MORE_THAN_5M], 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M");
 });
 
 add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() {
-  let histogramId = "LOOP_SHARING_STATE_CHANGE";
+  let histogramId = "LOOP_SHARING_STATE_CHANGE_1";
   let histogram = Services.telemetry.getHistogramById(histogramId);
   const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE;
 
   histogram.clear();
   for (let value of [SHARING_STATES.WINDOW_ENABLED,
                      SHARING_STATES.WINDOW_DISABLED,
                      SHARING_STATES.WINDOW_DISABLED,
                      SHARING_STATES.BROWSER_ENABLED,
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const HAWK_TOKEN_LENGTH = 64;
 const {
   LOOP_SESSION_TYPE,
   MozLoopServiceInternal,
   MozLoopService,
 } = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
 const {LoopCalls} = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
 const {LoopRooms} = Cu.import("resource:///modules/loop/LoopRooms.jsm", {});
@@ -118,18 +120,18 @@ function loadLoopPanel(aOverrideOptions 
   registerCleanupFunction(function() {
     Services.io.offline = WAS_OFFLINE;
   });
 
   // Turn off animations to make tests quicker.
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.setAttribute("animate", "false");
 
-  // Now get the actual API.
-  yield promiseGetMozLoopAPI();
+  // Now get the actual API loaded into gMozLoopAPI.
+  return promiseGetMozLoopAPI();
 }
 
 function promiseOAuthParamsSetup(baseURL, params) {
   return new Promise((resolve, reject) => {
     let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
                 createInstance(Ci.nsIXMLHttpRequest);
     xhr.open("POST", baseURL + "/setup_params", true);
     xhr.setRequestHeader("X-Params", JSON.stringify(params));
@@ -314,17 +316,17 @@ const mockDb = {
       callback(new Error("No 'id' field present"));
       return;
     }
     details._guid = this._next_guid++;
     this._store[details._guid] = details;
     callback(null, details);
   },
   remove: function(guid, callback) {
-    if (!guid in this._store) {
+    if (!(guid in this._store)) {
       callback(new Error("Could not find _guid '" + guid + "' in database"));
       return;
     }
     delete this._store[guid];
     callback(null);
   },
   getAll: function(callback) {
     callback(null, this._store);
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -468,52 +468,52 @@ describe("loop.OTSdkDriver", function ()
 
     it("should call mozLoop.noteConnectionLength with SHORTER_THAN_10S for calls less than 10s", function() {
       var endTimeMS = 9000;
 
       driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWith(mozLoop.telemetryAddValue,
-        "LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
+        "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1",
         mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S);
     });
 
     it("should call mozLoop.noteConnectionLength with BETWEEN_10S_AND_30S for 15s calls",
       function() {
         var endTimeMS = 15000;
 
         driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
         sinon.assert.calledOnce(mozLoop.telemetryAddValue);
         sinon.assert.calledWith(mozLoop.telemetryAddValue,
-          "LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
+          "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1",
           mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S);
       });
 
     it("should call mozLoop.noteConnectionLength with BETWEEN_30S_AND_5M for 60s calls",
       function() {
         var endTimeMS = 60 * 1000;
 
         driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
         sinon.assert.calledOnce(mozLoop.telemetryAddValue);
         sinon.assert.calledWith(mozLoop.telemetryAddValue,
-          "LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
+          "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1",
           mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M);
       });
 
     it("should call mozLoop.noteConnectionLength with MORE_THAN_5M for 10m calls", function() {
       var endTimeMS = 10 * 60 * 1000;
 
       driver._noteConnectionLengthIfNeeded(startTimeMS, endTimeMS);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWith(mozLoop.telemetryAddValue,
-        "LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
+        "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1",
         mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M);
     });
 
     it("should not call mozLoop.noteConnectionLength if" +
        " driver._sendTwoWayMediaTelemetry is false",
       function() {
         var endTimeMS = 10 * 60 * 1000;
         driver._sendTwoWayMediaTelemetry = false;
@@ -525,44 +525,44 @@ describe("loop.OTSdkDriver", function ()
   });
 
   describe("#_noteSharingState", function() {
     it("should record enabled sharing states for window", function() {
       driver._noteSharingState("window", true);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWithExactly(mozLoop.telemetryAddValue,
-        "LOOP_SHARING_STATE_CHANGE",
+        "LOOP_SHARING_STATE_CHANGE_1",
         mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED);
     });
 
     it("should record enabled sharing states for browser", function() {
       driver._noteSharingState("browser", true);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWithExactly(mozLoop.telemetryAddValue,
-        "LOOP_SHARING_STATE_CHANGE",
+        "LOOP_SHARING_STATE_CHANGE_1",
         mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED);
     });
 
     it("should record disabled sharing states for window", function() {
       driver._noteSharingState("window", false);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWithExactly(mozLoop.telemetryAddValue,
-        "LOOP_SHARING_STATE_CHANGE",
+        "LOOP_SHARING_STATE_CHANGE_1",
         mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED);
     });
 
     it("should record disabled sharing states for browser", function() {
       driver._noteSharingState("browser", false);
 
       sinon.assert.calledOnce(mozLoop.telemetryAddValue);
       sinon.assert.calledWithExactly(mozLoop.telemetryAddValue,
-        "LOOP_SHARING_STATE_CHANGE",
+        "LOOP_SHARING_STATE_CHANGE_1",
         mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED);
     });
   });
 
   describe("#forceDisconnectAll", function() {
     it("should not disconnect anything when not connected", function() {
       driver.session = session;
       driver.forceDisconnectAll(function() {});
--- a/browser/components/loop/test/shared/utils_test.js
+++ b/browser/components/loop/test/shared/utils_test.js
@@ -140,16 +140,41 @@ describe("loop.shared.utils", function()
       it("should return the localStorage value", function() {
         localStorage.setItem("test.true", true);
 
         expect(sharedUtils.getBoolPreference("test.true")).eql(true);
       });
     });
   });
 
+  describe("#formatURL", function() {
+    it("should decode encoded URIs", function() {
+      expect(sharedUtils.formatURL("http://invalid.com/?a=Foo%20Bar"))
+        .eql({
+          location: "http://invalid.com/?a=Foo Bar",
+          hostname: "invalid.com"
+        });
+    });
+
+    it("should change some idn urls to ascii encoded", function() {
+      // Note, this is based on the browser's list of what does/doesn't get
+      // altered for punycode, so if the list changes this could change in the
+      // future.
+      expect(sharedUtils.formatURL("http://\u0261oogle.com/"))
+        .eql({
+          location: "http://xn--oogle-qmc.com/",
+          hostname: "xn--oogle-qmc.com"
+        });
+    });
+
+    it("should return null if it the url is not valid", function() {
+      expect(sharedUtils.formatURL("hinvalid//url")).eql(null);
+    });
+  });
+
   describe("#composeCallUrlEmail", function() {
     var composeEmail;
 
     beforeEach(function() {
       // fake mozL10n
       sandbox.stub(navigator.mozL10n, "get", function(id) {
         switch(id) {
           case "share_email_subject5": return "subject";
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -8,16 +8,17 @@ var expect = chai.expect;
 
 describe("loop.standaloneRoomViews", function() {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
   var sharedActions = loop.shared.actions;
+  var sharedUtils = loop.shared.utils;
 
   var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
     dispatch = sandbox.stub(dispatcher, "dispatch");
     activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
@@ -91,16 +92,36 @@ describe("loop.standaloneRoomViews", fun
           location: "http://invalid.com",
           thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
         }]
       });
 
       expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
     });
 
+    it("should format the url for display", function() {
+      sandbox.stub(sharedUtils, "formatURL").returns({
+          location: "location",
+          hostname: "hostname"
+        });
+
+      var view = mountTestComponent({
+        roomName: "Mike's room",
+        roomContextUrls: [{
+          description: "Mark's super page",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
+      });
+
+      expect(view.getDOMNode()
+        .querySelector(".standalone-context-url-description-wrapper > a").textContent)
+        .eql("hostname");
+    });
+
     it("should not display context information if no urls are supplied", function() {
       var view = mountTestComponent({
         roomName: "Mike's room"
       });
 
       expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
     });
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/xpcshell/.eslintrc
@@ -0,0 +1,17 @@
+{
+  "ecmaFeatures": {
+    "arrowFunctions": true,
+    "blockBindings": true,
+    "destructuring": true,
+    "generators": true,
+    "restParams": true,
+    "spread": true,
+    "objectLiteralShorthandMethods": true,
+  },
+  "rules": {
+    "generator-star-spacing": [2, "after"],
+    // We should fix the errors and enable this (set to 2)
+    "no-var": 0,
+    "strict": [2, "global"]
+  }
+}
--- a/browser/components/loop/test/xpcshell/head.js
+++ b/browser/components/loop/test/xpcshell/head.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Http.jsm");
 Cu.import("resource://testing-common/httpd.js");
 Cu.import("resource:///modules/loop/MozLoopService.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
--- a/browser/components/loop/test/xpcshell/test_looppush_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_looppush_initialize.js
@@ -1,222 +1,223 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
-{
-  let dummyCallback = () => {};
-  let mockWebSocket = new MockWebSocketChannel();
-  let pushServerRequestCount = 0;
+
+"use strict";
+
+let dummyCallback = () => {};
+let mockWebSocket = new MockWebSocketChannel();
+let pushServerRequestCount = 0;
 
-  add_test(function test_initalize_offline() {
-    Services.io.offline = true;
-    do_check_false(MozLoopPushHandler.initialize());
-    Services.io.offline = false;
-    run_next_test();
-  });
+add_test(function test_initalize_offline() {
+  Services.io.offline = true;
+  do_check_false(MozLoopPushHandler.initialize());
+  Services.io.offline = false;
+  run_next_test();
+});
 
-  add_test(function test_initalize_missing_chanid() {
-    Assert.throws(() => MozLoopPushHandler.register(null, dummyCallback, dummyCallback));
-    run_next_test();
-  });
+add_test(function test_initalize_missing_chanid() {
+  Assert.throws(() => MozLoopPushHandler.register(null, dummyCallback, dummyCallback));
+  run_next_test();
+});
 
-  add_test(function test_initalize_missing_regcallback() {
-    Assert.throws(() => MozLoopPushHandler.register("chan-1", null, dummyCallback));
-    run_next_test();
-  });
+add_test(function test_initalize_missing_regcallback() {
+  Assert.throws(() => MozLoopPushHandler.register("chan-1", null, dummyCallback));
+  run_next_test();
+});
 
-  add_test(function test_initalize_missing_notifycallback() {
-    Assert.throws(() => MozLoopPushHandler.register("chan-1", dummyCallback, null));
-    run_next_test();
-  });
+add_test(function test_initalize_missing_notifycallback() {
+  Assert.throws(() => MozLoopPushHandler.register("chan-1", dummyCallback, null));
+  run_next_test();
+});
 
-  add_test(function test_initalize_websocket() {
-    do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
-    MozLoopPushHandler.register(
-      "chan-1",
-      function(err, url, id) {
-        Assert.equal(err, null, "err should be null to indicate success");
-        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
-        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
-        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
-                     "Should have the url from preferences");
-        Assert.equal(mockWebSocket.origin, kServerPushUrl,
-                     "Should have the origin url from preferences");
-        Assert.equal(mockWebSocket.protocol, "push-notification",
-                     "Should have the protocol set to push-notifications");
-        mockWebSocket.notify(15);
-      },
-      function(version, id) {
-        Assert.equal(version, 15, "Should have version number 15");
-        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
-        run_next_test();
-      });
-  });
+add_test(function test_initalize_websocket() {
+  do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
+  MozLoopPushHandler.register(
+    "chan-1",
+    function(err, url, id) {
+      Assert.equal(err, null, "err should be null to indicate success");
+      Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+      Assert.equal(id, "chan-1", "Should have channel id = chan-1");
+      Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
+                   "Should have the url from preferences");
+      Assert.equal(mockWebSocket.origin, kServerPushUrl,
+                   "Should have the origin url from preferences");
+      Assert.equal(mockWebSocket.protocol, "push-notification",
+                   "Should have the protocol set to push-notifications");
+      mockWebSocket.notify(15);
+    },
+    function(version, id) {
+      Assert.equal(version, 15, "Should have version number 15");
+      Assert.equal(id, "chan-1", "Should have channel id = chan-1");
+      run_next_test();
+    });
+});
 
-  add_test(function test_register_twice_same_channel() {
-    MozLoopPushHandler.register(
-      "chan-2",
-      function(err, url, id) {
-        Assert.equal(err, null, "Should return null for success");
-        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
-        Assert.equal(id, "chan-2", "Should have channel id = chan-2");
-        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
-                     "Should have the url from preferences");
-        Assert.equal(mockWebSocket.origin, kServerPushUrl,
-                     "Should have the origin url from preferences");
-        Assert.equal(mockWebSocket.protocol, "push-notification",
-                     "Should have the protocol set to push-notifications");
+add_test(function test_register_twice_same_channel() {
+  MozLoopPushHandler.register(
+    "chan-2",
+    function(err, url, id) {
+      Assert.equal(err, null, "Should return null for success");
+      Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+      Assert.equal(id, "chan-2", "Should have channel id = chan-2");
+      Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
+                   "Should have the url from preferences");
+      Assert.equal(mockWebSocket.origin, kServerPushUrl,
+                   "Should have the origin url from preferences");
+      Assert.equal(mockWebSocket.protocol, "push-notification",
+                   "Should have the protocol set to push-notifications");
 
-        // Register again for the same channel
-        MozLoopPushHandler.register(
-          "chan-2",
-          function(err, url, id) {
-            Assert.equal(err, null, "Should return null for success");
-            Assert.equal(id, "chan-2", "Should have channel id = chan-2");
-            run_next_test();
-          },
-          dummyCallback
-        );
-      },
-      dummyCallback
-    );
-  });
+      // Register again for the same channel
+      MozLoopPushHandler.register(
+        "chan-2",
+        function(err, url, id) {
+          Assert.equal(err, null, "Should return null for success");
+          Assert.equal(id, "chan-2", "Should have channel id = chan-2");
+          run_next_test();
+        },
+        dummyCallback
+      );
+    },
+    dummyCallback
+  );
+});
 
-  // Test that the PushHander will re-connect after the near-end disconnect.
-  // The uaID is cleared to force re-registration of all notification channels.
-  add_test(function test_reconnect_websocket() {
-    MozLoopPushHandler.uaID = undefined;
-    mockWebSocket.stop();
-    // Previously registered onRegistration callbacks will fire and be checked (see above).
-  });
-
-  // Test that the PushHander will re-connect after the far-end disconnect.
-  // The uaID is cleared to force re-regsitration of all notification channels.
-  add_test(function test_reopen_websocket() {
-    MozLoopPushHandler.uaID = undefined;
-    MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
-    mockWebSocket.serverClose();
-    // Previously registered onRegistration callbacks will fire and be checked (see above).
-  });
+// Test that the PushHander will re-connect after the near-end disconnect.
+// The uaID is cleared to force re-registration of all notification channels.
+add_test(function test_reconnect_websocket() {
+  MozLoopPushHandler.uaID = undefined;
+  mockWebSocket.stop();
+  // Previously registered onRegistration callbacks will fire and be checked (see above).
+});
 
-  // Force a re-registration cycle and have the PushServer return a 500.
-  // A retry should occur and the registration then complete.
-  add_test(function test_retry_registration() {
-    MozLoopPushHandler.uaID = undefined;
-    mockWebSocket.initRegStatus = 500;
-    mockWebSocket.stop();
-  });
+// Test that the PushHander will re-connect after the far-end disconnect.
+// The uaID is cleared to force re-regsitration of all notification channels.
+add_test(function test_reopen_websocket() {
+  MozLoopPushHandler.uaID = undefined;
+  MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
+  mockWebSocket.serverClose();
+  // Previously registered onRegistration callbacks will fire and be checked (see above).
+});
+
+// Force a re-registration cycle and have the PushServer return a 500.
+// A retry should occur and the registration then complete.
+add_test(function test_retry_registration() {
+  MozLoopPushHandler.uaID = undefined;
+  mockWebSocket.initRegStatus = 500;
+  mockWebSocket.stop();
+});
 
-  add_test(function test_reconnect_no_registration() {
-    let regCnt = 0;
-    MozLoopPushHandler.shutdown();
-    MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
-    MozLoopPushHandler.register(
-      "test-chan",
-      function(err, url, id) {
-        Assert.equal(++regCnt, 1, "onRegistered should only be called once");
-        Assert.equal(err, null, "err should be null to indicate success");
-        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
-        Assert.equal(id, "test-chan", "Should have channel id = test-chan");
-        mockWebSocket.stop();
-        setTimeout(run_next_test(), 0);
-      },
-      dummyCallback
-    );
-  });
+add_test(function test_reconnect_no_registration() {
+  let regCnt = 0;
+  MozLoopPushHandler.shutdown();
+  MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
+  MozLoopPushHandler.register(
+    "test-chan",
+    function(err, url, id) {
+      Assert.equal(++regCnt, 1, "onRegistered should only be called once");
+      Assert.equal(err, null, "err should be null to indicate success");
+      Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+      Assert.equal(id, "test-chan", "Should have channel id = test-chan");
+      mockWebSocket.stop();
+      setTimeout(run_next_test(), 0);
+    },
+    dummyCallback
+  );
+});
 
-  add_test(function test_ping_websocket() {
-    let pingReceived = false,
-        socketClosed = false;
-    mockWebSocket.defaultMsgHandler = (msg) => {
-      pingReceived = true;
-      // Do not send a ping response.
-    }
-    mockWebSocket.close = () => {
-      socketClosed = true;
-    }
+add_test(function test_ping_websocket() {
+  let pingReceived = false,
+      socketClosed = false;
+  mockWebSocket.defaultMsgHandler = (msg) => {
+    pingReceived = true;
+    // Do not send a ping response.
+  }
+  mockWebSocket.close = () => {
+    socketClosed = true;
+  }
 
-    MozLoopPushHandler.shutdown();
-    MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
-    MozLoopPushHandler.register(
-      "test-chan",
-      function(err, url) {
-        Assert.equal(err, null, "err should be null to indicate success");
-        waitForCondition(() => pingReceived).then(() => {
-          waitForCondition(() => socketClosed).then(() => {
-            run_next_test();
-          }, () => {
-            do_throw("should have closed the websocket");
-          });
+  MozLoopPushHandler.shutdown();
+  MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
+  MozLoopPushHandler.register(
+    "test-chan",
+    function(err, url) {
+      Assert.equal(err, null, "err should be null to indicate success");
+      waitForCondition(() => pingReceived).then(() => {
+        waitForCondition(() => socketClosed).then(() => {
+          run_next_test();
         }, () => {
-          do_throw("should have sent ping");
+          do_throw("should have closed the websocket");
         });
-      },
-      dummyCallback
-    );
-  });
+      }, () => {
+        do_throw("should have sent ping");
+      });
+    },
+    dummyCallback
+  );
+});
 
-  add_test(function test_retry_pushurl() {
-    MozLoopPushHandler.shutdown();
-    loopServer.registerPathHandler("/push-server-config", (request, response) => {
-      // The PushHandler should retry the request for the push-server-config for
-      // each of these cases without throwing an error.
-      let n = 0;
-      switch (++pushServerRequestCount) {
-      case ++n:
-        // Non-200 response
-        response.setStatusLine(null, 500, "Retry");
-        response.processAsync();
-        response.finish();
-        break;
-      case ++n:
-        // missing parameter
-        response.setStatusLine(null, 200, "OK");
-        response.write(JSON.stringify({pushServerURI: null}));
-        response.processAsync();
-        response.finish();
-        break;
-      case ++n:
-        // json parse error
-        response.setStatusLine(null, 200, "OK");
-        response.processAsync();
-        response.finish();
-        break;
-      case ++n:
-        response.setStatusLine(null, 200, "OK");
-        response.write(JSON.stringify({pushServerURI: kServerPushUrl}));
-        response.processAsync();
-        response.finish();
-
-        run_next_test();
-        break;
-      }
-    });
-
-    do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
-  });
-
-  function run_test() {
-    setupFakeLoopServer();
-
-    loopServer.registerPathHandler("/push-server-config", (request, response) => {
+add_test(function test_retry_pushurl() {
+  MozLoopPushHandler.shutdown();
+  loopServer.registerPathHandler("/push-server-config", (request, response) => {
+    // The PushHandler should retry the request for the push-server-config for
+    // each of these cases without throwing an error.
+    let n = 0;
+    switch (++pushServerRequestCount) {
+    case ++n:
+      // Non-200 response
+      response.setStatusLine(null, 500, "Retry");
+      response.processAsync();
+      response.finish();
+      break;
+    case ++n:
+      // missing parameter
+      response.setStatusLine(null, 200, "OK");
+      response.write(JSON.stringify({pushServerURI: null}));
+      response.processAsync();
+      response.finish();
+      break;
+    case ++n:
+      // json parse error
+      response.setStatusLine(null, 200, "OK");
+      response.processAsync();
+      response.finish();
+      break;
+    case ++n:
       response.setStatusLine(null, 200, "OK");
       response.write(JSON.stringify({pushServerURI: kServerPushUrl}));
       response.processAsync();
       response.finish();
-    });
+
+      run_next_test();
+      break;
+    }
+  });
 
-    Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
-    Services.prefs.setIntPref("loop.retry_delay.start", 10); // 10 ms
-    Services.prefs.setIntPref("loop.retry_delay.limit", 20); // 20 ms
-    Services.prefs.setIntPref("loop.ping.interval", 50); // 50 ms
-    Services.prefs.setIntPref("loop.ping.timeout", 20); // 20 ms
+  do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
+});
+
+function run_test() {
+  setupFakeLoopServer();
 
-    do_register_cleanup(function() {
-      Services.prefs.clearUserPref("services.push.serverULR");
-      Services.prefs.clearUserPref("loop.retry_delay.start");
-      Services.prefs.clearUserPref("loop.retry_delay.limit");
-      Services.prefs.clearUserPref("loop.ping.interval");
-      Services.prefs.clearUserPref("loop.ping.timeout");
-    });
+  loopServer.registerPathHandler("/push-server-config", (request, response) => {
+    response.setStatusLine(null, 200, "OK");
+    response.write(JSON.stringify({pushServerURI: kServerPushUrl}));
+    response.processAsync();
+    response.finish();
+  });
 
-    run_next_test();
-  };
-}
+  Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
+  Services.prefs.setIntPref("loop.retry_delay.start", 10); // 10 ms
+  Services.prefs.setIntPref("loop.retry_delay.limit", 20); // 20 ms
+  Services.prefs.setIntPref("loop.ping.interval", 50); // 50 ms
+  Services.prefs.setIntPref("loop.ping.timeout", 20); // 20 ms
+
+  do_register_cleanup(function() {
+    Services.prefs.clearUserPref("services.push.serverULR");
+    Services.prefs.clearUserPref("loop.retry_delay.start");
+    Services.prefs.clearUserPref("loop.retry_delay.limit");
+    Services.prefs.clearUserPref("loop.ping.interval");
+    Services.prefs.clearUserPref("loop.ping.timeout");
+  });
+
+  run_next_test();
+};
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -1,12 +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/. */
 
+"use strict";
+
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource:///modules/loop/LoopRooms.jsm");
 Cu.import("resource:///modules/Chat.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 
 let openChatOrig = Chat.open;
 
 const kContextEnabledPref = "loop.contextInConverations.enabled";
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -1,12 +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/. */
 
+"use strict";
+
 const { LoopCallsInternal } = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 
 let actionReceived = false;
 let openChatOrig = Chat.open;
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_directcall.js
@@ -1,12 +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/. */
 
+"use strict";
+
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 let openChatOrig = Chat.open;
 
 const contact = {
   name: [ "Mr Smith" ],
   email: [{
     type: "home",
--- a/browser/components/loop/test/xpcshell/test_loopservice_dnd.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_dnd.js
@@ -1,12 +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/. */
 
+"use strict";
+
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 let openChatOrig = Chat.open;
 
 add_test(function test_get_do_not_disturb() {
   Services.prefs.setBoolPref("loop.do_not_disturb", false);
 
   do_check_false(MozLoopService.doNotDisturb);
@@ -36,17 +38,17 @@ add_test(function test_do_not_disturb_di
   MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
     let opened = false;
     Chat.open = function() {
       opened = true;
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
-    waitForCondition(function() opened).then(() => {
+    waitForCondition(() => opened).then(() => {
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window");
     });
   });
 });
 
 add_test(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
--- a/browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_encryptionkey.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 /* global Services, Assert */
 
+"use strict";
+
 const kGuestKeyPref = "loop.key";
 const kFxAKeyPref = "loop.key.fxa";
 
 do_register_cleanup(function() {
   Services.prefs.clearUserPref(kGuestKeyPref);
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.fxAOAuthProfile = null;
 });
--- a/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_initialize.js
@@ -1,18 +1,20 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 let startTimerCalled = false;
 
 /**
  * Tests that registration doesn't happen when the expiry time is
  * not set.
  */
-add_task(function test_initialize_no_expiry() {
+add_task(function* test_initialize_no_expiry() {
   startTimerCalled = false;
 
   let initializedPromise = yield MozLoopService.initialize();
   Assert.equal(initializedPromise, "registration not needed",
                "Promise should be fulfilled");
   Assert.equal(startTimerCalled, false,
     "should not register when no expiry time is set");
 });
--- a/browser/components/loop/test/xpcshell/test_loopservice_locales.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_locales.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 function test_locale() {
   // Set the pref to something controlled.
   Services.prefs.setCharPref("general.useragent.locale", "ab-CD");
 
   Assert.equal(MozLoopService.locale, "ab-CD");
 
   Services.prefs.clearUserPref("general.useragent.locale");
 }
--- a/browser/components/loop/test/xpcshell/test_loopservice_loop_prefs.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_loop_prefs.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 /*global XPCOMUtils, Services, Assert */
 
+"use strict";
+
 var fakeCharPrefName = "color";
 var fakeBoolPrefName = "boolean";
 var fakePrefValue = "green";
 
 function test_getLoopPref()
 {
   Services.prefs.setCharPref("loop." + fakeCharPrefName, fakePrefValue);
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_notification.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_notification.js
@@ -1,12 +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/. */
 
+"use strict";
+
 XPCOMUtils.defineLazyModuleGetter(this, "Chat",
                                   "resource:///modules/Chat.jsm");
 
 let openChatOrig = Chat.open;
 
 add_test(function test_openChatWindow_on_notification() {
   Services.prefs.setCharPref("loop.seenToS", "unseen");
 
@@ -15,17 +17,17 @@ add_test(function test_openChatWindow_on
   MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA).then(() => {
     let opened = false;
     Chat.open = function() {
       opened = true;
     };
 
     mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
 
-    waitForCondition(function() opened).then(() => {
+    waitForCondition(() => opened).then(() => {
       do_check_true(opened, "should open a chat window");
 
       do_check_eq(Services.prefs.getCharPref("loop.seenToS"), "seen",
                   "should set the pref to 'seen'");
 
       run_next_test();
     }, () => {
       do_throw("should have opened a chat window");
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 Cu.import("resource://services-common/utils.js");
 
 /**
  * This file is to test general registration. Note that once successful
  * registration has taken place, we can no longer test the server side
  * parts as the service protects against this, hence, they need testing in
  * other test files.
  */
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration_retry.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration_retry.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/utils.js");
 
 /**
  * Tests that it's possible to retry registration after an error.
  */
 
 add_test(function test_retry_after_failed_push_reg() {
--- a/browser/components/loop/test/xpcshell/test_loopservice_restart.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_restart.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const FAKE_FXA_TOKEN_DATA = JSON.stringify({
   "token_type": "bearer",
   "access_token": "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",
   "scope": "profile"
 });
 const FAKE_FXA_PROFILE = JSON.stringify({
   "email": "test@example.com",
   "uid": "999999994d9f4b08a2cbfc0999999999",
@@ -15,47 +17,47 @@ const LOOP_FXA_TOKEN_PREF = "loop.fxa_oa
 const LOOP_FXA_PROFILE_PREF = "loop.fxa_oauth.profile";
 const LOOP_CREATED_ROOM_PREF = "loop.createdRoom";
 const LOOP_INITIAL_DELAY_PREF = "loop.initialDelay";
 
 /**
  * This file is to test restart+reauth.
  */
 
-add_task(function test_initialize_with_no_guest_rooms_and_no_auth_token() {
+add_task(function* test_initialize_with_no_guest_rooms_and_no_auth_token() {
   // Set time to be 2 seconds in the past.
   var nowSeconds = Date.now() / 1000;
   Services.prefs.setBoolPref(LOOP_CREATED_ROOM_PREF, false);
   Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
 
   yield MozLoopService.initialize().then((msg) => {
     Assert.equal(msg, "registration not needed", "Initialize should not register when the " +
                                                  "URLs are expired and there are no auth tokens");
   }, (error) => {
     Assert.ok(false, error, "should have resolved the promise that initialize returned");
   });
 });
 
-add_task(function test_initialize_with_created_room_and_no_auth_token() {
+add_task(function* test_initialize_with_created_room_and_no_auth_token() {
   Services.prefs.setBoolPref(LOOP_CREATED_ROOM_PREF, true);
   Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
 
   loopServer.registerPathHandler("/registration", (request, response) => {
     response.setStatusLine(null, 200, "OK");
   });
 
   yield MozLoopService.initialize().then((msg) => {
     Assert.equal(msg, "initialized without FxA status", "Initialize should register as a " +
                                                      "guest when no auth tokens but expired URLs");
   }, (error) => {
     Assert.ok(false, error, "should have resolved the promise that initialize returned");
   });
 });
 
-add_task(function test_initialize_with_invalid_fxa_token() {
+add_task(function* test_initialize_with_invalid_fxa_token() {
   Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
   Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
 
   // Only need to implement the FxA registration because the previous
   // test registered as a guest.
   loopServer.registerPathHandler("/registration", (request, response) => {
     response.setStatusLine(null, 401, "Unauthorized");
     response.write(JSON.stringify({
@@ -78,17 +80,17 @@ add_task(function test_initialize_with_i
     Assert.ok(MozLoopServiceInternal.errors.has("login"),
               "Initialization error should have been reported to UI");
     Assert.ok(MozLoopServiceInternal.errors.has("login"));
     Assert.ok(MozLoopServiceInternal.errors.get("login").friendlyDetailsButtonCallback,
               "Check that there is a retry callback");
   });
 });
 
-add_task(function test_initialize_with_fxa_token() {
+add_task(function* test_initialize_with_fxa_token() {
   Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
   Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
 
   MozLoopService.errors.clear();
 
   loopServer.registerPathHandler("/registration", (request, response) => {
     response.setStatusLine(null, 200, "OK");
   });
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 const LOOP_HAWK_PREF = "loop.hawk-session-token";
 const fakeSessionToken1 = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1751";
 const fakeSessionToken2 = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750";
 
 add_test(function test_registration_invalid_token() {
   Services.prefs.setCharPref(LOOP_HAWK_PREF, fakeSessionToken1);
   var authorizationAttempts = 0;
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_save.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_save.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 /**
  * Test that things behave reasonably when a reasonable Hawk-Session-Token
  * header is returned with the registration response.
  */
 add_test(function test_registration_returns_hawk_session_token() {
   var fakeSessionToken = "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750";
   Services.prefs.clearUserPref("loop.hawk-session-token");
 
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_send.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_send.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 add_test(function test_registration_uses_hawk_session_token() {
   Services.prefs.setCharPref("loop.hawk-session-token",
     "1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1750");
 
   loopServer.registerPathHandler("/registration", (request, response) => {
     // Check that we have an Authorization header with the correct bits. The
     // translation of values are tested in different modules, for the components
     // that we use.
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_validation.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_validation.js
@@ -1,11 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+"use strict";
+
 // XXX should report error if Hawk-Session-Token is lexically invalid
 // (not a string of 64 hex digits) to help resist other possible injection
 // attacks.  For now, however, we're just checking if it's the right length.
 add_test(function test_registration_handles_bogus_hawk_token() {
 
   var wrongSizeToken = "jdkasjkasjdlaksj";
   Services.prefs.clearUserPref("loop.hawk-session-token");
 
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -999,22 +999,24 @@ let gEditItemOverlay = {
     }
   },
 
   // nsINavBookmarkObserver
   onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aValue,
                 aLastModified, aItemType) {
     if (aProperty == "tags" && this._paneInfo.visibleRows.has("tagsRow"))
       this._onTagsChange(aItemId);
-    else if (this._paneInfo.isItem && aProperty == "title")
-      this._onItemTitleChange(aItemId, aValue);
-    else (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId)
+    else if (!this._paneInfo.isItem || this._paneInfo.itemId != aItemId)
       return;
 
     switch (aProperty) {
+    case "title":
+      if (this._paneInfo.isItem)
+        this._onItemTitleChange(aItemId, aValue);
+      break;
     case "uri":
       let newURI = NetUtil.newURI(aValue);
       if (!newURI.equals(this._paneInfo.uri)) {
         this._paneInfo.uri = newURI;
         if (this._paneInfo.visibleRows.has("locationRow"))
           this._initLocationField();
 
         if (this._paneInfo.visibleRows.has("tagsRow")) {
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -1463,16 +1463,22 @@ this.UITour = {
     if (aAnchor.targetName.startsWith(TARGET_SEARCHENGINE_PREFIX))
       return;
 
     this._setAppMenuStateForAnnotation(aChromeWindow, "info",
                                        this.targetIsInAppMenu(aAnchor),
                                        showInfoPanel.bind(this, this._correctAnchor(aAnchor.node)));
   },
 
+  isInfoOnTarget(aChromeWindow, aTargetName) {
+    let document = aChromeWindow.document;
+    let tooltip = document.getElementById("UITourTooltip");
+    return tooltip.getAttribute("targetName") == aTargetName && tooltip.state != "closed";
+  },
+
   hideInfo: function(aWindow) {
     let document = aWindow.document;
 
     let tooltip = document.getElementById("UITourTooltip");
     this._removeAnnotationPanelMutationObserver(tooltip);
     tooltip.hidePopup();
     this._setAppMenuStateForAnnotation(aWindow, "info", false);
 
--- a/browser/devtools/performance/modules/graphs.js
+++ b/browser/devtools/performance/modules/graphs.js
@@ -21,16 +21,19 @@ loader.lazyRequireGetter(this, "Profiler
   "devtools/shared/profiler/global");
 loader.lazyRequireGetter(this, "TimelineGlobal",
   "devtools/shared/timeline/global");
 loader.lazyRequireGetter(this, "MarkersOverview",
   "devtools/shared/timeline/markers-overview", true);
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/toolkit/event-emitter");
 
+// TODO get rid of retro mode in bug 1160313
+loader.lazyRequireGetter(this, "Services");
+
 /**
  * For line graphs
  */
 const HEIGHT = 35; // px
 const STROKE_WIDTH = 1; // px
 const DAMPEN_VALUES = 0.95;
 const CLIPHEAD_LINE_COLOR = "#666";
 const SELECTION_LINE_COLOR = "#555";
@@ -160,29 +163,49 @@ const GRAPH_DEFINITIONS = {
   timeline: {
     constructor: TimelineGraph,
     selector: "#markers-overview",
     needsBlueprints: true,
     primaryLink: true
   }
 };
 
+// TODO get rid of retro mode in bug 1160313
+const GRAPH_DEFINITIONS_RETRO = {
+  memory: {
+    constructor: MemoryGraph,
+    selector: "#memory-overview",
+  },
+  framerate: {
+    constructor: FramerateGraph,
+    selector: "#time-framerate",
+    needsBlueprints: true,
+    primaryLink: true
+  },
+  timeline: {
+    constructor: TimelineGraph,
+    selector: "#markers-overview",
+  }
+};
+
 /**
  * A controller for orchestrating the performance's tool overview graphs. Constructs,
  * syncs, toggles displays and defines the memory, framerate and timeline view.
  *
  * @param {object} definition
  * @param {DOMElement} root
  * @param {function} getBlueprint
  * @param {function} getTheme
  */
 function GraphsController ({ definition, root, getBlueprint, getTheme }) {
   this._graphs = {};
   this._enabled = new Set();
-  this._definition = definition || GRAPH_DEFINITIONS;
+  // TODO get rid of retro mode in bug 1160313
+  let RETRO_MODE = Services.prefs.getBoolPref("devtools.performance.ui.retro-mode");
+  this._definition = definition || (RETRO_MODE ? GRAPH_DEFINITIONS_RETRO : GRAPH_DEFINITIONS);
   this._root = root;
   this._getBlueprint = getBlueprint;
   this._getTheme = getTheme;
   this._primaryLink = Object.keys(this._definition).filter(name => this._definition[name].primaryLink)[0];
   this.$ = root.ownerDocument.querySelector.bind(root.ownerDocument);
 
   EventEmitter.decorate(this);
   this._onSelecting = this._onSelecting.bind(this);
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -279,21 +279,26 @@ let PerformanceController = {
     this._nonBooleanPrefs[prefName] = prefValue;
   },
 
   /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
+    // Store retro-mode here so we can easily list true/false
+    // values for reverting.
+    // TODO bug 1160313
+    let superMode = !this.getOption("retro-mode");
+
     let options = {
-      withMarkers: true,
-      withMemory: this.getOption("enable-memory"),
+      withMarkers: superMode ? true : false,
+      withMemory: superMode ? this.getOption("enable-memory") : false,
       withTicks: this.getOption("enable-framerate"),
-      withAllocations: this.getOption("enable-memory"),
+      withAllocations: superMode ? this.getOption("enable-memory") : false,
       allocationsSampleProbability: this.getPref("memory-sample-probability"),
       allocationsMaxLogLength: this.getPref("memory-max-log-length"),
       bufferSize: this.getPref("profiler-buffer-size"),
       sampleFrequency: this.getPref("profiler-sample-frequency")
     };
 
     this.emit(EVENTS.RECORDING_WILL_START);
 
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -117,10 +117,15 @@ support-files =
 [browser_profiler_tree-view-01.js]
 [browser_profiler_tree-view-02.js]
 [browser_profiler_tree-view-03.js]
 [browser_profiler_tree-view-04.js]
 [browser_profiler_tree-view-05.js]
 [browser_profiler_tree-view-06.js]
 [browser_profiler_tree-view-07.js]
 [browser_profiler_tree-view-08.js]
-[browser_timeline_blueprint.js]
-[browser_timeline_filters.js]
+[browser_timeline-blueprint.js]
+[browser_timeline-filters.js]
+[browser_timeline-waterfall-background.js]
+[browser_timeline-waterfall-generic.js]
+[browser_timeline-waterfall-sidebar.js]
+# remove in bug 1160313
+[browser_retro-test.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_retro-test.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that only js-calltree view is on, default, and many things are hidden
+ * when in retro mode.
+ */
+const HIDDEN_OPTIONS = ["option-enable-memory", "option-invert-flame-graph", "option-show-jit-optimizations", "option-flatten-tree-recursion"];
+
+Services.prefs.setBoolPref("devtools.performance.ui.retro-mode", true);
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, DetailsView, PerformanceController, $, $$, JsCallTreeView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  let model = PerformanceController.getCurrentRecording();
+
+  ok(model.getMemory().length === 0, "model did not record memory.");
+  ok(model.getTicks().length !== 0, "model did get ticks.");
+  ok(model.getAllocations().sites.length === 0, "model did get allocation data.");
+  ok(model.getAllocations().timestamps.length === 0, "model did get allocation data.");
+  ok(model.getAllocations().frames.length === 0, "model did get allocation data.");
+  ok(model.getAllocations().counts.length === 0, "model did get allocation data.");
+
+  ok(DetailsView.isViewSelected(JsCallTreeView),
+    "The jscalltree view is selected by default");
+
+  for (let option of $$("#performance-options-menupopup > menuitem")) {
+    if (HIDDEN_OPTIONS.indexOf(option.id) !== -1) {
+      ok(option.hidden === true, `${option.id} should be hidden.`);
+    } else {
+      ok(option.hidden === false, `${option.id} should be visible.`);
+    }
+  }
+
+  for (let viewbutton of $$("#performance-toolbar-controls-detail-views > toolbarbutton")) {
+    ok (viewbutton.hidden === true, `${viewbutton.id} should be hidden.`);
+  }
+
+  ok($("#markers-overview").hidden, "markers overview should be hidden.");
+  ok($("#memory-overview").hidden, "memory overview should be hidden.");
+  ok(!$("#time-framerate").hidden, "framerate should be shown.");
+
+  yield teardown(panel);
+  finish();
+}
rename from browser/devtools/performance/test/browser_timeline_blueprint.js
rename to browser/devtools/performance/test/browser_timeline-blueprint.js
rename from browser/devtools/performance/test/browser_timeline_filters.js
rename to browser/devtools/performance/test/browser_timeline-filters.js
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-background.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the waterfall background is a 1px high canvas stretching across
+ * the container bounds.
+ */
+
+function spawnTest () {
+  let { target, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, PerformanceController, OverviewView, DetailsView, WaterfallView } = panel.panelWin;
+
+  yield startRecording(panel);
+  ok(true, "Recording has started.");
+
+  let updated = 0;
+  OverviewView.on(EVENTS.OVERVIEW_RENDERED, () => updated++);
+
+  ok((yield waitUntil(() => updated > 0)),
+    "The overview graphs were updated a bunch of times.");
+  ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > 0)),
+    "There are some markers available.");
+
+  let rendered = Promise.all([
+    DetailsView.selectView("waterfall"),
+    once(WaterfallView, EVENTS.WATERFALL_RENDERED)
+  ]);
+  yield stopRecording(panel);
+  ok(true, "Recording has ended.");
+  yield rendered;
+
+  // Test the waterfall background.
+
+  let parentWidth = $("#waterfall-view").getBoundingClientRect().width;
+  let sidebarWidth = $(".waterfall-sidebar").getBoundingClientRect().width;
+  let detailsWidth = $("#waterfall-details").getBoundingClientRect().width;
+  let waterfallWidth = WaterfallView.waterfall._waterfallWidth;
+  is(waterfallWidth, parentWidth - sidebarWidth - detailsWidth,
+    "The waterfall width is correct.")
+
+  ok(WaterfallView.waterfall._canvas,
+    "A canvas should be created after the recording ended.");
+  ok(WaterfallView.waterfall._ctx,
+    "A 2d context should be created after the recording ended.");
+
+  is(WaterfallView.waterfall._canvas.width, waterfallWidth,
+    "The canvas width is correct.");
+  is(WaterfallView.waterfall._canvas.height, 1,
+    "The canvas height is correct.");
+
+  yield teardown(panel);
+  finish();
+}
rename from browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
rename to browser/devtools/performance/test/browser_timeline-waterfall-generic.js
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-generic.js
@@ -1,31 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Tests if the waterfall is properly built after finishing a recording.
  */
 
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, $$, EVENTS, TimelineController } = panel.panelWin;
+function spawnTest () {
+  let { target, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, $$, EVENTS, PerformanceController, OverviewView, WaterfallView } = panel.panelWin;
 
-  yield TimelineController.toggleRecording();
+  yield startRecording(panel);
   ok(true, "Recording has started.");
 
   let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
+  OverviewView.on(EVENTS.OVERVIEW_RENDERED, () => updated++);
 
   ok((yield waitUntil(() => updated > 0)),
     "The overview graphs were updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
+  ok((yield waitUntil(() => PerformanceController.getCurrentRecording().getMarkers().length > 0)),
     "There are some markers available.");
 
-  yield TimelineController.toggleRecording();
+  yield stopRecording(panel);
   ok(true, "Recording has ended.");
 
   // Test the header container.
 
   ok($(".waterfall-header-container"),
     "A header container should have been created.");
 
   // Test the header sidebar (left).
@@ -57,9 +57,11 @@ add_task(function*() {
     "Some marker name labels should have been created inside the sidebar.");
 
   // Test the markers waterfall (right).
 
   ok($$(".waterfall-marker-item").length,
     "Some marker waterfall nodes should have been created.");
   ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
     "Some marker color bars should have been created inside the waterfall.");
-});
+  yield teardown(panel);
+  finish();
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_timeline-waterfall-sidebar.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the sidebar is properly updated when a marker is selected.
+ */
+
+function spawnTest () {
+  let { target, panel } = yield initPerformance(SIMPLE_URL);
+  let { $, $$, EVENTS, PerformanceController, OverviewView } = panel.panelWin;
+  let { L10N, TIMELINE_BLUEPRINT } = devtools.require("devtools/shared/timeline/global");
+
+  yield startRecording(panel);
+  ok(true, "Recording has started.");
+
+  yield waitUntil(() => {
+    // Wait until we get 3 different markers.
+    let markers = PerformanceController.getCurrentRecording().getMarkers();
+    return markers.some(m => m.name == "Styles") &&
+           markers.some(m => m.name == "Reflow") &&
+           markers.some(m => m.name == "Paint");
+  });
+
+  yield stopRecording(panel);
+  ok(true, "Recording has ended.");
+
+  // Select everything
+  OverviewView.graphs.get("timeline").setSelection({ start: 0, end: OverviewView.graphs.get("timeline").width })
+
+  let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
+  let markers = PerformanceController.getCurrentRecording().getMarkers();
+
+  ok(bars.length > 2, "got at least 3 markers");
+
+  let sidebar = $("#waterfall-details");
+  for (let i = 0; i < bars.length; i++) {
+    let bar = bars[i];
+    bar.click();
+    let m = markers[i];
+
+    let name = TIMELINE_BLUEPRINT[m.name].label;
+
+    is($("#waterfall-details .marker-details-type").getAttribute("value"), name,
+      "sidebar title matches markers name");
+
+    let printedStartTime = $(".marker-details-start .marker-details-labelvalue").getAttribute("value");
+    let printedEndTime = $(".marker-details-end .marker-details-labelvalue").getAttribute("value");
+    let printedDuration= $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
+
+    let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
+
+    // Values are rounded. We don't use a strict equality.
+    is(toMs(m.start), printedStartTime, "sidebar start time is valid");
+    is(toMs(m.end), printedEndTime, "sidebar end time is valid");
+    is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
+  }
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -51,27 +51,32 @@ let DEFAULT_PREFS = [
   "devtools.performance.ui.show-idle-blocks",
   "devtools.performance.ui.enable-memory",
   "devtools.performance.ui.enable-framerate",
   "devtools.performance.ui.show-jit-optimizations",
   "devtools.performance.memory.sample-probability",
   "devtools.performance.memory.max-log-length",
   "devtools.performance.profiler.buffer-size",
   "devtools.performance.profiler.sample-frequency-khz",
+  "devtools.performance.ui.retro-mode",
 ].reduce((prefs, pref) => {
   prefs[pref] = Preferences.get(pref);
   return prefs;
 }, {});
 
 // Enable the new performance panel for all tests.
 Services.prefs.setBoolPref("devtools.performance.enabled", true);
 // Enable logging for all the tests. Both the debugger server and frontend will
 // be affected by this pref.
 Services.prefs.setBoolPref("devtools.debugger.log", false);
 
+// Disable retro mode.
+// TODO bug 1160313
+Services.prefs.setBoolPref("devtools.performance.ui.retro-mode", false);
+
 /**
  * Call manually in tests that use frame script utils after initializing
  * the tool. Must be called after initializing so we can detect
  * whether or not `content` is a CPOW or not. Call after init but before navigating
  * to different pages.
  */
 function loadFrameScripts () {
   mm = gBrowser.selectedBrowser.messageManager;
--- a/browser/devtools/performance/views/details-abstract-subview.js
+++ b/browser/devtools/performance/views/details-abstract-subview.js
@@ -67,16 +67,22 @@ let DetailsSubview = {
 
   /**
    * An array of preferences under `devtools.performance.` that the view should
    * observe and callback `this._onObservedPrefChange` upon change.
    */
   observedPrefs: [],
 
   /**
+   * Flag specifying if this view should update while the overview selection
+   * area is actively being dragged by the mouse.
+   */
+  shouldUpdateWhileMouseIsActive: false,
+
+  /**
    * Called when recording stops or is selected.
    */
   _onRecordingStoppedOrSelected: function(_, recording) {
     if (!recording || recording.isRecording()) {
       return;
     }
     if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
       this.render();
@@ -85,17 +91,24 @@ let DetailsSubview = {
     }
   },
 
   /**
    * Fired when a range is selected or cleared in the OverviewView.
    */
   _onOverviewRangeChange: function (_, interval) {
     if (DetailsView.isViewSelected(this)) {
-      let debounced = () => this.render(interval);
+      let debounced = () => {
+        if (!this.shouldUpdateWhileMouseIsActive && OverviewView.isMouseActive) {
+          // Don't render yet, while the selection is still being dragged.
+          setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced);
+        } else {
+          this.render(interval);
+        }
+      };
       setNamedTimeout("range-change-debounce", this.rangeChangeDebounceTime, debounced);
     } else {
       this.shouldUpdateWhenShown = true;
     }
   },
 
   /**
    * Fired when a view is selected in the DetailsView.
--- a/browser/devtools/performance/views/details-js-call-tree.js
+++ b/browser/devtools/performance/views/details-js-call-tree.js
@@ -8,17 +8,17 @@
  */
 let JsCallTreeView = Heritage.extend(DetailsSubview, {
 
   rerenderPrefs: [
     "invert-call-tree",
     "show-platform-data"
   ],
 
-  rangeChangeDebounceTime: 50, // ms
+  rangeChangeDebounceTime: 75, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
     this._onPrefChanged = this._onPrefChanged.bind(this);
--- a/browser/devtools/performance/views/details-js-flamegraph.js
+++ b/browser/devtools/performance/views/details-js-flamegraph.js
@@ -4,16 +4,18 @@
 "use strict";
 
 /**
  * FlameGraph view containing a pyramid-like visualization of a profile,
  * controlled by DetailsView.
  */
 let JsFlameGraphView = Heritage.extend(DetailsSubview, {
 
+  shouldUpdateWhileMouseIsActive: true,
+
   rerenderPrefs: [
     "invert-flame-graph",
     "flatten-tree-recursion",
     "show-platform-data",
     "show-idle-blocks"
   ],
 
   /**
--- a/browser/devtools/performance/views/details-memory-flamegraph.js
+++ b/browser/devtools/performance/views/details-memory-flamegraph.js
@@ -4,16 +4,18 @@
 "use strict";
 
 /**
  * FlameGraph view containing a pyramid-like visualization of memory allocation
  * sites, controlled by DetailsView.
  */
 let MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
 
+  shouldUpdateWhileMouseIsActive: true,
+
   rerenderPrefs: [
     "invert-flame-graph",
     "flatten-tree-recursion",
     "show-idle-blocks"
   ],
 
   /**
    * Sets up the view with event binding.
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -11,17 +11,17 @@ let WaterfallView = Heritage.extend(Deta
   observedPrefs: [
     "hidden-markers"
   ],
 
   rerenderPrefs: [
     "hidden-markers"
   ],
 
-  rangeChangeDebounceTime: 10, // ms
+  rangeChangeDebounceTime: 75, // ms
 
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     DetailsSubview.initialize.call(this);
 
     this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#waterfall-view"));
@@ -69,16 +69,21 @@ let WaterfallView = Heritage.extend(Deta
   },
 
   /**
    * Called when a marker is selected in the waterfall view,
    * updating the markers detail view.
    */
   _onMarkerSelected: function (event, marker) {
     let recording = PerformanceController.getCurrentRecording();
+    // Race condition in tests due to lazy rendering of markers in the
+    // waterfall? intermittent bug 1157523
+    if (!recording) {
+      return;
+    }
     let frames = recording.getFrames();
 
     if (event === "selected") {
       this.details.render({ toolbox: gToolbox, marker, frames });
     }
     if (event === "unselected") {
       this.details.empty();
     }
--- a/browser/devtools/performance/views/details.js
+++ b/browser/devtools/performance/views/details.js
@@ -89,19 +89,22 @@ let DetailsView = {
    * if currently selected. Called when a preference changes in `devtools.performance.ui.`.
    */
   setAvailableViews: Task.async(function* () {
     let recording = PerformanceController.getCurrentRecording();
     let isRecording = recording && recording.isRecording();
     let invalidCurrentView = false;
 
     for (let [name, { view }] of Iterator(this.components)) {
-      let isSupported = this._isViewSupported(name, false);
+      // TODO bug 1160313 get rid of retro mode checks.
+      let isRetro = PerformanceController.getOption("retro-mode");
+      let isSupported = isRetro ? name === "js-calltree" : this._isViewSupported(name, false);
 
-      $(`toolbarbutton[data-view=${name}]`).hidden = !isSupported;
+      // TODO bug 1160313 hide all view buttons, but let js-calltree still be "supported"
+      $(`toolbarbutton[data-view=${name}]`).hidden = isRetro ? true : !isSupported;
 
       // If the view is currently selected and not supported, go back to the
       // default view.
       if (!isSupported && this.isViewSelected(view)) {
         invalidCurrentView = true;
       }
     }
 
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -88,16 +88,26 @@ let OverviewView = {
     PerformanceController.off(EVENTS.CONSOLE_RECORDING_STOPPED, this._onRecordingStopped);
     PerformanceController.off(EVENTS.CONSOLE_RECORDING_WILL_STOP, this._onRecordingWillStop);
     this.graphs.off("selecting", this._onGraphSelecting);
     this.graphs.off("rendered", this._onGraphRendered);
     yield this.graphs.destroy();
   }),
 
   /**
+   * Returns true if any of the overview graphs have mouse dragging active,
+   * false otherwise.
+   */
+  get isMouseActive() {
+    return (this.markersOverview && this.markersOverview.isMouseActive) ||
+           (this.memoryOverview && this.memoryOverview.isMouseActive) ||
+           (this.framerateGraph && this.framerateGraph.isMouseActive);
+  },
+
+  /**
    * Disabled in the event we're using a Timeline mock, so we'll have no
    * timeline, ticks or memory data to show, so just block rendering and hide
    * the panel.
    */
   disable: function () {
     this._disabled = true;
     this.graphs.disableAll();
   },
--- a/browser/devtools/performance/views/toolbar.js
+++ b/browser/devtools/performance/views/toolbar.js
@@ -16,16 +16,29 @@ let ToolbarView = {
     this._onHiddenMarkersChanged = this._onHiddenMarkersChanged.bind(this);
     this._onPrefChanged = this._onPrefChanged.bind(this);
 
     this.optionsView = new OptionsView({
       branchName: BRANCH_NAME,
       menupopup: $("#performance-options-menupopup")
     });
 
+    // TODO bug 1160313 get rid of retro mode checks
+    // hide option buttons here, and any other buttons in the toolbar
+    // (details.js takes care of view buttons)
+    if (PerformanceController.getOption("retro-mode")) {
+      let RETRO_ELEMENTS = [
+        "#option-flatten-tree-recursion", "#option-enable-memory", "#option-invert-flame-graph",
+        "#option-show-jit-optimizations", "#filter-button"
+      ];
+      for (let selector of RETRO_ELEMENTS) {
+        $(selector).hidden = true;
+      }
+    }
+
     yield this.optionsView.initialize();
     this.optionsView.on("pref-changed", this._onPrefChanged);
 
     this._buildMarkersFilterPopup();
     this._updateHiddenMarkersPopup();
     $("#performance-filter-menupopup").addEventListener("popupshowing", this._onFilterPopupShowing);
     $("#performance-filter-menupopup").addEventListener("popuphiding",  this._onFilterPopupHiding);
   }),
--- a/browser/devtools/shared/widgets/Graphs.jsm
+++ b/browser/devtools/shared/widgets/Graphs.jsm
@@ -216,16 +216,24 @@ AbstractCanvasGraph.prototype = {
   get width() {
     return this._width;
   },
   get height() {
     return this._height;
   },
 
   /**
+   * Return true if the mouse is actively messing with the selection, false
+   * otherwise.
+   */
+  get isMouseActive() {
+    return this._isMouseActive;
+  },
+
+  /**
    * Returns a promise resolved once this graph is ready to receive data.
    */
   ready: function() {
     return this._ready.promise;
   },
 
   /**
    * Destroys this graph.
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser.ini
+++ /dev/null
@@ -1,19 +0,0 @@
-[DEFAULT]
-tags = devtools
-subsuite = devtools
-support-files =
-  doc_simple-test.html
-  head.js
-
-[browser_timeline_aaa_run_first_leaktest.js]
-[browser_timeline_overview-initial-selection-01.js]
-[browser_timeline_overview-initial-selection-02.js]
-[browser_timeline_overview-update.js]
-[browser_timeline_overview-theme.js]
-[browser_timeline_panels.js]
-[browser_timeline_recording-without-memory.js]
-[browser_timeline_recording.js]
-[browser_timeline_waterfall-background.js]
-[browser_timeline_waterfall-generic.js]
-[browser_timeline_waterfall-styles.js]
-[browser_timeline_waterfall-sidebar.js]
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the timeline leaks on initialization and sudden destruction.
- * You can also use this initialization format as a template for other tests.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-
-  ok(target, "Should have a target available.");
-  ok(panel, "Should have a panel available.");
-
-  ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window.");
-  ok(panel.panelWin.gTarget, "Should have a target reference on the panel window.");
-  ok(panel.panelWin.gFront, "Should have a front reference on the panel window.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the overview has an initial selection when recording has finished
- * and there is data available.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-  let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
-
-  $("#memory-checkbox").checked = true;
-  yield TimelineController.updateMemoryRecording();
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
-
-  ok((yield waitUntil(() => updated > 10)),
-    "The overview graph was updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
-    "There are some markers available.");
-  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
-    "There are some memory measurements available now.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  let interval = TimelineController.getInterval();
-  let markers = TimelineController.getMarkers();
-  let selection = TimelineView.markersOverview.getSelection();
-
-  is((selection.start) | 0,
-     (markers[0].start * TimelineView.markersOverview.dataScaleX) | 0,
-    "The initial selection start is correct.");
-
-  is((selection.end - selection.start) | 0,
-     (selectionRatio * TimelineView.markersOverview.width) | 0,
-    "The initial selection end is correct.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the overview has no initial selection when recording has finished
- * and there is no data available.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-  let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
-
-  $("#memory-checkbox").checked = true;
-  yield TimelineController.updateMemoryRecording();
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  yield TimelineController._stopRecordingAndDiscardData();
-  ok(true, "Recording has ended.");
-
-  let markers = TimelineController.getMarkers();
-  let memory = TimelineController.getMemory();
-  let selection = TimelineView.markersOverview.getSelection();
-
-  is(markers.length, 0,
-    "There are no markers available.");
-  is(memory.length, 0,
-    "There are no memory measurements available.");
-  is(selection.start, null,
-    "The initial selection start is correct.");
-  is(selection.end, null,
-    "The initial selection end is correct.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_overview-theme.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the markers and memory overviews render with the correct
- * theme on load, and rerenders when changed.
- */
-
-const LIGHT_BG = "#fcfcfc";
-const DARK_BG = "#14171a";
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel("about:blank");
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-
-  $("#memory-checkbox").checked = true;
-
-  setTheme("dark");
-
-  yield TimelineController.updateMemoryRecording();
-  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
-    "correct theme on load for markers.");
-  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
-    "correct theme on load for memory.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  let refreshed = Promise.all([
-    once(TimelineView.markersOverview, "refresh"),
-    once(TimelineView.memoryOverview, "refresh"),
-  ]);
-
-  setTheme("light");
-  yield refreshed;
-
-  ok(true, "Both memory and markers were rerendered after theme change.");
-  is(TimelineView.markersOverview.backgroundColor, LIGHT_BG,
-    "correct theme on after toggle for markers.");
-  is(TimelineView.memoryOverview.backgroundColor, LIGHT_BG,
-    "correct theme on after toggle for memory.");
-
-  refreshed = Promise.all([
-    once(TimelineView.markersOverview, "refresh"),
-    once(TimelineView.memoryOverview, "refresh"),
-  ]);
-
-  setTheme("dark");
-  yield refreshed;
-
-  ok(true, "Both memory and markers were rerendered after theme change.");
-  is(TimelineView.markersOverview.backgroundColor, DARK_BG,
-    "correct theme on after toggle for markers once more.");
-  is(TimelineView.memoryOverview.backgroundColor, DARK_BG,
-    "correct theme on after toggle for memory once more.");
-
-  refreshed = Promise.all([
-    once(TimelineView.markersOverview, "refresh"),
-    once(TimelineView.memoryOverview, "refresh"),
-  ]);
-
-  // Set theme back to light
-  setTheme("light");
-  yield refreshed;
-});
-
-/**
- * Mimics selecting the theme selector in the toolbox;
- * sets the preference and emits an event on gDevTools to trigger
- * the themeing.
- */
-function setTheme (newTheme) {
-  let oldTheme = Services.prefs.getCharPref("devtools.theme");
-  info("Setting `devtools.theme` to \"" + newTheme + "\"");
-  Services.prefs.setCharPref("devtools.theme", newTheme);
-  gDevTools.emit("pref-changed", {
-    pref: "devtools.theme",
-    newValue: newTheme,
-    oldValue: oldTheme
-  });
-}
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_overview-update.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the markers and memory overviews are continuously updated.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel("about:blank");
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-
-  $("#memory-checkbox").checked = true;
-  yield TimelineController.updateMemoryRecording();
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  ok("selectionEnabled" in TimelineView.markersOverview,
-    "The selection should not be enabled for the markers overview (1).");
-  is(TimelineView.markersOverview.selectionEnabled, false,
-    "The selection should not be enabled for the markers overview (2).");
-  is(TimelineView.markersOverview.hasSelection(), false,
-    "The markers overview shouldn't have a selection before recording.");
-
-  ok("selectionEnabled" in TimelineView.memoryOverview,
-    "The selection should not be enabled for the memory overview (1).");
-  is(TimelineView.memoryOverview.selectionEnabled, false,
-    "The selection should not be enabled for the memory overview (2).");
-  is(TimelineView.memoryOverview.hasSelection(), false,
-    "The memory overview shouldn't have a selection before recording.");
-
-  let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
-
-  ok((yield waitUntil(() => updated > 10)),
-    "The overviews were updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMemory().length > 10)),
-    "There are some memory measurements available now.");
-
-  ok("selectionEnabled" in TimelineView.markersOverview,
-    "The selection should still not be enabled for the markers overview (3).");
-  is(TimelineView.markersOverview.selectionEnabled, false,
-    "The selection should still not be enabled for the markers overview (4).");
-  is(TimelineView.markersOverview.hasSelection(), false,
-    "The markers overview should not have a selection while recording.");
-
-  ok("selectionEnabled" in TimelineView.memoryOverview,
-    "The selection should still not be enabled for the memory overview (3).");
-  is(TimelineView.memoryOverview.selectionEnabled, false,
-    "The selection should still not be enabled for the memory overview (4).");
-  is(TimelineView.memoryOverview.hasSelection(), false,
-    "The memory overview should not have a selection while recording.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  // TODO: Re-enable this assertion as part of bug 1120830
-  // is(TimelineController.getMarkers().length, 0,
-  //  "There are no markers available.");
-  isnot(TimelineController.getMemory().length, 0,
-    "There are some memory measurements available.");
-
-  is(TimelineView.markersOverview.selectionEnabled, true,
-    "The selection should now be enabled for the markers overview.");
-  // TODO: Re-enable this assertion as part of bug 1120830
-  // is(TimelineView.markersOverview.hasSelection(), false,
-  //  "The markers overview should not have a selection after recording.");
-
-  is(TimelineView.memoryOverview.selectionEnabled, true,
-    "The selection should now be enabled for the memory overview.");
-  // TODO: Re-enable this assertion as part of bug 1120830
-  // is(TimelineView.memoryOverview.hasSelection(), false,
-  //  "The memory overview should not have a selection after recording.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_panels.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the timeline panels are correctly shown and hidden when
- * recording starts and stops.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, EVENTS } = panel.panelWin;
-
-  is($("#record-button").hasAttribute("checked"), false,
-    "The record button should not be checked yet.");
-  is($("#timeline-pane").selectedPanel, $("#empty-notice"),
-    "An empty notice is initially displayed instead of the waterfall view.");
-
-  let whenRecStarted = panel.panelWin.once(EVENTS.RECORDING_STARTED);
-  EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
-  yield whenRecStarted;
-
-  ok(true, "Recording has started.");
-
-  is($("#record-button").getAttribute("checked"), "true",
-    "The record button should be checked now.");
-  is($("#timeline-pane").selectedPanel, $("#recording-notice"),
-    "A recording notice is now displayed instead of the waterfall view.");
-
-  let whenRecEnded = panel.panelWin.once(EVENTS.RECORDING_ENDED);
-  EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin);
-  yield whenRecEnded;
-
-  ok(true, "Recording has ended.");
-
-  is($("#record-button").hasAttribute("checked"), false,
-    "The record button should be unchecked again.");
-  is($("#timeline-pane").selectedPanel, $("#timeline-waterfall-container"),
-    "A waterfall view is now displayed.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_recording-without-memory.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the timeline actor isn't unnecessarily asked to record memory.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
-
-  ok((yield waitUntil(() => updated > 10)),
-    "The overview graph was updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
-    "There are some markers available.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  let markers = TimelineController.getMarkers();
-  let memory = TimelineController.getMemory();
-
-  isnot(markers.length, 0,
-    "There are some markers available.");
-  is(memory.length, 0,
-    "There are no memory measurements available.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_recording.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the timeline can properly start and stop a recording.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, gFront, TimelineController } = panel.panelWin;
-
-  $("#memory-checkbox").checked = true;
-  yield TimelineController.updateMemoryRecording();
-
-  is((yield gFront.isRecording()), false,
-    "The timeline actor should not be recording when the tool starts.");
-  is(TimelineController.getMarkers().length, 0,
-    "There should be no markers available when the tool starts.");
-  is(TimelineController.getMemory().length, 0,
-    "There should be no memory measurements available when the tool starts.");
-
-  yield TimelineController.toggleRecording();
-
-  is((yield gFront.isRecording()), true,
-    "The timeline actor should be recording now.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
-    "There are some markers available now.");
-  ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
-    "There are some memory measurements available now.");
-
-  info("Interval: " + TimelineController.getInterval().toSource());
-  info("Markers: " + TimelineController.getMarkers().toSource());
-  info("Memory: " + TimelineController.getMemory().toSource());
-
-  ok("startTime" in TimelineController.getInterval(),
-    "A `startTime` field was set on the recording data.");
-  ok("endTime" in TimelineController.getInterval(),
-    "An `endTime` field was set on the recording data.");
-
-  ok(TimelineController.getInterval().endTime >
-     TimelineController.getInterval().startTime,
-    "Some time has passed since the recording started.");
-
-  yield TimelineController.toggleRecording();
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-background.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the waterfall background is a 1px high canvas stretching across
- * the container bounds.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
-
-  ok((yield waitUntil(() => updated > 0)),
-    "The overview graphs were updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
-    "There are some markers available.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  // Test the waterfall background.
-
-  let parentWidth = $("#timeline-waterfall").getBoundingClientRect().width;
-  let waterfallWidth = TimelineView.waterfall._waterfallWidth;
-  let sidebarWidth = 150; // px
-  is(waterfallWidth, parentWidth - sidebarWidth,
-    "The waterfall width is correct.")
-
-  ok(TimelineView.waterfall._canvas,
-    "A canvas should be created after the recording ended.");
-  ok(TimelineView.waterfall._ctx,
-    "A 2d context should be created after the recording ended.");
-
-  is(TimelineView.waterfall._canvas.width, waterfallWidth,
-    "The canvas width is correct.");
-  is(TimelineView.waterfall._canvas.height, 1,
-    "The canvas height is correct.");
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-sidebar.js
+++ /dev/null
@@ -1,58 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the sidebar is properly updated when a marker is selected.
- */
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { $, $$, EVENTS, TimelineController, TimelineView, TIMELINE_BLUEPRINT} = panel.panelWin;
-  let { L10N } = devtools.require("devtools/shared/timeline/global");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  yield waitUntil(() => {
-    // Wait until we get 3 different markers.
-    let markers = TimelineController.getMarkers();
-    return markers.some(m => m.name == "Styles") &&
-           markers.some(m => m.name == "Reflow") &&
-           markers.some(m => m.name == "Paint");
-  });
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  // Select everything
-  TimelineView.markersOverview.setSelection({ start: 0, end: TimelineView.markersOverview.width })
-
-
-  let bars = $$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar");
-  let markers = TimelineController.getMarkers();
-
-  ok(bars.length > 2, "got at least 3 markers");
-
-  let sidebar = $("#timeline-waterfall-details");
-  for (let i = 0; i < bars.length; i++) {
-    let bar = bars[i];
-    bar.click();
-    let m = markers[i];
-
-    let name = TIMELINE_BLUEPRINT[m.name].label;
-
-    is($("#timeline-waterfall-details .marker-details-type").getAttribute("value"), name,
-      "sidebar title matches markers name");
-
-    let printedStartTime = $(".marker-details-start .marker-details-labelvalue").getAttribute("value");
-    let printedEndTime = $(".marker-details-end .marker-details-labelvalue").getAttribute("value");
-    let printedDuration= $(".marker-details-duration .marker-details-labelvalue").getAttribute("value");
-
-    let toMs = ms => L10N.getFormatStrWithNumbers("timeline.tick", ms);
-
-    // Values are rounded. We don't use a strict equality.
-    is(toMs(m.start), printedStartTime, "sidebar start time is valid");
-    is(toMs(m.end), printedEndTime, "sidebar end time is valid");
-    is(toMs(m.end - m.start), printedDuration, "sidebar duration is valid");
-  }
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js
+++ /dev/null
@@ -1,88 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-/**
- * Tests if the waterfall is properly built after making a selection
- * and the child nodes are styled correctly.
- */
-
-var gRGB_TO_HSL = {
- "rgb(193, 132, 214)": "hsl(285,50%,68%)",
- "rgb(152, 61, 183)": "hsl(285,50%,48%)",
- "rgb(161, 223, 138)": "hsl(104,57%,71%)",
- "rgb(96, 201, 58)": "hsl(104,57%,51%)",
- "rgb(240, 195, 111)": "hsl(39,82%,69%)",
- "rgb(227, 155, 22)": "hsl(39,82%,49%)",
- "rgb(204, 204, 204)": "hsl(0,0%,80%)",
- "rgb(153, 153, 153)": "hsl(0,0%,60%)",
-};
-
-add_task(function*() {
-  let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
-  let { TIMELINE_BLUEPRINT } = devtools.require("devtools/shared/timeline/global");
-  let { $, $$, EVENTS, TimelineController } = panel.panelWin;
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has started.");
-
-  let updated = 0;
-  panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
-
-  ok((yield waitUntil(() => updated > 0)),
-    "The overview graphs were updated a bunch of times.");
-  ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
-    "There are some markers available.");
-
-  yield TimelineController.toggleRecording();
-  ok(true, "Recording has ended.");
-
-  // Test the table sidebars.
-
-  for (let sidebar of [
-    ...$$(".timeline-header-sidebar"),
-    ...$$(".timeline-marker-sidebar")
-  ]) {
-    is(sidebar.getAttribute("width"), "150",
-      "The table's sidebar width is correct.");
-  }
-
-  // Test the table ticks.
-
-  for (let tick of $$(".timeline-header-tick")) {
-    ok(tick.getAttribute("value").match(/^\d+ ms$/),
-      "The table's timeline ticks appear to have correct labels.");
-    ok(tick.style.transform.match(/^translateX\(.*px\)$/),
-      "The table's timeline ticks appear to have proper translations.");
-  }
-
-  // Test the marker bullets.
-
-  for (let bullet of $$(".timeline-marker-bullet")) {
-    let type = bullet.getAttribute("type");
-
-    ok(type in TIMELINE_BLUEPRINT,
-      "The bullet type is present in the timeline blueprint.");
-    is(gRGB_TO_HSL[bullet.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
-      "The bullet's background color is correct.");
-    is(gRGB_TO_HSL[bullet.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
-      "The bullet's border color is correct.");
-  }
-
-  // Test the marker bars.
-
-  for (let bar of $$(".timeline-marker-bar")) {
-    let type = bar.getAttribute("type");
-
-    ok(type in TIMELINE_BLUEPRINT,
-      "The bar type is present in the timeline blueprint.");
-    is(gRGB_TO_HSL[bar.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill,
-      "The bar's background color is correct.");
-    is(gRGB_TO_HSL[bar.style.borderColor], TIMELINE_BLUEPRINT[type].stroke,
-      "The bar's border color is correct.");
-
-    ok(bar.getAttribute("width") > 0,
-      "The bar appears to have a proper width.");
-    ok(bar.style.transform.match(/^translateX\(.*px\)$/),
-      "The bar appears to have proper translations.");
-  }
-});
deleted file mode 100644
--- a/browser/devtools/timeline/test/doc_simple-test.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!-- Any copyright is dedicated to the Public Domain.
-     http://creativecommons.org/publicdomain/zero/1.0/ -->
-<!doctype html>
-
-<html>
-  <head>
-    <meta charset="utf-8"/>
-    <title>Timeline test page</title>
-  </head>
-
-  <body>
-    <script type="text/javascript">
-      var x = 1;
-      function test() {
-        document.body.style.borderTop = x + "px solid red";
-        x = 1^x;
-        document.body.innerHeight; // flush pending reflows
-      }
-
-      // Prevent this script from being garbage collected.
-      window.setInterval(test, 1);
-    </script>
-  </body>
-
-</html>
deleted file mode 100644
--- a/browser/devtools/timeline/test/head.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
-
-// Disable logging for all the tests. Both the debugger server and frontend will
-// be affected by this pref.
-let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
-Services.prefs.setBoolPref("devtools.debugger.log", false);
-
-// Enable the tool while testing.
-let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled");
-Services.prefs.setBoolPref("devtools.timeline.enabled", true);
-
-let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
-let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
-let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {});
-let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
-let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
-
-let TargetFactory = devtools.TargetFactory;
-let Toolbox = devtools.Toolbox;
-
-const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/";
-const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html";
-
-// All tests are asynchronous.
-waitForExplicitFinish();
-
-registerCleanupFunction(() => {
-  info("finish() was called, cleaning up...");
-  Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging);
-  Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled);
-});
-
-// Close the toolbox and all opened tabs automatically.
-registerCleanupFunction(function*() {
-  let target = TargetFactory.forTab(gBrowser.selectedTab);
-  yield gDevTools.closeToolbox(target);
-
-  while (gBrowser.tabs.length > 1) {
-    gBrowser.removeCurrentTab();
-  }
-});
-
-function addTab(url) {
-  info("Adding tab: " + url);
-
-  let deferred = promise.defer();
-  let tab = gBrowser.selectedTab = gBrowser.addTab(url);
-  let linkedBrowser = tab.linkedBrowser;
-
-  linkedBrowser.addEventListener("load", function onLoad() {
-    linkedBrowser.removeEventListener("load", onLoad, true);
-    info("Tab added and finished loading: " + url);
-    deferred.resolve(tab);
-  }, true);
-
-  return deferred.promise;
-}
-
-/**
- * Spawns a new tab and starts up a toolbox with the timeline panel
- * automatically selected.
- *
- * Must be used within a task.
- *
- * @param string url
- *        The location of the new tab to spawn.
- * @return object
- *         A promise resolved once the timeline is initialized, with the
- *         {target, panel} instances.
- */
-function* initTimelinePanel(url) {
-  info("Initializing a timeline pane.");
-
-  let tab = yield addTab(url);
-  let target = TargetFactory.forTab(tab);
-
-  yield target.makeRemote();
-
-  let toolbox = yield gDevTools.showToolbox(target, "timeline");
-  let panel = toolbox.getCurrentPanel();
-  return { target, panel };
-}
-
-/**
- * Waits until a predicate returns true.
- *
- * @param function predicate
- *        Invoked once in a while until it returns true.
- * @param number interval [optional]
- *        How often the predicate is invoked, in milliseconds.
- */
-function waitUntil(predicate, interval = 10) {
-  if (predicate()) {
-    return promise.resolve(true);
-  }
-  let deferred = promise.defer();
-  setTimeout(function() {
-    waitUntil(predicate).then(() => deferred.resolve(true));
-  }, interval);
-  return deferred.promise;
-
-}
-
-/**
- * Wait until next tick.
- */
-function nextTick() {
-  let def = promise.defer();
-  executeSoon(() => def.resolve())
-  return def.promise;
-}
-
-/**
- * Wait for eventName on target.
- * @param {Object} target An observable object that either supports on/off or
- * addEventListener/removeEventListener
- * @param {String} eventName
- * @param {Boolean} useCapture Optional, for addEventListener/removeEventListener
- * @return A promise that resolves when the event has been handled
- */
-function once(target, eventName, useCapture=false) {
-  info("Waiting for event: '" + eventName + "' on " + target + ".");
-
-  let deferred = promise.defer();
-
-  for (let [add, remove] of [
-    ["addEventListener", "removeEventListener"],
-    ["addListener", "removeListener"],
-    ["on", "off"]
-  ]) {
-    if ((add in target) && (remove in target)) {
-      target[add](eventName, function onEvent(...aArgs) {
-        info("Got event: '" + eventName + "' on " + target + ".");
-        target[remove](eventName, onEvent, useCapture);
-        deferred.resolve.apply(deferred, aArgs);
-      }, useCapture);
-      break;
-    }
-  }
-
-  return deferred.promise;
-}
--- a/browser/devtools/webide/modules/simulator-process.js
+++ b/browser/devtools/webide/modules/simulator-process.js
@@ -66,19 +66,21 @@ SimulatorProcess.prototype = {
     });
 
     this.on("stdout", (e, data) => this.log(e, data.trim()));
     this.on("stderr", (e, data) => this.log(e, data.trim()));
 
     let environment;
     if (OS.indexOf("linux") > -1) {
       environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
-      if ("DISPLAY" in Environment) {
-        environment.push("DISPLAY=" + Environment.DISPLAY);
-      }
+      ["DISPLAY", "XAUTHORITY"].forEach(key => {
+        if (key in Environment) {
+          environment.push(key + "=" + Environment[key]);
+        }
+      });
     }
 
     // Spawn a B2G instance.
     this.process = Subprocess.call({
       command: b2g,
       arguments: this.args,
       environment: environment,
       stdout: data => this.emit("stdout", data),
new file mode 100644
--- /dev/null
+++ b/browser/docs/DirectoryLinksProvider.rst
@@ -0,0 +1,248 @@
+=============================================
+Directory Links Architecture and Data Formats
+=============================================
+
+Directory links are enhancements to the new tab experience that combine content
+Firefox already knows about from user browsing with external content. There are
+3 kinds of links:
+
+- directory links fill in additional tiles on the new tab page if there would
+  have been empty tiles because the user has a clean profile or cleared history
+- suggested links are shown if certain triggering criteria matches the user's
+  browsing behavior, i.e., if the user has a top site that matches one of
+  several possible sites. E.g., only show a sports suggestion if the user has a
+  sport site as a top site
+- enhanced links replace a matching user's visible history tile from the same
+  site but only the visual aspects: title, image, and rollover image
+
+To power the above features, DirectoryLinksProvider module downloads, at most
+once per 24 hours, the directory source links as JSON with enough data for
+Firefox to determine what should be shown or not. This module also handles
+reporting back data about the tiles via asynchronous pings that don't return
+data from the server.
+
+For the directory source and ping endpoints, the default preference values point
+to Mozilla key-pinned servers with encryption. No cookies are set by the servers
+but not enforced by Firefox.
+
+- default directory source endpoint:
+  https://tiles.services.mozilla.com/v3/links/fetch/%LOCALE%/%CHANNEL%
+- default directory ping endpoint: https://tiles.services.mozilla.com/v3/links/
+
+
+Preferences
+===========
+
+There are two main preferences that control downloading links and reporting
+metrics.
+
+``browser.newtabpage.directory.source``
+---------------------------------------
+
+This endpoint tells Firefox where to download directory source file as a GET
+request. It should return JSON of the appropriate format containing the relevant
+links data. The value can be a data URI, e.g., an empty JSON object effectively
+turns off remote downloading: ``data:text/plain,{}``
+
+The preference value will have %LOCALE% and %CHANNEL% replaced by the
+appropriate values for the build of Firefox, e.g.,
+
+- directory source endpoint:
+  https://tiles.services.mozilla.com/v3/links/fetch/en-US/release
+
+``browser.newtabpage.directory.ping``
+-------------------------------------
+
+This endpoint tells Firefox where to report Tiles metrics as a POST request. The
+data is sent as a JSON blob. Setting it to empty effectively turns off reporting
+of Tiles data.
+
+A path segment will be appended to the endpoint of "view" or "click" depending
+on the type of ping, e.g.,
+
+- ``view`` ping endpoint: https://tiles.services.mozilla.com/v3/links/view
+- ``click`` ping endpoint: https://tiles.services.mozilla.com/v3/links/click
+
+
+Data Flow
+=========
+
+When Firefox starts, it checks for a cached directory source file. If one
+exists, it checks for its timestamp to determine if a new file should be
+downloaded.
+
+If a directory source file needs to be downloaded, a GET request is made then
+cacheed and unpacked the JSON into the different types of links. Various checks
+filter out invalid links, e.g., those with http-hosted images or those that
+don't fit the allowed suggestions.
+
+When a new tab page is built, DirectoryLinksProvider module provides additional
+link data that is combined with history link data to determine which links can
+be displayed or not.
+
+When a new tab page is shown, a ``view`` ping is sent with relevant tiles data.
+Similarly, when the user clicks on various parts of tiles (to load the page,
+pin, block, etc.), a ``click`` ping is sent with similar data. Both of these can
+trigger downloading of fresh directory source links if 24 hours have elapsed
+since last download.
+
+Users can turn off the ping with in-new-tab-page controls.
+
+As the new tab page is rendered, any images for tiles are downloaded if not
+already cached. The default servers hosting the images are Mozilla CDN that
+don't use cookies: https://tiles.cdn.mozilla.net/
+
+
+Source JSON Format
+==================
+
+Firefox expects links data in a JSON object with top level keys each providing
+an array of tile objects. The keys correspond to the different types of links:
+``directory``, ``suggested``, and ``enhanced``.
+
+Example
+-------
+
+Below is an example directory source file::
+
+  {
+      "directory": [
+          {
+              "bgColor": "",
+              "directoryId": 498,
+              "enhancedImageURI": "https://tiles.cdn.mozilla.net/images/d11ba0b3095bb19d8092cd29be9cbb9e197671ea.28088.png",
+              "imageURI": "https://tiles.cdn.mozilla.net/images/1332a68badf11e3f7f69bf7364e79c0a7e2753bc.5316.png",
+              "title": "Mozilla Community",
+              "type": "affiliate",
+              "url": "http://contribute.mozilla.org/"
+          }
+      ],
+      "enhanced": [
+          {
+              "bgColor": "",
+              "directoryId": 776,
+              "enhancedImageURI": "https://tiles.cdn.mozilla.net/images/44a14fc405cebc299ead86514dff0e3735c8cf65.10814.png",
+              "imageURI": "https://tiles.cdn.mozilla.net/images/20e24aa2219ec7542cc8cf0fd79f0c81e16ebeac.11859.png",
+              "title": "TurboTax",
+              "type": "sponsored",
+              "url": "https://turbotax.intuit.com/"
+          }
+      ],
+      "suggested": [
+          {
+              "bgColor": "#cae1f4",
+              "directoryId": 702,
+              "frecent_sites": [
+                  "addons.mozilla.org",
+                  "air.mozilla.org",
+                  "blog.mozilla.org",
+                  "bugzilla.mozilla.org",
+                  "developer.mozilla.org",
+                  "etherpad.mozilla.org",
+                  "hacks.mozilla.org",
+                  "hg.mozilla.org",
+                  "mozilla.org",
+                  "planet.mozilla.org",
+                  "quality.mozilla.org",
+                  "support.mozilla.org",
+                  "treeherder.mozilla.org",
+                  "wiki.mozilla.org"
+              ],
+              "imageURI": "https://tiles.cdn.mozilla.net/images/9ee2b265678f2775de2e4bf680df600b502e6038.3875.png",
+              "title": "Thanks for testing!",
+              "type": "affiliate",
+              "url": "https://www.mozilla.com/firefox/tiles"
+          }
+      ]
+  }
+
+Link Object
+-----------
+
+Each link object has various values that Firefox uses to display a tile:
+
+- ``url`` - string url for the page to be loaded when the tile is clicked. Only
+  https and http URLs are allowed.
+- ``title`` - string that appears below the tile.
+- ``type`` - string relationship of the link to Mozilla. Expected values:
+  affiliate, organic, sponsored.
+- ``imageURI`` - string url for the tile image to show. Only https and data URIs
+  are allowed.
+- ``enhancedImageURI`` - string url for the image to be shown before the user
+  hovers. Only https and data URIs are allowed.
+- ``bgColor`` - string css color for additional fill background color.
+- ``directoryId`` - id of the tile to be used during ping reporting
+
+Suggested Link Object Extras
+----------------------------
+
+A suggested link has additional values:
+
+- ``frecent_sites`` - array of strings of the sites that can trigger showing a
+  Suggested Tile if the user has the site in one of the top 100 most-frecent
+  pages. Only preapproved array of strings that are hardcoded into the
+  DirectoryLinksProvider module are allowed.
+
+The preapproved arrays follow a policy for determining what topic grouping is
+allowed as well as the composition of a grouping. The topics are broad
+uncontroversial categories, e.g., Mobile Phone, News, Technology, Video Game,
+Web Development. There are at least 5 sites within a grouping, and as many
+popular sites relevant to the topic are included to avoid having one site be
+clearly dominant. These requirements provide some deniability of which site
+actually triggered a suggestion during ping reporting, so it's more difficult to
+determine if a user has gone to a specific site.
+
+
+Ping JSON Format
+================
+
+Firefox reports back an action and the state of tiles on the new tab page based
+on the user opening a new tab or clicking a tile. The top level keys of the
+ping:
+
+- ``locale`` - string locale of the Firefox build
+- ``tiles`` - array of tiles ping objects
+
+An additional key at the top level indicates which action triggered the ping.
+The value associated to the action key is the 0-based index into the tiles array
+of which tile triggered the action. Valid actions: block, click, pin, sponsored,
+sponsored_link, unpin, view. E.g., if the second tile is being clicked, the ping
+will have ``"click": 1``
+
+Example
+-------
+
+Below is an example ``click`` ping with 3 tiles: a pinned suggested tile
+followed by a history tile and a directory tile. The first tile is being
+blocked::
+
+  {
+      "locale": "en-US",
+      "tiles": [
+          {
+              "id": 702,
+              "pin": 1,
+          },
+          {},
+          {
+              "id": 498,
+          }
+      ],
+      "block": 0
+  }
+
+Tiles Ping Object
+-----------------
+
+Each tile of the new tab page is reported back as part of the ping with some or
+none of the following optional values:
+
+- ``id`` - id that was provided as part of the downloaded link object (for all
+  types of links: directory, suggested, enhanced); not present if the tile was
+  created from user behavior, e.g., visiting pages
+- ``pinned`` - 1 if the tile is pinned; not present otherwise
+- ``pos`` - integer position if the tile is not in the natural order, e.g., a
+  pinned tile after an empty slot; not present otherwise
+- ``score`` - integer truncated score based on the tile's frecency; not present
+  if 0
+- ``url`` - empty string if it's an enhanced tile; not present otherwise
--- a/browser/docs/index.rst
+++ b/browser/docs/index.rst
@@ -2,9 +2,10 @@
 Firefox
 =======
 
 This is the nascent documentation of the Firefox front-end code.
 
 .. toctree::
    :maxdepth: 1
 
+   DirectoryLinksProvider
    UITelemetry
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -103,17 +103,17 @@ add_contact_button=Add Contact
 ### LOCALIZATION NOTE (valid_email_text_description): This is displayed when
 ### the user enters an invalid email address, preventing the addition of the
 ### contact.
 valid_email_text_description=Please enter a valid email address
 
 ## LOCALIZATION NOTE (add_or_import_contact_title): This is the subtitle of the
 ## panel.
 add_or_import_contact_title=Add or Import Contact
-import_contacts_button=Import
+import_contacts_button2=Import from Google
 importing_contacts_progress_button=Importing…
 import_contacts_failure_message=Some contacts could not be imported. Please try again.
 ## LOCALIZATION NOTE(import_contacts_success_message): Success notification message
 ## when user's contacts have been successfully imported.
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## In this item, don't translate the part between {{..}}
 import_contacts_success_message={{total}} contact was successfully imported.;{{total}} contacts were successfully imported.
@@ -122,19 +122,19 @@ import_contacts_success_message={{total}
 sync_contacts_button=Sync Contacts
 
 ## LOCALIZATION NOTE(import_failed_description simple): Displayed when an import of
 ## contacts fails. This is displayed in the error field.
 import_failed_description_simple=Sorry, contact import failed
 import_failed_description_some=Some contacts could not be imported
 import_failed_support_button=Help
 
-## LOCALIZATION NOTE(remove_contact_menu_button): Displayed in the contact list in
+## LOCALIZATION NOTE(remove_contact_menu_button2): Displayed in the contact list in
 ## a pop-up menu next to the contact's name.
-remove_contact_menu_button=Remove Contact
+remove_contact_menu_button2=Remove Contact…
 ## LOCALIZATION NOTE(confirm_delete_contact_alert): This is an alert that is displayed
 ## to confirm deletion of a contact.
 confirm_delete_contact_alert=Are you sure you want to delete this contact?
 ## LOCALIZATION NOTE(confirm_delete_contact_remove_button, confirm_delete_contact_cancel_button):
 ## These are displayed on the alert with confirm_delete_contact_alert
 confirm_delete_contact_remove_button=Remove Contact
 confirm_delete_contact_cancel_button=Cancel
 
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -45,16 +45,44 @@ const PREF_SELECTED_LOCALE = "general.us
 const PREF_DIRECTORY_SOURCE = "browser.newtabpage.directory.source";
 
 // The preference that tells where to send click/view pings
 const PREF_DIRECTORY_PING = "browser.newtabpage.directory.ping";
 
 // The preference that tells if newtab is enhanced
 const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
 
+// Only allow explicitly approved frecent sites with display name
+const ALLOWED_FRECENT_SITES = new Map([
+  [ 'airdroid.com,android-developers.blogspot.com,android.com,androidandme.com,androidapplications.com,androidapps.com,androidauthority.com,androidcentral.com,androidcommunity.com,androidfilehost.com,androidforums.com,androidguys.com,androidheadlines.com,androidpit.com,androidpolice.com,androidspin.com,androidtapp.com,androinica.com,droid-life.com,droidforums.net,droidviews.com,droidxforums.com,forum.xda-developers.com,phandroid.com,play.google.com,shopandroid.com,talkandroid.com,theandroidsoul.com,thedroidguy.com,videodroid.org',
+    'Technology' ],
+  [ 'assurancewireless.com,att.com,attsavings.com,boostmobile.com,budgetmobile.com,consumercellular.com,credomobile.com,gosmartmobile.com,h2owirelessnow.com,lycamobile.com,lycamobile.us,metropcs.com,myfamilymobile.com,polarmobile.com,qlinkwireless.com,republicwireless.com,sprint.com,straighttalk.com,t-mobile.com,tracfonewireless.com,verizonwireless.com,virginmobile.com,virginmobile.com.au,virginmobileusa.com,vodafone.co.uk,vodafone.com,vzwshop.com',
+    'Mobile Phone' ],
+  [ 'addons.mozilla.org,air.mozilla.org,blog.mozilla.org,bugzilla.mozilla.org,developer.mozilla.org,etherpad.mozilla.org,forums.mozillazine.org,hacks.mozilla.org,hg.mozilla.org,mozilla.org,planet.mozilla.org,quality.mozilla.org,support.mozilla.org,treeherder.mozilla.org,wiki.mozilla.org',
+    'Mozilla' ],
+  [ '3dprint.com,4sysops.com,access-programmers.co.uk,accountingweb.com,addictivetips.com,adweek.com,afterdawn.com,akihabaranews.com,anandtech.com,appsrumors.com,arstechnica.com,belkin.com,besttechinfo.com,betanews.com,bgr.com,botcrawl.com,breakingmuscle.com,canonrumors.com,cheap-phones.com,chip.de,chip.eu,cio.com,citeworld.com,cleanpcremove.com,cnet.com,commentcamarche.net,computer.org,computerhope.com,computershopper.com,computerweekly.com,contextures.com,coolest-gadgets.com,crn.com,csoonline.com,daniweb.com,data.com,datacenterknowledge.com,ddj.com,devicemag.com,digitaltrends.com,dottech.org,dpreview.com,dslreports.com,edugeek.net,eetimes.com,engadget.com,epic.com,eurekalert.org,eweek.com,experts-exchange.com,extremetech.com,fosshub.com,freesoftwaremagazine.com,funkyspacemonkey.com,futuremark.com,gadgetreview.com,ghacks.net,gizmodo.co.uk,gizmodo.com,globalsecurity.org,greenbot.com,gunup.com,guru3d.com,head-fi.org,hexus.net,hothardware.com,howtoforge.com,idg.com.au,idigitaltimes.com,idownloadblog.com,ihackmyi.com,ilounge.com,infomine.com,informationweek.com,intellireview.com,intomobile.com,iphonehacks.com,ismashphone.com,isource.com,it168.com,itechpost.com,itpro.co.uk,itworld.com,jailbreaknation.com,kioskea.net,laptoping.com,laptopmag.com,lightreading.com,livescience.com,malwaretips.com,mediaroom.com,mobilemag.com,modmyi.com,modmymobile.com,mophie.com,mozillazine.org,neoseeker.com,neowin.net,newscientist.com,newsoxy.com,nextadvisor.com,notebookcheck.com,notebookreview.com,nvidia.com,nwc.com,orafaq.com,osdir.com,osxdaily.com,our-hometown.com,pcadvisor.co.uk,pchome.net,pcmag.com,pconline.com.cn,pcpop.com,pcpro.co.uk,pcreview.co.uk,pcrisk.com,pcwelt.de,phonerebel.com,phonescoop.com,physorg.com,pocket-lint.com,post-theory.com,prnewswire.co.uk,prnewswire.com,programming4.us,quickpwn.com,readwrite.com,redmondpie.com,redorbit.com,reviewed.com,safer-networking.org,sciencedaily.com,sciencenews.org,scientificamerican.com,scientificblogging.com,sciverse.com,servicerow.com,sinfuliphone.com,singularityhub.com,slashdot.org,slashgear.com,softonic.com,softonic.com.br,softonic.fr,sophos.com,space.com,sparkfun.com,speedguide.net,stuff.tv,techdailynews.net,techdirt.com,techeblog.com,techhive.com,techie-buzz.com,technewsworld.com,techniqueworld.com,technobuffalo.com,technologyreview.com,technologytell.com,techpowerup.com,techpp.com,techrepublic.com,techshout.com,techweb.com,techworld.com,techworld.com.au,techworldtweets.com,telecomfile.com,tgdaily.com,theinquirer.net,thenextweb.com,theregister.co.uk,thermofisher.com,theverge.com,thewindowsclub.com,tomsguide.com,tomshardware.com,tomsitpro.com,toptenreviews.com,trustedreviews.com,tuaw.com,tweaktown.com,ubergizmo.com,unwiredview.com,venturebeat.com,wccftech.com,webmonkey.com,webpronews.com,windows7codecs.com,windowscentral.com,windowsitpro.com,windowstechies.com,winsupersite.com,wired.co.uk,wired.com,wp-themes.com,xda-developers.com,xml.com,zdnet.com,zmescience.com,zol.com.cn',
+    'Technology' ],
+  [ '9to5mac.com,appadvice.com,apple.com,appleinsider.com,appleturns.com,appsafari.com,cultofmac.com,everymac.com,insanelymac.com,iphoneunlockspot.com,isource.com,itunes.apple.com,lowendmac.com,mac-forums.com,macdailynews.com,macenstein.com,macgasm.net,macintouch.com,maclife.com,macnews.com,macnn.com,macobserver.com,macosx.com,macpaw.com,macrumors.com,macsales.com,macstories.net,macupdate.com,macuser.co.uk,macworld.co.uk,macworld.com,maxiapple.com,spymac.com,theapplelounge.com',
+    'Technology' ],
+  [ 'alistapart.com,answers.microsoft.com,backpack.openbadges.org,blog.chromium.org,caniuse.com,codefirefox.com,codepen.io,css-tricks.com,css3generator.com,cssdeck.com,csswizardry.com,devdocs.io,docs.angularjs.org,ghacks.net,github.com,html5demos.com,html5rocks.com,html5test.com,iojs.org,khanacademy.org,l10n.mozilla.org,learn.jquery.com,marketplace.firefox.com,mozilla-hispano.org,mozillians.org,news.ycombinator.com,npmjs.com,packagecontrol.io,quirksmode.org,readwrite.com,reps.mozilla.org,smashingmagazine.com,stackoverflow.com,status.modern.ie,teamtreehouse.com,tutorialspoint.com,udacity.com,validator.w3.org,w3.org,w3cschool.cc,w3schools.com,whatcanidoformozilla.org',
+    'Web Development' ],
+  [ 'classroom.google.com,codecademy.com,elearning.ut.ac.id,khanacademy.org,learn.jquery.com,teamtreehouse.com,tutorialspoint.com,udacity.com,w3cschool.cc,w3schools.com',
+    'Web Education' ],
+  [ 'abebooks.co.uk,abebooks.com,alibris.com,allaboutcircuits.com,allyoucanbooks.com,answersingenesis.org,artnet.com,audiobooks.com,barnesandnoble.com,barnesandnobleinc.com,bartleby.com,betterworldbooks.com,biggerbooks.com,bncollege.com,bookbyte.com,bookdepository.com,bookfinder.com,bookrenter.com,booksamillion.com,booksite.com,boundless.com,brookstone.com,btol.com,calibre-ebook.com,campusbookrentals.com,casadellibro.com,cbomc.com,cengagebrain.com,chapters.indigo.ca,christianbook.com,ciscopress.com,coursesmart.com,cqpress.com,crafterschoice.com,crossings.com,cshlp.org,deseretbook.com,directtextbook.com,discountmags.com,doubledaybookclub.com,doubledaylargeprint.com,doverpublications.com,ebooks.com,ecampus.com,fellabooks.net,fictionwise.com,flatworldknowledge.com,grolier.com,harpercollins.com,hayhouse.com,historybookclub.com,hpb.com,hpbmarketplace.com,interweave.com,iseeme.com,katiekazoo.com,knetbooks.com,learnoutloud.com,librarything.com,literaryguild.com,lulu.com,lww.com,macmillan.com,magazines.com,mbsdirect.net,militarybookclub.com,mypearsonstore.com,mysteryguild.com,netplaces.com,noble.com,novelguide.com,onespirit.com,oxfordjournals.org,paperbackswap.com,papy.co.jp,peachpit.com,penguin.com,penguingroup.com,pimsleur.com,powells.com,qpb.com,quepublishing.com,reviews.com,rhapsodybookclub.com,rodalestore.com,royalsocietypublishing.org,sagepub.com,scrubsmag.com,sfbc.com,simonandschuster.com,simonandschuster.net,simpletruths.com,teach12.net,textbooks.com,textbookx.com,thegoodcook.com,thriftbooks.com,tlsbooks.com,toshibabookplace.com,tumblebooks.com,urbookdownload.com,valorebooks.com,valuemags.com,wwnorton.com,zoobooks.com',
+    'Literature' ],
+  [ 'aceshowbiz.com,aintitcoolnews.com,askkissy.com,askmen.com,atraf.co.il,audioboom.com,beamly.com,blippitt.com,bollywoodlife.com,bossip.com,buzzlamp.com,celebdirtylaundry.com,celebfocus.com,celebitchy.com,celebrity-gossip.net,celebrityabout.com,celebwild.com,chisms.net,concertboom.com,crushable.com,cultbox.co.uk,dailyentertainmentnews.com,dayscafe.com,deadline.com,deathandtaxesmag.com,diaryofahollywoodstreetking.com,digitalspy.com,egotastic.com,empirenews.net,enelbrasero.com,everydaycelebs.com,ew.com,extratv.com,facade.com,fanaru.com,fhm.com,geektyrant.com,glamourpage.com,heatworld.com,hlntv.com,hollyscoop.com,hollywoodreporter.com,hollywoodtuna.com,hypable.com,infotransfer.net,insideedition.com,interaksyon.com,jezebel.com,justjared.com,justjaredjr.com,komando.com,koreaboo.com,maxgo.com,maxim.com,maxviral.com,mediatakeout.com,mosthappy.com,moviestalk.com,my.ology.com,ngoisao.net,nofilmschool.com,nolocreo.com,octane.tv,ouchpress.com,people.com,peopleenespanol.com,perezhilton.com,pinkisthenewblog.com,platotv.tv,playbill.com,playbillvault.com,playgroundmag.net,popeater.com,popnhop.com,popsugar.co.uk,popsugar.com,purepeople.com,radaronline.com,rantchic.com,reshareworthy.com,rinkworks.com,ripbird.com,sara-freder.com,screenjunkies.com,soapcentral.com,soapoperadigest.com,sobadsogood.com,splitsider.com,starcasm.net,starpulse.com,straightfromthea.com,stupidcelebrities.net,stupiddope.com,tbn.org,theawesomedaily.com,theawl.com,thefrisky.com,thefw.com,theresacaputo.com,thezooom.com,tvnotas.com.mx,twanatells.com,vanswarpedtour.com,vietgiaitri.com,viral.buzz,vulture.com,wakavision.com,worthytales.net,wwtdd.com,younghollywood.com',
+    'Entertainment News' ],
+  [ '247wallst.com,4-traders.com,advfn.com,agweb.com,allbusiness.com,barchart.com,barrons.com,beckershospitalreview.com,benzinga.com,bizjournals.com,bizsugar.com,bloomberg.com,bloomberglaw.com,business-standard.com,businessinsider.com,businessinsider.com.au,businesspundit.com,businessweek.com,businesswire.com,cboe.com,cheatsheet.com,chicagobusiness.com,cjonline.com,cnbc.com,cnnmoney.com,cqrcengage.com,dailyfinance.com,dailyfx.com,dealbreaker.com,djindexes.com,dowjones.com,easierstreetdaily.com,economist.com,economyandmarkets.com,economywatch.com,edweek.org,eleconomista.es,entrepreneur.com,etfdailynews.com,etfdb.com,ewallstreeter.com,fastcolabs.com,fastcompany.com,financeformulas.net,financialpost.com,flife.de,forbes.com,forexpros.com,fortune.com,foxbusiness.com,ft.com,ftpress.com,fx-exchange.com,hbr.org,howdofinance.com,ibtimes.com,inc.com,investopedia.com,investors.com,investorwords.com,journalofaccountancy.com,kiplinger.com,lendingandcredit.net,lfb.org,mainstreet.com,markettraders.com,marketwatch.com,maxkeiser.com,minyanville.com,ml.com,moneycontrol.com,moneymappress.com,moneynews.com,moneysavingexpert.com,morningstar.com,mortgagenewsdaily.com,motleyfool.com,mt.co.kr,nber.org,nyse.com,oilprice.com,pewsocialtrends.org,principal.com,qz.com,rantfinance.com,realclearmarkets.com,recode.net,reuters.ca,reuters.co.in,reuters.co.uk,reuters.com,rttnews.com,seekingalpha.com,smallbiztrends.com,streetinsider.com,thecheapinvestor.com,theeconomiccollapseblog.com,themoneyconverter.com,thestreet.com,tickertech.com,tradingeconomics.com,updown.com,valuewalk.com,wikinvest.com,wsj.com,zacks.com',
+    'Financial News' ],
+  [ '10tv.com,8newsnow.com,9news.com,abc.net.au,abc7.com,abc7chicago.com,abcnews.go.com,aclu.org,activistpost.com,ajc.com,al.com,alan.com,alarab.net,aljazeera.com,americanthinker.com,app.com,aristeguinoticias.com,azcentral.com,baltimoresun.com,becomingminimalist.com,beforeitsnews.com,bigstory.ap.org,blackamericaweb.com,bloomberg.com,bloombergview.com,boston.com,bostonherald.com,breitbart.com,buffalonews.com,c-span.org,canada.com,cbs46.com,cbsnews.com,chicagotribune.com,chron.com,citizensvoice.com,citylab.com,cleveland.com,cnn.com,coed.com,countercurrentnews.com,courant.com,ctvnews.ca,dailyherald.com,dailynews.com,dallasnews.com,delawareonline.com,democratandchronicle.com,democraticunderground.com,democrats.org,denverpost.com,desmoinesregister.com,dispatch.com,elcomercio.pe,english.aljazeera.net,examiner.com,farsnews.com,firstcoastnews.com,firstpost.com,firsttoknow.com,foreignpolicy.com,foxnews.com,freebeacon.com,freep.com,fresnobee.com,gazette.com,global.nytimes.com,heraldtribune.com,hindustantimes.com,hngn.com,humanevents.com,huzlers.com,indiatimes.com,indystar.com,irishtimes.com,jacksonville.com,jpost.com,jsonline.com,kansascity.com,kctv5.com,kentucky.com,kickerdaily.com,king5.com,kmov.com,knoxnews.com,kpho.com,kvue.com,kwqc.com,kxan.com,lainformacion.com,latimes.com,ldnews.com,lex18.com,linternaute.com,livemint.com,lostateminor.com,m24.ru,macleans.ca,manchestereveningnews.co.uk,marinecorpstimes.com,masslive.com,mavikocaeli.com.tr,mcall.com,medium.com,mentalfloss.com,mercurynews.com,metro.us,miamiherald.com,militarytimes.com,mk.ru,mlive.com,mondotimes.com,montrealgazette.com,msnbc.com,msnewsnow.com,mynews13.com,mysanantonio.com,mysuncoast.com,nbclosangeles.com,nbcnewyork.com,nbcphiladelphia.com,ndtv.com,newindianexpress.com,news.cincinnati.com,news.google.com,news.msn.com,news.yahoo.com,news10.net,news8000.com,newsday.com,newsdaymarketing.net,newsen.com,newsmax.com,newsobserver.com,newsok.com,newsru.ua,newstatesman.com,newszoom.com,nj.com,nola.com,northjersey.com,nouvelobs.com,npr.org,nwfdailynews.com,nwitimes.com,nydailynews.com,nytimes.com,observer.com,ocregister.com,okcfox.com,omaha.com,onenewspage.com,ontheissues.org,oregonlive.com,orlandosentinel.com,palmbeachpost.com,pe.com,pennlive.com,philly.com,pilotonline.com,polar.com,post-gazette.com,postandcourier.com,presstelegram.com,presstv.ir,propublica.org,providencejournal.com,realclearpolitics.com,recorderonline.com,reporterdock.com,reporterherald.com,respublica.al,reuters.com,rg.ru,roanoke.com,sacbee.com,scmp.com,scnow.com,sdpnoticias.com,seattletimes.com,semana.com,sfgate.com,sharepowered.com,sinembargo.mx,slate.com,sltrib.com,sotomayortv.com,sourcewatch.org,spectator.co.uk,squaremirror.com,star-telegram.com,staradvertiser.com,startribune.com,statesman.com,stltoday.com,streetwise.co,stuff.co.nz,success.com,suffolknewsherald.com,sun-sentinel.com,sunnewsnetwork.ca,suntimes.com,supernewschannel.com,surenews.com,svoboda.org,syracuse.com,tampabay.com,tbd.com,telegram.com,telegraph.co.uk,tennessean.com,the-open-mind.com,theadvocate.com,theage.com.au,theatlantic.com,thebarefootwriter.com,theblaze.com,thecalifornian.com,thedailysheeple.com,thefix.com,theintelligencer.net,thelocal.com,thenational.ae,thenewstribune.com,theparisreview.org,thereporter.com,therepublic.com,thestar.com,thetelegram.com,thetimes.co.uk,theuspatriot.com,time.com,timescall.com,timesdispatch.com,timesleaderonline.com,timesofisrael.com,toledoblade.com,toprightnews.com,townhall.com,tpnn.com,trendolizer.com,triblive.com,tribune.com.pk,tricities.com,troymessenger.com,trueactivist.com,truthandaction.org,tsn.ua,tulsaworld.com,twincities.com,upi.com,usatoday.com,utsandiego.com,vagazette.com,viralwomen.com,vitalworldnews.com,voasomali.com,vox.com,washingtonexaminer.com,washingtonpost.com,watchdog.org,wave3.com,wavy.com,wbay.com,wbtw.com,wcpo.com,wctrib.com,wdtn.com,weeklystandard.com,westernjournalism.com,wfsb.com,wgrz.com,whas11.com,winonadailynews.com,wishtv.com,wistv.com,wkbn.com,wkow.com,wlfi.com,wmtw.com,wmur.com,wopular.com,world-top-news.com,worldnews.com,wplol.us,wpsdlocal6.com,wptz.com,wric.com,wsmv.com,wthitv.com,wthr.com,wtnh.com,wtol.com,wtsp.com,wvec.com,wwlp.com,wwltv.com,wyff4.com,yonhapnews.co.kr,yourbreakingnews.com',
+    'News' ],
+  [ '2k.com,360game.vn,4399.com,a10.com,activision.com,addictinggames.com,alawar.com,alienwarearena.com,anagrammer.com,andkon.com,aq.com,arcadeprehacks.com,arcadeyum.com,arcgames.com,archeagegame.com,armorgames.com,askmrrobot.com,battle.net,battlefieldheroes.com,bigfishgames.com,bigpoint.com,bioware.com,bluesnews.com,boardgamegeek.com,bollyheaven.com,bubblebox.com,bukkit.org,bungie.net,buycraft.net,callofduty.com,candystand.com,cda.pl,challonge.com,championselect.net,cheapassgamer.com,cheatcc.com,cheatengine.org,cheathappens.com,chess.com,civfanatics.com,clashofclans-tools.com,clashofclansbuilder.com,comdotgame.com,commonsensemedia.org,coolrom.com,crazygames.com,csgolounge.com,curse.com,d20pfsrd.com,destructoid.com,diablofans.com,diablowiki.net,didigames.com,dota2.com,dota2lounge.com,dressupgames.com,dulfy.net,ebog.com,elderscrollsonline.com,elitedangerous.com,elitepvpers.com,emuparadise.me,enjoydressup.com,escapegames24.com,escapistmagazine.com,eventhubs.com,eveonline.com,farming-simulator.com,feed-the-beast.com,flashgames247.com,flightrising.com,flipline.com,flonga.com,freegames.ws,freeonlinegames.com,fresh-hotel.org,friv.com,friv.today,fullypcgames.net,funny-games.biz,funtrivia.com,futhead.com,g2a.com,gamasutra.com,game-debate.com,game-oldies.com,game321.com,gamebaby.com,gamebaby.net,gamebanana.com,gamefaqs.com,gamefly.com,gamefront.com,gamegape.com,gamehouse.com,gameinformer.com,gamejolt.com,gamemazing.com,gamemeteor.com,gamerankings.com,gamersgate.com,games-msn.com,games-workshop.com,games.com,games2girls.com,gamesbox.com,gamesfreak.net,gametop.com,gametracker.com,gametrailers.com,gamezhero.com,gbatemp.net,geforce.com,gematsu.com,giantbomb.com,girl.me,girlsgames123.com,girlsplay.com,gog.com,gogames.me,gonintendo.com,goodgamestudios.com,gosugamers.net,greenmangaming.com,gtaforums.com,gtainside.com,guildwars2.com,hackedarcadegames.com,hearthpwn.com,hirezstudios.com,hitbox.tv,hltv.org,howrse.com,icy-veins.com,indiedb.com,jayisgames.com,jigzone.com,joystiq.com,juegosdechicas.com,kabam.com,kbhgames.com,kerbalspaceprogram.com,king.com,kixeye.com,kizi.com,kogama.com,kongregate.com,kotaku.com,lolcounter.com,lolking.net,lolnexus.com,lolpro.com,lolskill.net,lootcrate.com,lumosity.com,mafa.com,mangafox.me,mangapark.com,mariowiki.com,maxgames.com,megagames.com,metacritic.com,mindjolt.com,minecraft.net,minecraftforum.net,minecraftservers.org,minecraftskins.com,mineplex.com,miniclip.com,mmo-champion.com,mmobomb.com,mmohuts.com,mmorpg.com,mmosite.com,mobafire.com,moddb.com,modxvm.com,mojang.com,moshimonsters.com,mousebreaker.com,moviestarplanet.com,mtgsalvation.com,muchgames.com,myonlinearcade.com,myplaycity.com,myrealgames.com,mythicspoiler.com,n4g.com,newgrounds.com,nexon.net,nexusmods.com,ninjakiwi.com,nintendo.com,nintendoeverything.com,nintendolife.com,nitrome.com,nosteam.ro,notdoppler.com,noxxic.com,operationsports.com,origin.com,ownedcore.com,pacogames.com,pathofexile.com,pcgamer.com,pch.com,pcsx2.net,penny-arcade.com,planetminecraft.com,plarium.com,playdota.com,playpink.com,playsides.com,playstationlifestyle.net,playstationtrophies.org,pog.com,pokemon.com,polygon.com,popcap.com,primarygames.com,probuilds.net,ps3hax.net,psnprofiles.com,psu.com,qq.com,r2games.com,resourcepack.net,retrogamer.com,rewardtv.com,riotgames.com,robertsspaceindustries.com,roblox.com,robocraftgame.com,rockpapershotgun.com,rockstargames.com,roosterteeth.com,runescape.com,schoolofdragons.com,screwattack.com,scufgaming.com,segmentnext.com,shacknews.com,shockwave.com,shoryuken.com,siliconera.com,silvergames.com,skydaz.com,smashbros.com,solomid.net,starcitygames.com,starsue.net,steamcommunity.com,steamgifts.com,strategywiki.org,supercheats.com,surrenderat20.net,swtor.com,tankionline.com,tcgplayer.com,teamfortress.com,teamliquid.net,tetrisfriends.com,thesims3.com,thesimsresource.com,thetechgame.com,topg.org,totaljerkface.com,toucharcade.com,transformice.com,trueachievements.com,twcenter.net,twitch.tv,twoplayergames.org,unity3d.com,vg247.com,vgchartz.com,videogamesblogger.com,warframe.com,warlight.net,warthunder.com,watchcartoononline.com,websudoku.com,wildstar-online.com,wildtangent.com,wineverygame.com,wizards.com,worldofsolitaire.com,worldoftanks.com,wowhead.com,wowprogress.com,wowwiki.com,xbox.com,xbox360iso.com,xboxachievements.com,xfire.com,xtremetop100.com,y8.com,yoyogames.com,zybez.net,zynga.com',
+    'Video Game' ],
+]);
+
 // Only allow link urls that are http(s)
 const ALLOWED_LINK_SCHEMES = new Set(["http", "https"]);
 
 // Only allow link image urls that are https or data
 const ALLOWED_IMAGE_SCHEMES = new Set(["https", "data"]);
 
 // The frecency of a directory link
 const DIRECTORY_FRECENCY = 1000;
@@ -117,21 +145,16 @@ let DirectoryLinksProvider = {
     matchOSLocale: PREF_MATCH_OS_LOCALE,
     prefSelectedLocale: PREF_SELECTED_LOCALE,
   }),
 
   get _linksURL() {
     if (!this.__linksURL) {
       try {
         this.__linksURL = Services.prefs.getCharPref(this._observedPrefs["linksURL"]);
-
-        // Temporarily override the default for en-US until new endpoint is live
-        if (this.locale == "en-US" && !Services.prefs.prefHasUserValue(this._observedPrefs["linksURL"])) {
-          this.__linksURL = "data:text/plain;base64,ewogICAgImRpcmVjdG9yeSI6IFsKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDQ5OCwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy9kMTFiYTBiMzA5NWJiMTlkODA5MmNkMjliZTljYmI5ZTE5NzY3MWVhLjI4MDg4LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzEzMzJhNjhiYWRmMTFlM2Y3ZjY5YmY3MzY0ZTc5YzBhN2UyNzUzYmMuNTMxNi5wbmciLAogICAgICAgICAgICAidGl0bGUiOiAiTW96aWxsYSBDb21tdW5pdHkiLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHA6Ly9jb250cmlidXRlLm1vemlsbGEub3JnLyIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICAgImJnQ29sb3IiOiAiIiwKICAgICAgICAgICAgImRpcmVjdG9yeUlkIjogNTAwLAogICAgICAgICAgICAiZW5oYW5jZWRJbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzL2NjNjM3NzRiN2E5YWFlMDJmZTM2YmM1Y2FmOTBjMWUyNWU2NmEyYmMuMTM3OTEucG5nIiwKICAgICAgICAgICAgImltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvZTgyMmNkNDYyOGM1MTYyMzEzZjQ5ZjVkNDU1NmY4YWFmZGYzODc1MC4xMTUxMy5wbmciLAogICAgICAgICAgICAidGl0bGUiOiAiTW96aWxsYSBNYW5pZmVzdG8iLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHBzOi8vd3d3Lm1vemlsbGEub3JnL2Fib3V0L21hbmlmZXN0by8iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwMiwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy80MGU1NjMwNDA1ZDUwMzFjYTczMzkzYmQ3YmMwMDY0MTU2ZjJjYzgyLjEwOTg0LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzQ5MGQ0MmQxZjlhNzZjMDc3Mzk2MjZkMWI4YTU2OTE2OWFlYzhmYmUuMTEwMzkucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkN1c3RvbWl6ZSBGaXJlZm94IiwKICAgICAgICAgICAgInR5cGUiOiAiYWZmaWxpYXRlIiwKICAgICAgICAgICAgInVybCI6ICJodHRwOi8vZmFzdGVzdGZpcmVmb3guY29tL2ZpcmVmb3gvZGVza3RvcC9jdXN0b21pemUvIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAiYmdDb2xvciI6ICIiLAogICAgICAgICAgICAiZGlyZWN0b3J5SWQiOiA1MDQsCiAgICAgICAgICAgICJlbmhhbmNlZEltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvODc3ZjFjNTYxZTczNWY3YjlmNDE5ZmY5YWM3OWViOGM3NDgxMTE5ZC4xNjc0NC5wbmciLAogICAgICAgICAgICAiaW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy8yNWM5ZmJiMDczMDhiODRkMTYwZmMxYjc5NTkzNjRhMmMxOGY5M2I5LjY0MDQucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkZpcmVmb3ggTWFya2V0cGxhY2UiLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHBzOi8vbWFya2V0cGxhY2UuZmlyZWZveC5jb20vIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgICAiYmdDb2xvciI6ICIjM2ZiNThlIiwKICAgICAgICAgICAgImRpcmVjdG9yeUlkIjogNTA1LAogICAgICAgICAgICAiZW5oYW5jZWRJbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzcyMDEyMWU3NDYyZDhjNzg2M2I0ZGQ4ZmE3YjVjMTA4OWI1ZjVmYjIuMzM4NjIucG5nIiwKICAgICAgICAgICAgImltYWdlVVJJIjogImh0dHBzOi8vZHRleDRrdmJwcG92dC5jbG91ZGZyb250Lm5ldC9pbWFnZXMvMGU2MDMxNjc1YTljNDkxZGQwYzY1ZTljNjdjZmJmNTRhNTg4MGYxNy4yMjk1LnN2ZyIsCiAgICAgICAgICAgICJ0aXRsZSI6ICJNb3ppbGxhIFdlYm1ha2VyIiwKICAgICAgICAgICAgInR5cGUiOiAiYWZmaWxpYXRlIiwKICAgICAgICAgICAgInVybCI6ICJodHRwczovL3dlYm1ha2VyLm9yZy8%2FdXRtX3NvdXJjZT1kaXJlY3RvcnktdGlsZXMmdXRtX21lZGl1bT1maXJlZm94LWJyb3dzZXIiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwNiwKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy9kOTcxY2JhZmEwMzA5YTIwMWU1MThhY2RhYzRmMWVlNGRhYmM3ZWFhLjE1MTA5LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzL2I0YWRjNThkZDNjMDJkYTM1NTEwNDk3N2I5MTAyNTUwNjBjZmQ2ZDguMTAzNTAucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIkZpcmVmb3ggU3luYyIsCiAgICAgICAgICAgICJ0eXBlIjogImFmZmlsaWF0ZSIsCiAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovL21vemlsbGEtZXVyb3BlLm9yZy9maXJlZm94L3N5bmMiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiIsCiAgICAgICAgICAgICJkaXJlY3RvcnlJZCI6IDUwNywKICAgICAgICAgICAgImVuaGFuY2VkSW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy8yMmZiODU2Y2Q1ODM2NTg1NWViNzI1YjE1NjVmMDhhNzI0NjRlMDM5LjE4NzE3LnBuZyIsCiAgICAgICAgICAgICJpbWFnZVVSSSI6ICJodHRwczovL2R0ZXg0a3ZicHBvdnQuY2xvdWRmcm9udC5uZXQvaW1hZ2VzLzA2OGUwY2NiZDg3MDFhMjhlMmYwNzhjNjQwZWUwNzJiOWExNmUyZTEuMTI0OTAucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIlByaXZhY3kgUHJpbmNpcGxlcyIsCiAgICAgICAgICAgICJ0eXBlIjogImFmZmlsaWF0ZSIsCiAgICAgICAgICAgICJ1cmwiOiAiaHR0cDovL2V1cm9wZS5tb3ppbGxhLm9yZy9wcml2YWN5L3lvdSIKICAgICAgICB9CiAgICBdLAogICAgInN1Z2dlc3RlZCI6IFsKICAgICAgICB7CiAgICAgICAgICAgICJiZ0NvbG9yIjogIiNjYWUxZjQiLAogICAgICAgICAgICAiZGlyZWN0b3J5SWQiOiA3MDIsCiAgICAgICAgICAgICJmcmVjZW50X3NpdGVzIjogWwogICAgICAgICAgICAgICAgImFkZG9ucy5tb3ppbGxhLm9yZyIsCiAgICAgICAgICAgICAgICAiYWlyLm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJibG9nLm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJidWd6aWxsYS5tb3ppbGxhLm9yZyIsCiAgICAgICAgICAgICAgICAiZGV2ZWxvcGVyLm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJldGhlcnBhZC5tb3ppbGxhLm9yZyIsCiAgICAgICAgICAgICAgICAiaGFja3MubW96aWxsYS5vcmciLAogICAgICAgICAgICAgICAgImhnLm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJtb3ppbGxhLm9yZyIsCiAgICAgICAgICAgICAgICAicGxhbmV0Lm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJxdWFsaXR5Lm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJzdXBwb3J0Lm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJzdXBwb3J0Lm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJ0cmVlaGVyZGVyLm1vemlsbGEub3JnIiwKICAgICAgICAgICAgICAgICJ3aWtpLm1vemlsbGEub3JnIgogICAgICAgICAgICBdLAogICAgICAgICAgICAiaW1hZ2VVUkkiOiAiaHR0cHM6Ly9kdGV4NGt2YnBwb3Z0LmNsb3VkZnJvbnQubmV0L2ltYWdlcy85ZWUyYjI2NTY3OGYyNzc1ZGUyZTRiZjY4MGRmNjAwYjUwMmU2MDM4LjM4NzUucG5nIiwKICAgICAgICAgICAgInRpdGxlIjogIlRoYW5rcyBmb3IgdGVzdGluZyEiLAogICAgICAgICAgICAidHlwZSI6ICJhZmZpbGlhdGUiLAogICAgICAgICAgICAidXJsIjogImh0dHBzOi8vd3d3Lm1vemlsbGEuY29tL2ZpcmVmb3gvdGlsZXMiCiAgICAgICAgfQogICAgXQp9Cg%3D%3D";
-        }
       }
       catch (e) {
         Cu.reportError("Error fetching directory links url from prefs: " + e);
       }
     }
     return this.__linksURL;
   },
 
@@ -427,16 +450,24 @@ let DirectoryLinksProvider = {
    */
   getEnhancedLink: function DirectoryLinksProvider_getEnhancedLink(link) {
     // Use the provided link if it's already enhanced
     return link.enhancedImageURI && link ? link :
            this._enhancedLinks.get(NewTabUtils.extractSite(link.url));
   },
 
   /**
+   * Get the display name of an allowed frecent sites. Returns undefined for a
+   * unallowed frecent sites.
+   */
+  getFrecentSitesName(sites) {
+    return ALLOWED_FRECENT_SITES.get(sites.join(","));
+  },
+
+  /**
    * Check if a url's scheme is in a Set of allowed schemes
    */
   isURLAllowed: function DirectoryLinksProvider_isURLAllowed(url, allowed) {
     // Assume no url is an allowed url
     if (!url) {
       return true;
     }
 
@@ -463,16 +494,23 @@ let DirectoryLinksProvider = {
       let validityFilter = function(link) {
         // Make sure the link url is allowed and images too if they exist
         return this.isURLAllowed(link.url, ALLOWED_LINK_SCHEMES) &&
                this.isURLAllowed(link.imageURI, ALLOWED_IMAGE_SCHEMES) &&
                this.isURLAllowed(link.enhancedImageURI, ALLOWED_IMAGE_SCHEMES);
       }.bind(this);
 
       rawLinks.suggested.filter(validityFilter).forEach((link, position) => {
+        // Only allow suggested links with approved frecent sites
+        let name = this.getFrecentSitesName(link.frecent_sites);
+        if (name == undefined) {
+          return;
+        }
+
+        link.targetedName = name;
         link.lastVisitDate = rawLinks.suggested.length - position;
 
         // We cache suggested tiles here but do not push any of them in the links list yet.
         // The decision for which suggested tile to include will be made separately.
         this._cacheSuggestedLinks(link);
         this._frequencyCaps.set(link.url, DEFAULT_FREQUENCY_CAP);
       });
 
--- a/browser/modules/ReaderParent.jsm
+++ b/browser/modules/ReaderParent.jsm
@@ -17,16 +17,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm");
 
 const gStringBundle = Services.strings.createBundle("chrome://global/locale/aboutReader.properties");
 
 let ReaderParent = {
+  _readerModeInfoPanelOpen: false,
 
   MESSAGES: [
     "Reader:AddToList",
     "Reader:AddToPocket",
     "Reader:ArticleGet",
     "Reader:FaviconRequest",
     "Reader:ListStatusRequest",
     "Reader:PocketEnabledGet",
@@ -182,16 +183,30 @@ let ReaderParent = {
       button.removeAttribute("readeractive");
       button.hidden = !browser.isArticle;
       let enterText = gStringBundle.GetStringFromName("readerView.enter");
       button.setAttribute("tooltiptext", enterText);
       command.setAttribute("label", enterText);
       command.setAttribute("hidden", !browser.isArticle);
       command.setAttribute("accesskey", gStringBundle.GetStringFromName("readerView.enter.accesskey"));
     }
+
+    let currentUriHost = browser.currentURI && browser.currentURI.asciiHost;
+    if (browser.isArticle &&
+        !Services.prefs.getBoolPref("browser.reader.detectedFirstArticle") &&
+        currentUriHost && !currentUriHost.endsWith("mozilla.org")) {
+      this.showReaderModeInfoPanel(browser);
+      Services.prefs.setBoolPref("browser.reader.detectedFirstArticle", true);
+      this._readerModeInfoPanelOpen = true;
+    } else if (this._readerModeInfoPanelOpen) {
+      if (UITour.isInfoOnTarget(win, "readerMode-urlBar")) {
+        UITour.hideInfo(win);
+      }
+      this._readerModeInfoPanelOpen = false;
+    }
   },
 
   forceShowReaderIcon: function(browser) {
     browser.isArticle = true;
     this.updateReaderButton(browser);
   },
 
   buttonClick(event) {
@@ -223,20 +238,26 @@ let ReaderParent = {
    *
    * @param browser The <browser> that the tour should be started for.
    */
   showReaderModeInfoPanel(browser) {
     let win = browser.ownerDocument.defaultView;
     let targetPromise = UITour.getTarget(win, "readerMode-urlBar");
     targetPromise.then(target => {
       let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+      let icon = "chrome://browser/skin/";
+      if (win.devicePixelRatio > 1) {
+        icon += "reader-tour@2x.png";
+      } else {
+        icon += "reader-tour.png";
+      }
       UITour.showInfo(win, browser.messageManager, target,
-                      browserBundle.GetStringFromName("readerView.promo.firstDetectedArticle.title"),
-                      browserBundle.GetStringFromName("readerView.promo.firstDetectedArticle.body"),
-                      "chrome://browser/skin/reader-tour.png");
+                      browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.title"),
+                      browserBundle.GetStringFromName("readingList.promo.firstUse.readerView.body"),
+                      icon);
     });
   },
 
   /**
    * Gets an article for a given URL. This method will download and parse a document.
    *
    * @param url The article URL.
    * @param browser The browser where the article is currently loaded.
--- a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
+++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js
@@ -264,16 +264,19 @@ add_task(function test_updateSuggestedTi
 
   // Initial setup
   let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3], "directory": [someOtherSite]};
   let dataURI = 'data:application/json,' + JSON.stringify(data);
 
   let testObserver = new TestFirstRun();
   DirectoryLinksProvider.addObserver(testObserver);
 
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
+
   yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
   let links = yield fetchData();
 
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = function(site) {
     return topSites.indexOf(site) >= 0;
   }
 
@@ -378,16 +381,17 @@ add_task(function test_updateSuggestedTi
   topSites = [];
   testObserver = new TestRemovingSuggestedTile();
   DirectoryLinksProvider.addObserver(testObserver);
   DirectoryLinksProvider.onManyLinksChanged();
   yield testObserver.promise;
 
   // Cleanup
   yield promiseCleanDirectoryLinksProvider();
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   NewTabUtils.getProviderLinks = origGetProviderLinks;
   DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount;
 });
 
 add_task(function test_suggestedLinksMap() {
   let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]};
   let dataURI = 'data:application/json,' + JSON.stringify(data);
@@ -416,16 +420,19 @@ add_task(function test_suggestedLinksMap
       isIdentical(suggestedLinksItr.next().value, link);
     }
   })
 
   yield promiseCleanDirectoryLinksProvider();
 });
 
 add_task(function test_topSitesWithSuggestedLinks() {
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
+
   let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"];
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = function(site) {
     return topSites.indexOf(site) >= 0;
   }
 
   // Mock out getProviderLinks() so we don't have to populate cache in NewTabUtils
   let origGetProviderLinks = NewTabUtils.getProviderLinks;
@@ -463,28 +470,29 @@ add_task(function test_topSitesWithSugge
 
   // Re-adding freetaxusa.com as a topsite will add it to _topSitesWithSuggestedLinks.
   topSites.push(popped);
   expectedTopSitesWithSuggestedLinks.push(popped);
   DirectoryLinksProvider._handleLinkChanged({url: "http://" + popped});
   isIdentical([...DirectoryLinksProvider._topSitesWithSuggestedLinks], expectedTopSitesWithSuggestedLinks);
 
   // Cleanup.
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   NewTabUtils.getProviderLinks = origGetProviderLinks;
 });
 
 add_task(function test_suggestedAttributes() {
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
 
   let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount;
   DirectoryLinksProvider._getCurrentTopSiteCount = () => 8;
 
-  let frecent_sites = ["top.site.com"];
+  let frecent_sites = "addons.mozilla.org,air.mozilla.org,blog.mozilla.org,bugzilla.mozilla.org,developer.mozilla.org,etherpad.mozilla.org,forums.mozillazine.org,hacks.mozilla.org,hg.mozilla.org,mozilla.org,planet.mozilla.org,quality.mozilla.org,support.mozilla.org,treeherder.mozilla.org,wiki.mozilla.org".split(",");
   let imageURI = "https://image/";
   let title = "the title";
   let type = "affiliate";
   let url = "http://test.url/";
   let data = {
     suggested: [{
       frecent_sites,
       imageURI,
@@ -509,30 +517,33 @@ add_task(function test_suggestedAttribut
         resolve();
       }
     });
   });
 
   // Make sure we get the expected attributes on the suggested tile
   let link = gLinks.getLinks()[0];
   do_check_eq(link.imageURI, imageURI);
+  do_check_eq(link.targetedName, "Mozilla");
   do_check_eq(link.targetedSite, frecent_sites[0]);
   do_check_eq(link.title, title);
   do_check_eq(link.type, type);
   do_check_eq(link.url, url);
 
   // Cleanup.
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount;
   gLinks.removeProvider(DirectoryLinksProvider);
   DirectoryLinksProvider.removeObserver(gLinks);
 });
 
 add_task(function test_frequencyCappedSites_views() {
   Services.prefs.setCharPref(kPingUrlPref, "");
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
 
   let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount;
   DirectoryLinksProvider._getCurrentTopSiteCount = () => 8;
 
   let testUrl = "http://frequency.capped/link";
   let targets = ["top.site.com"];
@@ -589,25 +600,28 @@ add_task(function test_frequencyCappedSi
   synthesizeAction("view");
   checkFirstTypeAndLength("affiliate", 2);
   synthesizeAction("view");
   checkFirstTypeAndLength("affiliate", 2);
   synthesizeAction("view");
   checkFirstTypeAndLength("organic", 1);
 
   // Cleanup.
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount;
   gLinks.removeProvider(DirectoryLinksProvider);
   DirectoryLinksProvider.removeObserver(gLinks);
   Services.prefs.setCharPref(kPingUrlPref, kPingUrl);
 });
 
 add_task(function test_frequencyCappedSites_click() {
   Services.prefs.setCharPref(kPingUrlPref, "");
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
 
   let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount;
   DirectoryLinksProvider._getCurrentTopSiteCount = () => 8;
 
   let testUrl = "http://frequency.capped/link";
   let targets = ["top.site.com"];
@@ -658,16 +672,17 @@ add_task(function test_frequencyCappedSi
   // Make sure the link disappears after the first click
   checkFirstTypeAndLength("affiliate", 2);
   synthesizeAction("view");
   checkFirstTypeAndLength("affiliate", 2);
   synthesizeAction("click");
   checkFirstTypeAndLength("organic", 1);
 
   // Cleanup.
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount;
   gLinks.removeProvider(DirectoryLinksProvider);
   DirectoryLinksProvider.removeObserver(gLinks);
   Services.prefs.setCharPref(kPingUrlPref, kPingUrl);
 });
 
 add_task(function test_reportSitesAction() {
@@ -1134,16 +1149,18 @@ add_task(function test_DirectoryLinksPro
   yield promiseSetupDirectoryLinksProvider({linksURL: dataURI});
   links = yield fetchData();
   do_check_eq(links.length, 0); // There are no directory links.
   checkEnhanced("http://example.net", undefined);
   checkEnhanced("http://example.com", "data:,fresh");
 });
 
 add_task(function test_DirectoryLinksProvider_enhancedURIs() {
+  let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName;
+  DirectoryLinksProvider.getFrecentSitesName = () => "";
   let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
   NewTabUtils.isTopPlacesSite = () => true;
   let origCurrentTopSiteCount = DirectoryLinksProvider._getCurrentTopSiteCount;
   DirectoryLinksProvider._getCurrentTopSiteCount = () => 8;
 
   let data = {
     "suggested": [
       {url: "http://example.net", enhancedImageURI: "data:,net1", title:"SuggestedTitle", frecent_sites: ["test.com"]}
@@ -1177,16 +1194,17 @@ add_task(function test_DirectoryLinksPro
 
   // Check that the suggested tile with the same URL replaces the directory tile.
   links = gLinks.getLinks();
   do_check_eq(links.length, 1);
   do_check_eq(links[0].title, "SuggestedTitle");
   do_check_eq(links[0].enhancedImageURI, "data:,net1");
 
   // Cleanup.
+  DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName;
   NewTabUtils.isTopPlacesSite = origIsTopPlacesSite;
   DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount;
   gLinks.removeProvider(DirectoryLinksProvider);
 });
 
 add_task(function test_DirectoryLinksProvider_setDefaultEnhanced() {
   function checkDefault(expected) {
     Services.prefs.clearUserPref(kNewtabEnhancedPref);
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -89,16 +89,17 @@ browser.jar:
   skin/classic/browser/Toolbar-small.png
   skin/classic/browser/undoCloseTab.png                        (../shared/undoCloseTab.png)
   skin/classic/browser/update-badge.svg                        (../shared/update-badge.svg)
   skin/classic/browser/urlbar-arrow.png
   skin/classic/browser/session-restore.svg                  (../shared/incontent-icons/session-restore.svg)
   skin/classic/browser/tab-crashed.svg                      (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/welcome-back.svg                     (../shared/incontent-icons/welcome-back.svg)
   skin/classic/browser/reader-tour.png                      (../shared/reader/reader-tour.png)
+  skin/classic/browser/reader-tour@2x.png                   (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                       (../shared/reader/readerMode.svg)
   skin/classic/browser/readinglist/icons.svg          (../shared/readinglist/icons.svg)
   skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
 * skin/classic/browser/readinglist/sidebar.css        (readinglist/sidebar.css)
   skin/classic/browser/webRTC-shareDevice-16.png
   skin/classic/browser/webRTC-shareDevice-64.png
   skin/classic/browser/webRTC-sharingDevice-16.png    (../shared/webrtc/webRTC-sharingDevice-16.png)
   skin/classic/browser/webRTC-shareMicrophone-16.png
--- a/browser/themes/linux/preferences/in-content/preferences.css
+++ b/browser/themes/linux/preferences/in-content/preferences.css
@@ -1,18 +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/. */
 
 %include ../../../shared/incontentprefs/preferences.inc.css
 
-spinbuttons {
-  -moz-appearance: none;
-}
-
 .treecol-sortdirection {
   /* override the Linux only toolkit rule */
   -moz-appearance: none;
 }
 
 .actionsMenu {
   font-family: "Clear Sans", sans-serif;
   font-size: 1.25rem;
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -141,16 +141,17 @@ browser.jar:
   skin/classic/browser/urlbar-arrow.png
   skin/classic/browser/urlbar-arrow@2x.png
   skin/classic/browser/urlbar-popup-blocked.png
   skin/classic/browser/urlbar-popup-blocked@2x.png
   skin/classic/browser/session-restore.svg            (../shared/incontent-icons/session-restore.svg)
   skin/classic/browser/tab-crashed.svg                (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/welcome-back.svg               (../shared/incontent-icons/welcome-back.svg)
   skin/classic/browser/reader-tour.png                (../shared/reader/reader-tour.png)
+  skin/classic/browser/reader-tour@2x.png             (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                 (../shared/reader/readerMode.svg)
   skin/classic/browser/readinglist/icons.svg          (../shared/readinglist/icons.svg)
   skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
 * skin/classic/browser/readinglist/sidebar.css        (readinglist/sidebar.css)
   skin/classic/browser/webRTC-shareDevice-16.png
   skin/classic/browser/webRTC-shareDevice-16@2x.png
   skin/classic/browser/webRTC-shareDevice-64.png
   skin/classic/browser/webRTC-shareDevice-64@2x.png
--- a/browser/themes/osx/preferences/in-content/preferences.css
+++ b/browser/themes/osx/preferences/in-content/preferences.css
@@ -4,39 +4,16 @@
 
 %include ../../../shared/incontentprefs/preferences.inc.css
 
 prefpane .groupbox-title {
   background: none;
   margin-bottom: 0;
 }
 
-spinbuttons {
-  -moz-appearance: none;
-}
-
-.spinbuttons-up {
-  margin-top: 0 !important;
-  border-radius: 4px 4px 0 0;
-}
-
-.spinbuttons-down  {
-  margin-bottom: 0 !important;
-  border-radius: 0 0 4px 4px;
-}
-
-.spinbuttons-button > .button-box {
-  -moz-padding-start: 2px !important;
-  -moz-padding-end: 3px !important;
-}
-
-.spinbuttons-button > .button-box > .button-text {
-  display: none;
-}
-
 .actionsMenu > .menulist-label-box > .menulist-icon {
   margin-top: 2px;
   -moz-margin-start: 2px;
   -moz-margin-end: 8px !important;
 }
 
 #downloadFolder > .fileFieldContentBox {
   -moz-padding-start: 3px;
--- a/browser/themes/shared/devtools/toolbars.inc.css
+++ b/browser/themes/shared/devtools/toolbars.inc.css
@@ -873,19 +873,22 @@
   background-color: hsla(206,37%,4%,.4);
   color: var(--theme-selection-color);
 }
 
 .theme-light .devtools-tab:hover:active {
   background-color: rgba(170,170,170,.4);
 }
 
+.devtools-tab:not([selected])[highlighted] {
+  box-shadow: 0 2px 0 var(--theme-highlight-green) inset;
+}
+
 .theme-dark .devtools-tab:not([selected])[highlighted] {
   background-color: hsla(99,100%,14%,.2);
-  box-shadow: 0 2px 0 #7bc107 inset;
 }
 
 .theme-light .devtools-tab:not([selected])[highlighted] {
   background-color: rgba(44, 187, 15, .2);
 }
 
 .devtools-tab > image {
   border: none;
index 8e557e03821219211c19f65a90ca03d2f0b3b03f..be346b3847928f94d7a0bbca019f40d86edd0899
GIT binary patch
literal 2672
zc$@)n3Xk=PP)<h;3K|Lk000e1NJLTq001xm001xu1^@s6R|5Hm00001b5ch_0Itp)
z=>Px<DoI2^RA}DST6<6wM;aec9%2%ZND^|yXf9g0x~jXY%9Z|eRhPOyEYCYjUh=S1
zw2Ve`M)CA=CKrPdPCb=}$chR+5uz4IjG&-s;6MZx78Y2R_dbAifd%%(zGi0ox@UXV
zWnq_H0|~cPKc?yKnfZSGeck=__YFbJ+|L7I9>jbIF9>ftc;U8rxbK|!d19e_Azp)6
z4DmL^5{P9Gzr<LE>rh(|?(2*1_58U6_rZd^1@V5?<*Tvn;(@|}QFZI6j+AMQ)Hq24
znlc0PxQW&&w1z>MQq$JiCoj&uU9g?-@p&KK7YyN#pXK>e3GR(wy5wr!&27EUhbwg?
zZPb{|B!Ae@{k=$AdO@JdIrvm@Vr9N`*RS)$AMdRkO3G|iUHPt8ThcIW5Gswd(MXv|
z>9De{py>W*@L5YC0%n^CvfWo8esMe{B}}dw?;a=3)HAuR>5FHI{}7z+Xe%<iqKFI#
zwrpF^T)Sg-RSYLo$aHloBW)Vh8D-g*uf@S<zXS0qHZ6~toSbL^E{ceV_&pr8S_5UK
zD`mR+CD%l86NF=Nu6Kh4T^P8`63ci&YGIZrwp6OC(2-^unb@BA_>bVbUdNjDm<i#L
zTHiQ+DkTirvS8FG{zJjQr&ghD1?BWc66Y~ET^87v*}isUe)VnN_WD7CKu^-9%Q<-;
z<GJPgV@%2eKOp#C>7#!bX){$U);^vv+P0(13Ygg$L1gw=!08E}aJAO6#2_+kGzU#9
zr$cj#(J+^n4Bs6BvEXGi;el27R#koDDLrMTQk#?)EW-D`FcA(OU8~UWdL9bP>4XAz
zhgk%-zjj*s*=mI$j!D3GeExLkgvLaZ&E4WWH0Q7txJ3d@qQzNPb9QR<MtWbF<Rp>O
zxRFTZ#}X;L7z>)BtVnL$K&0{_tw`oY5U05l%jk;!KaJ{O|K$1&mTN<_73jICyjX<L
z!k#ab;|A(}S^3vcmN)S{ba}P$zjWzR1W8hqk)%up6RH0>0N-t-D9Zfs;lsb7MFh(>
zt?4WX2?<$Fk<>T@m<ymO+O%ueu9Z->h0~c5GM%8Ip`rY_B0LL?MpKPfCNF2Bzc4g3
z^h3y)nrj50Oy>0T^uOSZ&co5@zX6$+J9>ygZvcr$6G3!zG>D3da)@o)wt>^9PlFM~
z2&k&60tfzn0Bqd2(dj)~wrl|v<rPjz!1oKW|1_l*d{?Yk@d0E?J9<D<V-tWHavA&M
z_k-~8aF_eX$Hz~rg(F9f{0YkHXRigc+XV;(!sE^!P*+#Sya)+ZQc~g&1qB7*=+UE0
z-_4shgY4{VP+VN>uzzxLGSheW?%mUxl3XtT7P}gr_U01|Rn+Q~K&^#9WMm{7jcI|1
z$e+uZGF%QF_Cw@cB|ws-4DZkr%n0pt@4<(Wt`b215A+X!rlzJzK=oMVE=|y)q9T_G
zATwIKcJ1%5i(y@T{0a*TW2d{>p1}3%*Qc~)RPW9T;|>7?p-`A$yBeWCuL%T#BkmFq
zi^QP1x_S~MV#%x$kjZ4%@XqYXPT)lY0|QswC7@EOz@U6^5>WkY5>TmB5AoT09_#xo
zRw|Vx|1Tz_8`tr%gEyZg5Cki^?h<HiX$5!g+?fPaKbr(hMw1wCXjTG?)oS%)cL^|{
zrhuDng1ZFJa*zF@`Rp7BgcBpzT>>|5++fP~$j!}lNf6RRGH{mw9PA%#=Kvz$-y!Tc
z<}QJnXEos5xpR|%>YWK~G#HT?0IgPw>>1p>dl#%<zusj6csCAYS0Tm=Eh{VA{xT*s
z)y_JN{Q|8<%Rr$}pwVOqtXsDZK%d_w0$rV5CvZ^B!(~U#o`6QJaR|LmKj~g1Flrkc
z9Rq#RK4!Ph&dLS}2?=OK8E<vRjvWla>2~77hY$bJ{;-cG!`o<fJG+@%$92i83=0bb
zhrc=uh6abg<FdyfE-ud6hDLhLnl&IbH5FuJWB~YHaOlt>AQFkd{{8zIqE1Or)EJJl
zdfR)Wi~1ye7o8DM3@ezCa@9TFyLay-cN!5`y?QlsUSJ6z96Wds?Xcjv>^Y+;G*8fc
zv6o?Vbo2oZBG0owjrl>>cNJ2RGZMqY!;D*9TU$FNYHDggettgKv}u!7XsB{fUS7_$
zS5#CmUTSG+Dd2E8Rssk}18_rSc5_FXxOwwt3=VO6Om!j)hRoYt@Cco@A@uk6gRNV)
zGD6zkgGRSq*v^=vJplxyHFzon$}uH#0<?3bhj9GV+tHc$RjAg#PNxM|(bC*9NeGre
zJ-2=W0WU8vW;J^9<O!n%y<X2O2CNw&nczzKDUOUg3^$@fLa<h?t(mpx0wmDc(Fu4w
z9<yio`1pWZw{C%efB=w~n8?fnVVe+)j*K!SSOU<Ty758Ad%C#L!ks&Ju9~gL1hQ>W
zuL!iYwSm06JOD=(d6yt5DGB)d`!l1D&faJquo^(i`;{wKLh%{E)0N0vU_(R0zh=sm
z>^w<krIAXcjNb&$At38w7DFO1Fc2I&b__@)5@rcT=7(f}=Z<sOb(rr?U^lp_MUXu>
zJVHMdNXv`(q6Y}YeDVEazGwpOH;aq!^IzaWv$$v~ptgIwp5j}T{E~(aaeIeIDo?tQ
z^You5Gis~3JjLzXx9LY^WpoW+AkBSJS1y&w$*x{W$;$+I+0gnso&EAhgX1QB=snTr
zL~3g+ai%SXNNbBG(psX4l$IzWO|XGTYqlVjA8COgl1OfiAkrXEInAbw0=g%kMTg$t
z9q>Bz{Z?XM?sj5(`kv)`l8+WwH*#|y*FO2pS67o-#B#M{aHJKdpZs1XPvD7fd{@vT
zy)$m2sejjxrBBWeKrrEP9QQwq!{<}x2XGteb4C!w48TclEb&cg9PwGsC&Z4k`@+-j
zp6!tIn>wVjDmW+J!)w0hk4fqwpChF7hsp#4Wu}TG<Bulg!mOzzOoom9+ru_PlYz54
z8SPQT8Ga0LtY-7sr`?4^qe{Ap+jJhg3kzo=>BpMWn`bgJW0VF`IjpC28O_Q|6Np&z
zVpHVCCJ1elUP8%|%<h;Yb&9lpwNb6s8+AAH3wPtw)`FSLSh9(}0K(UIS$R$ENj+&c
z^l8YkEP*O#(dFJv6S9=H+F>)3tiZT^!TFxpv}WbS9u+A!kThA>+;%Z2C}=tM^JgT%
z=V*yVBeQtv(xo5p+PbopdUC`_naxm;!Ga!b(Y{JWBFYV;k!64dUY;l2w)?+>-{f|v
z3p&P(1A3CCRR&Tm=<U0`a^=e3qQEkqbMuH<{;}Ug_~97kyYL|L5x1o+b4aTfp-d*q
z&XY7StBo{?^1vqAY=+CZS!*N>3az2PQP_R$%P+tFGyVi22;2I6;wS!wf+gaO1$Z5=
z*GQ=4&}IJcRLYt4sT@vtL`1|YREOI_usi$;Ufbuy#~-#|s(53;7GN#;;j#~In{!{R
eOh;^11^pj`T?e|8rgpOc0000<MNUMnLSTYrNgGB0
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..1a60d93ca9311f8bf9e0ce3f6f175370a69f648e
GIT binary patch
literal 6426
zc$@(m8Rh1QP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h00001b5ch_0Itp)
z=>Py2)Ja4^RCwC$T?trJW%s|NqAV&ZYN#o$m0MX~^_Y+OS%1r2D?ud1<<Z0~%Ow>>
z&0H{4QXs~^n*Y?#&(thS%`JgMLEKRUVHoy(XNF<+`@iRM-r=1)!#+cx+~+yZUFOcc
zGw1idzjNO09ENH9n-$6nl(eWq$URn+G;eiKHYl?tC)S*_=A;cLHk{a^*qIPpyr&I*
z2fo`1pF;!Fya|*}6y6FIXM+lD$B6?c9XaXDNf%CD<iv@SZUS+_&${BhojB=$&#=d5
zw#Ij~c;3MfD!mP*@Q$cxP-;(3`f$>plUFzy#K{m&UgP9-PKKepj@O2AGMJNBIT?W8
z?ZruV1O`B9kMC-OHRQPhgDAcwmPT7tdM7Nspzwa23<QO)TC--#xy#qQqMk~QrsNjg
z%9krLr7CSnnMS8jYIGW{j@9W{R;SYHG;+08S)x?S3gxQYwEU6>(UR0tSFYdq*2mY^
z-PYE27=qUy0ReCwDKJ`7G-dTXDBd3Fp(`pK6f<P;vSpKQ+<CAst)L{TOs!S1X4=$R
zoi@8fo^b!E`2T$O>~+W5@-ipRNL^6t+G4F}^(+BlO7R`B1Ohz_nlNF)=sS;N4-}QD
z^M#62Xmr}lQjPRUfigKbO&;-MjP&@%TlsrF56<2+{(Qz)eU2r~>vBlq@xdP%U(CCf
z?d=tj|6M>-(T|r><e?9972;H>x=^OlYJ|@%RcXp%lhc1&`SsURk<$7hO*uYGKv*Ha
zw#I5s6yF#A^r0iiJabCQ;<P$o5q(M3rAXBUpzQUL`Myk$*o~J!@pN9uu?WwFLOv(o
z3n<@vT+Q)1pCtQBT&&LG=u1msp(~WDv#*AQZ?m(rdmU?H52VDl0%~e<rM?w<9uCOw
zP~wN2JacvhaJgOs+?Fm?7yl-aUhH=)WuAe;X+>A2biMN9T}>X-;BaE|LxTH(nBrsN
zB6XIAvmxYn$W$8H-?#7kIXXJNfi<EV(xe?_tgV_-sc(x_8jRkcU3>S=E|RNr$RFkm
zr|xd9BIeUenXB~*5>d*gl^B4>WDlPOppfGrNx2B`H3Fvs;6cYE3$CQd!)Tf+RcXsE
zTn+I?YU+)4svXi&%f<`>;d)Z)!MODwF=E8X#O(Y~9a0VOdU%%n!CR+O76}!iR}`&G
z10{3vL%d7HvBX8c55UiY@G~JuK@@m`n$h7$$;@*}va2Pk3d7GCZOK<(t(t=FZx5^u
zwvEv?(3E3~{0W}TAaEfS8m*d8&7*v!<jtScJdKo2D~BkPD3n&NUL+OAfs$!P*M#@d
zU<C+(0H6^FyMsyYV1J-?!f)pMTphT;*uHSa2A+Lmq9mexYy4sG1A7CHiqo^NfZpMc
z<j6I0@LmiQuP^B)O0!;sN`~Y3X^aTZ;ky1fzI-pD?llPnsX^r35+iy7T1+TbXX|T2
zu{?3&#EJh#+wdY%QtN*j1j6^$$oIgN{T3}+Hi2^~bIVITtGkgQzt_SP%>)Q&g(y9c
zQm%or<IB&35<b=MK}6KGK*2EqMF=7dE^R{uhWkHHrN3|uKE4o@sddVOKOJ3&l++Cy
zac%x75NKoD4ok($JN^7-aU8EgW2X5&s_1`>N<3{u^(CGv-K1303Kc5d=vrL)v45Q5
zbs7l%nS$Cu*EnKx;F_qSM$G*yXG3n8Mh^c8THY6fL$_i}y*t{v1_yzropxw!Upe;k
z=_SzOgddtE*T|r8WW@79!V*u>h+a{Y^7IN#_@w-pI_H(n?dfBK&$&JMeh>wWh^koz
zph#Q{3_fk74<&pCjX~l$M@Gf$$F7YY{{#pxP_)A<pEI`JTJ))trJB5#jwH@8=6VUG
z{CF25PN(U_q(X_p<50L`Q9{Kr>_rF&Ei?eM7y&@hQ2<3tRBedJQamJfAvz)D40il=
zM>=W{n?SrBd*u9=kDfTWoG2ekiH#^>hJmpSpbcxhp<&h21yxdMWfKJli9a?F{vK{0
zfad{%YX%@tfYG4RG)CJBVx$cWbw!PUZVuv7L*Ek8Q8#1*YfHA8t<cZ6LvN@5F2B9A
zRM@nSE>I>JDc*qR5dc&C9u)VfK?xTn7%QEYfTx1Jsm};l!Wtu}8MKYi*9eKB=R~=x
zAm@@xiHD~P3BSD)yT4yVR%m4j5Qx#WK|jCG(&fu16CXdZSe3_@d?Dx4t{<&<J-^c>
z-b6_!%KSk*)j;4neQxR=a!m*jh)6(L1~r0mZ4JC6y1gQHyPhGx!<M6<J9F;BDja|4
zf(6v<E#?=H<G|qb96EI9J1UJ<s?};)g<7M@Dpkd2$y9L$A~h}9GSyRaNRCV$YwEZ@
zk|kAAzm+<cnHHH!QwBQFa?Zz|J$qcSzpE1#RI}LwE38&+k?RI2mC8qK^JcJdW}&!G
zpFVGK`gw^m$}gCUdTf#R2P7sY?$>HK)SJaf<Z}5PG|Dfcqf&1wqN%6X>eZ{qb1%|r
z)Ec%~jD^#UE;2IG8^=64QSF0zc|G>%?+;QclyS|9t`>w+Cnu*jv5`hxQ>(g`{x&$0
z)t5td10dY2<m#oMpx^?IId?|-uC+m?4R3dAYwMwaY_oD}PDS5*^UW0OmTzB|1{x{-
zz+?RqC5ii+6<a%zm&)&A1FZ{H3u?{%;C8;oAu6lu50OYD?3OKC*tv7(va@E*s*`wl
zc(98XFJ`?rc(ccj9b>aHvkcympO?@6a{3o`)5cBg(xprJ`|J9y*|TS}8#ZiUA3u6r
z?^^KXmtT$()B>y8cs@M$$Hm6^)%8c@<>s+7XU=4&PMvC2nKo@28yp<WzvJG$d#t;=
zyG7oSl$ca65TsJ+4IDq{jE%Dz(NGI)PJ=HySfkMt)b$7a`s=S|R}7I)KKX=KzMGqy
z#olr7z`^>~0`2hO!{0@ZsBKN!1;qG5mFachz=4_d@qG4+_FGIjq?B6n`CGSct+y>m
zO-Vh3Lzx|G(mE!_7p9ZE%vGV#`n+5u5*hrVk3asHUA1aet!3rPmF&WW3t1Nzmudl7
zuwVgye_h_=>gsBC0Mu%AHjW#-SQBGR=m(51xSmRL0x)C73|0q&2elNwsjt=Y$8m9S
z?B2b54W7Gw`*t=eDvH0aF3%^jY}qnH0KVO7ZkMzjGK^Jq^yADKxs~;OxhVk5<Y%*2
zuU_S!dE&$g{+zQ_^?t4;0N|aGyu9CPLPNy+0pEW>adGk0#tHxd;nb;9P{qymOiKa4
z{l;|cr0YU;Rhy(ATO4W}#FeO=#tZ;28uK=uECm2~L#<l1dL74)NiV6<MjBt;d-v}B
zwmJHJsscc%Q1Xqc+6a8dlC4)u0RR#b67m(!u4`|~4jAzNty{Oe%<<By04SA}*4i!P
zn)gi>0PLWn?SK&g);IyRUuI_J$z}k|3jkMppHi*jHbO&e;Qw>Vxoc?v%H(CNpPyf?
zL{X+_vH)=Qsu0@)UD4JW0bqkxdJt!|Gc64O@nmWvmhQo{Bmms+*WhZ-ZrIjpAq2n{
zTWdqOn-vWKKw}e&)&Nds&z}7O_6r$#LZk%@LyxbUYuvXg08~m9`}pzWT8UbvZqfi8
zKYn~JwuswS1_1b}!^}1G*ldJNx}v?PE(rqw_%?vEv-3OVdSNpFhH~%Ty)A;afDr&a
zU=*hz003pe$W?6wJg24%Kzv-hA1?eeYK)OqaF6ZVw@+;d06=QnTiMi&up~heSQ!BN
z8Zc(em{AP{!1Lw>J&t<pQyl<cgx_cgBm6u95Ed4;t|}Wq24Wh-4Fw|%<GHnwrfvhy
zo;~YH4a~GO)fMf8BVp=i%?pAy1QA%^BD@A-X#jTZ+Vu&}o;GR+kgkZXI1{4qSptA4
z7#e@jrOgj-av;Dt^e@3RIEFbzQ>IL@R9A$K8WS+t710KJ>RyGOJ4*uq3Q0&vsFl>E
zB}|kLN(N;EhyVc2>1WHz%J{+n^tNQl5*`2of|+Z899P_%9GthJCS&z4Dk{44tQcWZ
z@<G8esf;ZyD&|MhsD%|Ufr`JrXwf2e^5n^S5X=lfs$f|11ykbzunwTNI8MCpSpon`
zK9usJ!Xh>&JBPmp+r^6)^&r4!pMU;2J8|MfJqX`!`POW|qf}ZNj_b%eRW>l+5{<)4
z8#Zj1Z0RA%h7wZxp@f6-Q<76yrBcZ&e%P>K{5((s!l6Tl*zx1X>p|G^%@(tD0E*+q
z{<wzBp|W|RbdTsz?n`C$eQ8iokO2VTN@ize*GUTU3#uIx24xo(6!MBD-^)nP;FZoP
zfPM7n5qm4*7F$|c$}4@qfB`J`U)eQl*62alvuBSUgw2~a^Y5%L2vC?kJUsrVG7rd<
zA%3N-wCs_&Ye0PkX0CaAd$ZAxqYd7ZC`n|uZQW*ak}gomq)C%_rGEI~hwS_Bzt5(m
zq!<7I0<-A|2r%($+_-W4Y;2$|0zy52kYWI+Drd*h<HCKC5|e(YF93%Q9<tcH&67W$
z<lk}S$`yX1Ze{lY08pAip<~93VMmP`#U>{w8vy{aeED)c2>$;5JUu~;A|OBws|yI+
z_iy8h8Ans&!KUm$4;L4gQT4S0!PkN<I(hcupBJm0pbK0M0E`_wmi^#^57?0-M^*{|
z2)Lpoj^N*a|IJev0RfD8T|j{4UDyTTP~AEK(jo>|X(0Eqr1~1bb?eqy6ol%81_0=1
z#E21WQc_Z-0D!<mU;+Zz6955h5V*0m1p(&QVw}=$RZYqgwg-TQ`o=`Z_?lrl27Bq!
zCEgwc1_sti0s;b9FE6iZOFPWIUcGuXPaovF`}XZ)ckkZKPvBj@em&pX&B(~85&)3-
z^XFH9kdwovr=`~k1g?#7y{7adT%Oe3!NCEBCpG4n$5~&2@c<}^FlF47^4G3i%eR*G
zv#C*_0Dudcnv%*sxc`8?aQ*^YSXfvs03dVb%+a@+VHp%Gje_<9w5Ds~9RY<OK74o<
z4nwxDYG!m*YXtsoY~98O4jeF0zOSz@Um{K23#Ap52CnG6yZ6|0=g!p_01#MWMOx1A
z99VWGEs@r!7U0@|LFoP1)pVgAJx#Hvr>Eyfjct7}lx(s_VAG~eRVqOUz#o77Q7ZsI
zz`Z3PkZud81=Rt7#<verdaYtd(8l;x&I5>SY_*`IxP<S!$;`~G^1iy%07pkhzHtV|
z7zF4oBqT)Ne?&U)s-z)}rT~5aZgq(pLBf*pYsJjfHDPuTN=mA|hX&yOz5DF>^XIDt
zz{<*sjfsh=GCGP%rK%Eus0UFyFlIzYrVf!)tOT{-74FXlH^l|h1OQ@$|NQe${#*z^
z&z?QmBS((t&%tdaTVi4&o0ggeD_wa0hZ+C?R7OECwG-!oI@Kp?iWL<Z@uh(S2fkyj
zDT@sa0B|-aFzR6xd-?KZwy4Mu08l=7P_RK;fq)Pi8p>aL6!nO`_SZFj4?u!DS{Vfa
zQ^4v{9DMJ<)H7}n2~!3hQ9l^fZ>j)*9}j&<A;BR$brArZot^a!GuXny!t?;V_uhN_
zHRwGJyB@}`o(CYIWn7sn$~`*-r=fQ>NBA-;DoP?BvWBa62~7zAP!bsI%#2L-Y3x(>
z=8c=YCj;BOdGpxR)KvZ$Y5)P|qmMq)g8+R^u)(T%01$u#3WF|g6BHx|rl6u`nI(#%
zLC~g2lO~O7N>l^@0B=K=f+Ru08)DKOO%x6w`0Vh}570zJMCbt+J$kg>=*B#WF#>?5
zp3|pKFQDSa)hk$2Wf2p3D!Opt!m_3kI|u*(zJ``8&_`@cEN?`~2Hgz*J^o)lECH<S
zm&s)O^$8Ou=s~!9_ilMD;HauH0MgRZj+m2rs-hxz!7Juk#?%laV!+p62Vj6EIVo8`
zA_m47xE*K)8fdWW_4e)C{AYLW+~JkqwryKhEEel&2tX79fXk0a6kUr2aHZo#^U)J8
zhhD!J9v;r#xN%draNqIY?cFwRM%iTVwqcX~bnlJ!uDGARej|6y+uqH~yL`WX123*9
z<aoN*dcC;N$7}iXwyx{E;GEy%Ik~3)9PY*M*iKus!S>TN8*Qhq*kn8Di!HX}7H!Ke
zEXjY8l%CjilILEV_hx+GdZdeg>){^<wf@)mgB{+TbhMyI%5xO_w}OHKzDG`yo|R)W
zde%Ohaf^K4Ub_9@<!d3Dn>TN=*KgfU!L%nmk)~Uli+ZYxnC^gvW~ewVD-x=7Nokqx
zo3IoS;}bE{Kz4+G#`xTHXMApz%Z{5fnD1_t3mp4Ixbd>>M)`h6_;ltw6gb`yHl6Va
zcjfnRzKuKfF(KRV9{OI8t)b<z`HDNU{`XnT>N6fU5_2v)t~<4mS$22<vry#8%nw+?
z%=B5#Ok4joGvNzwuR~`}X|)w;O_H<o(}%AK^kru6T=|~w?_0V5tOEOySy+-cc<|tN
zv9G_Qx#NXa1wtG2aeLa?*}axsEQ{kaPUu+Ok1@r^E3z3;G5Jj;WuOCh6QLvOl5oQ$
zvMz}a9Y}>X)?X8RPQ*XvQ)XZ6G{!G_8nfpC2TtVl7Tdzg3&NIAcV<iQOlIA=+04ol
z^BK<{7cp~oEoWRee?5G4pzr*>Ck~GC3fjZCdV4V*zAL8fzqB_wGg||8qOepU#iS0s
z(dgPXRE`-dDpw4k-m7!x&co6Rr1wcE-?NFO7mc!Ika*I<oB_fJ>PEQ&stH>!5`?ff
zh?tH7pnf0i!UM7Akt+|x?z_{OJ@?$0?{0H7G3+zun@h8qwP)rrOAkYh_=1_e>nmo)
zw$;qc?W-7%-76L!z4}v9YPuG>F|rD!`J6ZSK6*Tk4XOOfKmh(5nvjuuh0J%lpR0(2
zxcdgRfds-P5z6Vz79@DKK9s#Mx;hp0+#vWiiHJ@|MUSia9sv`S58@x|!h^9l+Ep*#
zKbp={<<^i{jMuq&%&MOka6l|(JohhU79U)CAoxL8T1JL0KR=&M$;wX|Hf-2%O8It{
z3fNs65IUeu7<@lQ{F4sD+2@pLN<FUSco`&Q69&UJpe7R$*6D-`!i*ZilqSLe;dE4V
zQrkpS!UlN1KX>f^gsTk?-I$$`pK-M0!EC-f2Y_()xx73=oSp@JKv44I)3fh6IXV3c
zIp0x`Rp}qgBhwOXf&;o?uO2>jd~umt%lmqegXL_Z>>{7lj!w!(rwpN^teeV`L{giW
z$|)lRNT~EcVd@fzi&2WJy|J7;abx@*&R~4+yE8i@W^KKmbSR^sM8)_2mq>NN;kW(K
z)$ED2p3u%eo@I|V%p{*o|H+dlkIs`R^qnkOa*cG=jXWQdwjn6q)nv%~%F<JbB$a1O
z64CMhspLSy{9=R>$#3v~I-S`UGh@t|gcZ@br5RkyR|l=%!qPJ7mTf-n*vKMzV{Dj4
zmeh^Ts6^6PIa^y>4+*>bz+a&*cj>@DN98HSlP_ehGzmh0FxLzXhQu(#g;6pdfx-g>
zxx?rZZz8*1zhg=BuBDbnC?KIF$DwMjrT;WJ<J^D&1Kz@Z{jQBAeN{~px^=)3^YYlS
zWB(2LfJt64NRSp?piG>3HOtE=cch5+m?*<d_Cnec9+P{8Nf#<f{`uzVG|!t^iu(#G
z$!C74GIjmNjV>5?^(AyOn@mng+Ag%k+5ocx2KtK*%*`&AiwO`=)+EKM+#|6iCn23+
zr3pM~x)7FPIy0WIbXS%Dl(R=W?-vC^+7*?yBB5uITvb5jk?ez|zCA@bO(wmqur|;c
zRR{0f<@Z@eq4cqiN@dF#Z7rlaJP=)S6!HWcC`^=SxEG4>y2!YsLz==JcMAd|vJ_Dz
z%JS4JWGj@bvrhkUel;$jAemG<GEFR#YIWTgc1TN3*m@iUS@)tPsV5<oEUge&77V$_
zBC{2bkH?jqSrL}Ey~`o7he5vJ%5n&E0-5_D!80U)4o#QePL*m3Nq$IuvS_tdEzZad
z-o0<12bTE0sQ6B3$DWxq=fbSo1c<IkO)zWhwSfHx=S#Bku9PS>g@mFEQ-rWOD7o2k
zO=+4;QzR)?=RPe|rpFd4Qxl3+IgmXYKr2#el^TJv=}g+3EsFf@&r56Dw{QOjdN$5X
zMOt%vrXp?aGnZdp2!t*2t0R`Y-U!AJFxr=buWuG-<_71=%93SDwZarARc(;rDPN|@
zO35pVxPIsU_nw|hCgL2;SCI;PpnY<{LectJReWVYv_Kot21}j;g5iXoM;`<PW|qAM
zK}5Fs`nvt+=hI(6n%t|`!*@nLd=dz01w$k5`hxL1e(IMm_XUbP-hTV-kr*6lFqZPZ
zSlhax(mSAoV1pe0TqwS}T49Y~*r9SdARt{4lpfd$bw+Ud3ZxHy)(h|LhTnC>XSKs;
zKle(nNzI@Eu|dVzqY~R=ZRsEo2mGuZ0%(Uc*9M=}3ZG4s-mLf<0z!k*5*2A(iL^p#
oNh!No;T8a<Drw%Dx0-MN4-+oM8s$K!R{#J207*qoM6N<$g3jVX>Hq)$
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -118,16 +118,17 @@ browser.jar:
         skin/classic/browser/update-badge.svg                        (../shared/update-badge.svg)
         skin/classic/browser/urlbar-arrow.png
         skin/classic/browser/urlbar-popup-blocked.png
         skin/classic/browser/urlbar-history-dropmarker.png
         skin/classic/browser/session-restore.svg                     (../shared/incontent-icons/session-restore.svg)
         skin/classic/browser/tab-crashed.svg                         (../shared/incontent-icons/tab-crashed.svg)
         skin/classic/browser/welcome-back.svg                        (../shared/incontent-icons/welcome-back.svg)
         skin/classic/browser/reader-tour.png                         (../shared/reader/reader-tour.png)
+        skin/classic/browser/reader-tour@2x.png                      (../shared/reader/reader-tour@2x.png)
         skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
         skin/classic/browser/readinglist/icons.svg                   (../shared/readinglist/icons.svg)
         skin/classic/browser/readinglist/readinglist-icon.svg        (../shared/readinglist/readinglist-icon.svg)
 *       skin/classic/browser/readinglist/sidebar.css                 (readinglist/sidebar.css)
         skin/classic/browser/notification-pluginNormal.png           (../shared/plugins/notification-pluginNormal.png)
         skin/classic/browser/notification-pluginAlert.png            (../shared/plugins/notification-pluginAlert.png)
         skin/classic/browser/notification-pluginBlocked.png          (../shared/plugins/notification-pluginBlocked.png)
         skin/classic/browser/webRTC-shareDevice-16.png
--- a/build/mobile/robocop/Makefile.in
+++ b/build/mobile/robocop/Makefile.in
@@ -44,55 +44,23 @@ manifest := $(srcdir)/AndroidManifest.xm
 manifest_TARGET := export
 manifest_FLAGS += \
   -DMOZ_ANDROID_SHARED_ID='$(ANDROID_PACKAGE_NAME).sharedID' \
   -DMOZ_ANDROID_SHARED_ACCOUNT_TYPE='$(ANDROID_PACKAGE_NAME)_sync' \
   $(NULL)
 
 ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
 
-# Install robocop configs and helper
-INSTALL_TARGETS += robocop
-robocop_TARGET  := export
-robocop_DEST    := $(CURDIR)
-robocop_FILES   := \
-  $(TESTPATH)/robocop.ini \
-  $(TESTPATH)/robocop_autophone.ini \
-  $(NULL)
-robocop-deps := $(notdir $(robocop_FILES))
-
-ROBOCOP_FILES := \
-  $(wildcard $(TESTPATH)/*.html) \
-  $(wildcard $(TESTPATH)/*.jpg) \
-  $(wildcard $(TESTPATH)/*.sjs) \
-  $(wildcard $(TESTPATH)/test*.js) \
-  $(wildcard $(TESTPATH)/robocop*.js) \
-  $(wildcard $(TESTPATH)/*.xml) \
-  $(wildcard $(TESTPATH)/*.ogg) \
-  $(wildcard $(TESTPATH)/*.mp4) \
-  $(wildcard $(TESTPATH)/*.webm) \
-  $(wildcard $(TESTPATH)/*.swf) \
-  $(wildcard $(TESTPATH)/reader_mode_pages) \
-  $(NULL)
-
-ROBOCOP_DEST := $(DEPTH)/_tests/testing/mochitest/tests/robocop/
-ROBOCOP_TARGET := export
-INSTALL_TARGETS += ROBOCOP
-
 GARBAGE += \
   AndroidManifest.xml \
-  $(robocop-deps) \
-  $(testconstants-dep) \
   $(NULL)
 
 JAVAFILES += \
   $(java-harness) \
   $(java-tests) \
-  $(robocop-deps) \
-  $(testconstants-dep) \
   $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 tools:: $(ANDROID_APK_NAME).apk
 
 GENERATED_DIRS += $(dir-tests)
 
--- a/build/mobile/robocop/moz.build
+++ b/build/mobile/robocop/moz.build
@@ -13,8 +13,28 @@ main.recursive_make_targets += [OBJDIR +
 main.extra_jars += [SRCDIR + '/robotium-solo-4.3.1.jar']
 main.assets = TOPSRCDIR + '/mobile/android/base/tests/assets'
 main.referenced_projects += ['Fennec']
 
 main.add_classpathentry('harness', SRCDIR,
     dstdir='harness/org/mozilla/gecko')
 main.add_classpathentry('src', TOPSRCDIR + '/mobile/android/base/tests',
     dstdir='src/org/mozilla/gecko/tests')
+
+base = '/mobile/android/base/tests/'
+TEST_HARNESS_FILES.testing.mochitest += [
+    base + 'robocop.ini',
+    base + 'robocop_autophone.ini',
+]
+TEST_HARNESS_FILES.testing.mochitest.tests.robocop += [base + x for x in [
+    '*.html',
+    '*.jpg',
+    '*.mp4',
+    '*.ogg',
+    '*.sjs',
+    '*.swf',
+    '*.webm',
+    '*.xml',
+    'robocop*.js',
+    'test*.js',
+]]
+# The ** below preserves directory structure.
+TEST_HARNESS_FILES.testing.mochitest.tests.robocop.reader_mode_pages += [base + 'reader_mode_pages/**']
--- a/dom/events/EventStateManager.cpp
+++ b/dom/events/EventStateManager.cpp
@@ -3789,20 +3789,21 @@ EventStateManager::DispatchMouseOrPointe
     // Although the primary frame was checked in event callback, it may not be
     // the same object after event dispatch and handling, so refetch it.
     targetFrame = mPresContext->GetPrimaryFrameFor(aTargetContent);
 
     // If we are entering/leaving remote content, dispatch a mouse enter/exit
     // event to the remote frame.
     if (IsRemoteTarget(aTargetContent)) {
       if (aMessage == NS_MOUSE_EXIT_SYNTH) {
-        // For remote content, send a normal widget mouse exit event.
+        // For remote content, send a "top-level" widget mouse exit event.
         nsAutoPtr<WidgetMouseEvent> remoteEvent;
         CreateMouseOrPointerWidgetEvent(aMouseEvent, NS_MOUSE_EXIT,
                                         aRelatedContent, remoteEvent);
+        remoteEvent->exit = WidgetMouseEvent::eTopLevel;
 
         // mCurrentTarget is set to the new target, so we must reset it to the
         // old target and then dispatch a cross-process event. (mCurrentTarget
         // will be set back below.) HandleCrossProcessEvent will query for the
         // proper target via GetEventTarget which will return mCurrentTarget.
         mCurrentTarget = targetFrame;
         HandleCrossProcessEvent(remoteEvent, &status);
       } else if (aMessage == NS_MOUSE_ENTER_SYNTH) {
--- a/js/src/devtools/automation/winbuildenv.sh
+++ b/js/src/devtools/automation/winbuildenv.sh
@@ -29,17 +29,17 @@ fi
 # bogus option or something. I don't know why exactly.)
 export PATH="$(perl -le 'print join ":", grep { -d $_ } split ":", $ENV{PATH}')"
 
 # PATH also needs to point to mozmake.exe, which can come from either
 # newer mozilla-build or tooltool.
 if ! which mozmake 2>/dev/null; then
     export PATH="$PATH:$SOURCE/.."
     if ! which mozmake 2>/dev/null; then
-	TT_SERVER=${TT_SERVER:-http://tooltool.pvt.build.mozilla.org/build}
+	TT_SERVER=${TT_SERVER:-https://api.pub.build.mozilla.org/tooltool/}
 	( cd $SOURCE/..; ./scripts/scripts/tooltool/tooltool_wrapper.sh $SOURCE/browser/config/tooltool-manifests/${platform:-win32}/releng.manifest $TT_SERVER setup.sh c:/mozilla-build/python27/python.exe C:/mozilla-build/tooltool.py )
     fi
 fi
 
 # Set INCLUDE to any existing directories from either the INCLUDE set by the
 # mozconfig, or pre-existing directories. I have observed cases where extra
 # nonexistent directories in the INCLUDE search path causes the compiler to
 # fail to find files that show up later in the search path.
--- a/layout/base/nsCaret.cpp
+++ b/layout/base/nsCaret.cpp
@@ -115,16 +115,17 @@ static bool
 IsBidiUI()
 {
   return Preferences::GetBool("bidi.browser.ui");
 }
 
 nsCaret::nsCaret()
 : mOverrideOffset(0)
 , mIsBlinkOn(false)
+, mBlinkCount(-1)
 , mVisible(false)
 , mReadOnly(false)
 , mShowDuringSelection(false)
 , mIgnoreUserModify(true)
 {
 }
 
 nsCaret::~nsCaret()
@@ -589,16 +590,17 @@ void nsCaret::ResetBlinking()
     mBlinkTimer = do_CreateInstance("@mozilla.org/timer;1", &err);
     if (NS_FAILED(err))
       return;
   }
 
   uint32_t blinkRate = static_cast<uint32_t>(
     LookAndFeel::GetInt(LookAndFeel::eIntID_CaretBlinkTime, 500));
   if (blinkRate > 0) {
+    mBlinkCount = Preferences::GetInt("ui.caretBlinkCount", -1);
     mBlinkTimer->InitWithFuncCallback(CaretBlinkCallback, this, blinkRate,
                                       nsITimer::TYPE_REPEATING_SLACK);
   }
 }
 
 void nsCaret::StopBlinking()
 {
   if (mBlinkTimer)
@@ -908,16 +910,29 @@ nsCaret::ComputeCaretRects(nsIFrame* aFr
 void nsCaret::CaretBlinkCallback(nsITimer* aTimer, void* aClosure)
 {
   nsCaret* theCaret = reinterpret_cast<nsCaret*>(aClosure);
   if (!theCaret) {
     return;
   }
   theCaret->mIsBlinkOn = !theCaret->mIsBlinkOn;
   theCaret->SchedulePaint();
+
+  // mBlinkCount of -1 means blink count is not enabled.
+  if (theCaret->mBlinkCount == -1) {
+    return;
+  }
+
+  // Track the blink count, but only at end of a blink cycle.
+  if (!theCaret->mIsBlinkOn) {
+    // If we exceeded the blink count, stop the timer.
+    if (--theCaret->mBlinkCount <= 0) {
+      theCaret->StopBlinking();
+    }
+  }
 }
 
 void
 nsCaret::SetIgnoreUserModify(bool aIgnoreUserModify)
 {
   mIgnoreUserModify = aIgnoreUserModify;
   SchedulePaint();
 }
--- a/layout/base/nsCaret.h
+++ b/layout/base/nsCaret.h
@@ -201,16 +201,22 @@ protected:
      */
     int32_t               mOverrideOffset;
 
     /**
      * mIsBlinkOn is true when we're in a blink cycle where the caret is on.
      */
     bool                  mIsBlinkOn;
     /**
+     * mBlinkCount is used to control the number of times to blink the caret
+     * before stopping the blink. This is reset each time we reset the
+     * blinking.
+     */
+    int32_t               mBlinkCount;
+    /**
      * mIsVisible is true when SetVisible was last called with 'true'.
      */
     bool                  mVisible;
     /**
      * mReadOnly is true when the caret is set to "read only" mode (i.e.,
      * it doesn't blink).
      */
     bool                  mReadOnly;
--- a/layout/tools/reftest/runreftest.py
+++ b/layout/tools/reftest/runreftest.py
@@ -201,19 +201,17 @@ class RefTest(object):
       prefs['reftest.filter'] = options.filter
     if options.shuffle:
       prefs['reftest.shuffle'] = True
     prefs['reftest.focusFilterMode'] = options.focusFilterMode
 
     # Ensure that telemetry is disabled, so we don't connect to the telemetry
     # server in the middle of the tests.
     prefs['toolkit.telemetry.enabled'] = False
-    # Don't send Telemetry reports to the production server. This is
-    # needed as Telemetry sends pings also if FHR upload is enabled.
-    prefs['toolkit.telemetry.server'] = 'http://%(server)s/telemetry-dummy/'
+    prefs['toolkit.telemetry.unified'] = False
     # Likewise for safebrowsing.
     prefs['browser.safebrowsing.enabled'] = False
     prefs['browser.safebrowsing.malware.enabled'] = False
     # And for snippets.
     prefs['browser.snippets.enabled'] = False
     prefs['browser.snippets.syncPromo.enabled'] = False
     prefs['browser.snippets.firstrunHomepage.enabled'] = False
     # And for useragent updates.
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -42,16 +42,19 @@ pref("toolkit.storage.synchronous", 0);
 pref("browser.viewport.desktopWidth", 980);
 // The default fallback zoom level to render pages at. Set to -1 to fit page; otherwise
 // the value is divided by 1000 and clamped to hard-coded min/max scale values.
 pref("browser.viewport.defaultZoom", -1);
 
 /* allow scrollbars to float above chrome ui */
 pref("ui.scrollbarsCanOverlapContent", 1);
 
+/* turn off the caret blink after 10 cycles */
+pref("ui.caretBlinkCount", 10);
+
 /* cache prefs */
 pref("browser.cache.disk.enable", true);
 pref("browser.cache.disk.capacity", 20480); // kilobytes
 pref("browser.cache.disk.max_entry_size", 4096); // kilobytes
 pref("browser.cache.disk.smart_size.enabled", true);
 pref("browser.cache.disk.smart_size.first_run", true);
 
 #ifdef MOZ_PKG_SPECIAL
@@ -858,15 +861,19 @@ pref("reader.color_scheme", "auto");
 pref("reader.color_scheme.values", "[\"dark\",\"auto\",\"light\"]");
 
 // Whether to use a vertical or horizontal toolbar.
 pref("reader.toolbar.vertical", false);
 
 // Whether or not to display buttons related to reading list in reader view.
 pref("browser.readinglist.enabled", true);
 
+// Telemetry settings.
+// Whether to use the unified telemetry behavior, requires a restart.
+pref("toolkit.telemetry.unified", false);
+
 // Selection carets never fall-back to internal LongTap detector.
 pref("selectioncaret.detects.longtap", false);
 
 // Enable Service workers for Android on non-release builds
 #ifdef NIGHTLY_BUILD
 pref("dom.serviceWorkers.enabled", true);
 #endif
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -99,16 +99,17 @@ import android.nfc.NdefMessage;
 import android.nfc.NdefRecord;
 import android.nfc.NfcAdapter;
 import android.nfc.NfcEvent;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.StrictMode;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
+import android.support.v4.view.MenuItemCompat;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Base64;
 import android.util.Base64OutputStream;
 import android.util.Log;
 import android.view.InputDevice;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
@@ -118,16 +119,17 @@ import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.SubMenu;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.view.ViewTreeObserver;
 import android.view.Window;
 import android.view.animation.Interpolator;
+import android.widget.Button;
 import android.widget.ListView;
 import android.widget.RelativeLayout;
 import android.widget.Toast;
 import android.widget.ViewFlipper;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.ByteArrayOutputStream;
@@ -3033,16 +3035,50 @@ public class BrowserApp extends GeckoApp
         reader.setIcon(resolveReadingListIconID(isPageInReadingList));
         reader.setTitle(resolveReadingListTitleID(isPageInReadingList));
 
         back.setEnabled(tab.canDoBack());
         forward.setEnabled(tab.canDoForward());
         desktopMode.setChecked(tab.getDesktopMode());
         desktopMode.setIcon(tab.getDesktopMode() ? R.drawable.ic_menu_desktop_mode_on : R.drawable.ic_menu_desktop_mode_off);
 
+        View backButtonView = MenuItemCompat.getActionView(back);
+
+        if (backButtonView != null) {
+            backButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
+                @Override
+                public boolean onLongClick(View view) {
+                    Tab tab = Tabs.getInstance().getSelectedTab();
+                    if (tab != null) {
+                        closeOptionsMenu();
+                        return tabHistoryController.showTabHistory(tab,
+                                TabHistoryController.HistoryAction.BACK);
+                    }
+                    return false;
+                }
+            });
+        }
+
+        View forwardButtonView = MenuItemCompat.getActionView(forward);
+
+        if (forwardButtonView != null) {
+            forwardButtonView.setOnLongClickListener(new Button.OnLongClickListener() {
+                @Override
+                public boolean onLongClick(View view) {
+                    Tab tab = Tabs.getInstance().getSelectedTab();
+                    if (tab != null) {
+                        closeOptionsMenu();
+                        return tabHistoryController.showTabHistory(tab,
+                                TabHistoryController.HistoryAction.FORWARD);
+                    }
+                    return false;
+                }
+            });
+        }
+
         String url = tab.getURL();
         if (AboutPages.isAboutReader(url)) {
             String urlFromReader = ReaderModeUtils.getUrlFromAboutReader(url);
             if (urlFromReader != null) {
                 url = urlFromReader;
             }
         }
 
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1270,16 +1270,17 @@ public abstract class GeckoApp
         GeckoScreenOrientation.getInstance().update(getResources().getConfiguration().orientation);
 
         setContentView(getLayout());
 
         // Set up Gecko layout.
         mRootLayout = (OuterLayout) findViewById(R.id.root_layout);
         mGeckoLayout = (RelativeLayout) findViewById(R.id.gecko_layout);
         mMainLayout = (RelativeLayout) findViewById(R.id.main_layout);
+        mLayerView = (LayerView) findViewById(R.id.layer_view);
 
         // Determine whether we should restore tabs.
         mShouldRestore = getSessionRestoreState(savedInstanceState);
         if (mShouldRestore && savedInstanceState != null) {
             boolean wasInBackground =
                 savedInstanceState.getBoolean(SAVED_STATE_IN_BACKGROUND, false);
 
             // Don't log OOM-kills if only one activity was destroyed. (For example
@@ -1409,23 +1410,22 @@ public abstract class GeckoApp
         if (mCameraView == null) {
             // Pre-ICS devices need the camera surface in a visible layout.
             if (Versions.preICS) {
                 mCameraView = new SurfaceView(this);
                 ((SurfaceView)mCameraView).getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
             }
         }
 
-        if (mLayerView == null) {
-            LayerView layerView = (LayerView) findViewById(R.id.layer_view);
-            layerView.initializeView(EventDispatcher.getInstance());
-            mLayerView = layerView;
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createObjectEvent(
-                GeckoEvent.ACTION_OBJECT_LAYER_CLIENT, layerView.getLayerClientObject()));
-        }
+        // XXX our editor tests require the GeckoView to have focus to pass, so we have to
+        // manually shift focus to the GeckoView. requestFocus apparently doesn't work at
+        // this stage of starting up, so we have to unset and reset the focusability.
+        mLayerView.setFocusable(false);
+        mLayerView.setFocusable(true);
+        mLayerView.setFocusableInTouchMode(true);
     }
 
     /**
      * Loads the initial tab at Fennec startup.
      *
      * If Fennec was opened with an external URL, that URL will be loaded.
      * Otherwise, unless there was a session restore, the default URL
      * (about:home) be loaded.
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -117,16 +117,20 @@ public class GeckoView extends LayerView
         a.recycle();
         init(context, url, doInit);
     }
 
     private void init(Context context, String url, boolean doInit) {
         // Perform common initialization for Fennec/GeckoView.
         GeckoAppShell.setLayerView(this);
 
+        initializeView(EventDispatcher.getInstance());
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createObjectEvent(
+                GeckoEvent.ACTION_OBJECT_LAYER_CLIENT, getLayerClientObject()));
+
         // TODO: Fennec currently takes care of its own initialization, so this
         // flag is a hack used in Fennec to prevent GeckoView initialization.
         // This should go away once Fennec also uses GeckoView for
         // initialization.
         if (!doInit)
             return;
 
         // If running outside of a GeckoActivity (eg, from a library project),
@@ -176,23 +180,19 @@ public class GeckoView extends LayerView
             "Link:Favicon",
             "Prompt:Show",
             "Prompt:ShowTop");
 
         EventDispatcher.getInstance().registerGeckoThreadListener(mNativeEventListener,
             "Accessibility:Ready",
             "GeckoView:Message");
 
-        initializeView(EventDispatcher.getInstance());
-
         if (GeckoThread.launch()) {
             // This is the first launch, so finish initialization and go.
             GeckoProfile profile = GeckoProfile.get(context).forceCreate();
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createObjectEvent(
-                GeckoEvent.ACTION_OBJECT_LAYER_CLIENT, getLayerClientObject()));
 
         } else if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
             // If Gecko is already running, that means the Activity was
             // destroyed, so we need to re-attach Gecko to this GeckoView.
             connectToGecko();
         }
     }
 
--- a/mobile/android/base/adjust/AdjustHelper.java
+++ b/mobile/android/base/adjust/AdjustHelper.java
@@ -12,24 +12,27 @@ import com.adjust.sdk.Adjust;
 import com.adjust.sdk.AdjustConfig;
 import com.adjust.sdk.AdjustReferrerReceiver;
 import com.adjust.sdk.LogLevel;
 
 public class AdjustHelper implements AdjustHelperInterface {
     public void onCreate(final Context context, final String maybeAppToken) {
         final String environment;
         final String appToken;
+        final LogLevel logLevel;
         if (maybeAppToken != null) {
             environment = AdjustConfig.ENVIRONMENT_PRODUCTION;
             appToken = maybeAppToken;
+            logLevel = LogLevel.WARN;
         } else {
             environment = AdjustConfig.ENVIRONMENT_SANDBOX;
             appToken = "ABCDEFGHIJKL";
+            logLevel = LogLevel.VERBOSE;
         }
         AdjustConfig config = new AdjustConfig(context, appToken, environment);
-        config.setLogLevel(LogLevel.VERBOSE);
+        config.setLogLevel(logLevel);
         Adjust.onCreate(config);
     }
 
     public void onReceive(final Context context, final Intent intent) {
         new AdjustReferrerReceiver().onReceive(context, intent);
     }
 }
--- a/mobile/android/base/gfx/JavaPanZoomController.java
+++ b/mobile/android/base/gfx/JavaPanZoomController.java
@@ -122,16 +122,18 @@ class JavaPanZoomController
     /* Whether or not to wait for a double-tap before dispatching a single-tap */
     private boolean mWaitForDoubleTap;
     /* Used to change the scrollY direction */
     private boolean mNegateWheelScrollY;
     /* Whether the current event has been default-prevented. */
     private boolean mDefaultPrevented;
     /* Whether longpress events are enabled, or suppressed by robocop tests. */
     private boolean isLongpressEnabled;
+    /* Whether longpress detection should be ignored */
+    private boolean mIgnoreLongPress;
 
     // Handler to be notified when overscroll occurs
     private Overscroll mOverscroll;
 
     public JavaPanZoomController(PanZoomTarget target, View view, EventDispatcher eventDispatcher) {
         mTarget = target;
         mSubscroller = new SubdocumentScrollHelper(eventDispatcher);
         mX = new AxisX(mSubscroller);
@@ -388,21 +390,32 @@ class JavaPanZoomController
             break;
         }
     }
 
     /** This function must be called on the UI thread. */
     public void startingNewEventBlock(MotionEvent event, boolean waitingForTouchListeners) {
         checkMainThread();
         mSubscroller.cancel();
-        if (waitingForTouchListeners && (event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
-            // this is the first touch point going down, so we enter the pending state
-            // setting the state will kill any animations in progress, possibly leaving
-            // the page in overscroll
-            setState(PanZoomState.WAITING_LISTENERS);
+        mIgnoreLongPress = false;
+        if (waitingForTouchListeners) {
+            if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
+                // this is the first touch point going down, so we enter the pending state
+                // setting the state will kill any animations in progress, possibly leaving
+                // the page in overscroll
+                setState(PanZoomState.WAITING_LISTENERS);
+            } else if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_POINTER_DOWN) {
+                // this is a second (or more) touch point going down, and we're waiting for
+                // the content listeners to respond. while we're waiting though we might end
+                // up triggering a long-press from the first touch point, which would be bad
+                // because from the user's point of view they are already in a multi-touch
+                // gesture. to prevent this from happening we set a flag that discards long-press
+                // gesture detections.
+                mIgnoreLongPress = true;
+            }
         }
     }
 
     /** This must be called on the UI thread. */
     @Override
     public void pageRectUpdated() {
         if (mState == PanZoomState.NOTHING) {
             synchronized (mTarget.getLock()) {
@@ -1352,17 +1365,17 @@ class JavaPanZoomController
      * longpress events that are spuriously created on slower test devices.
      */
     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
         this.isLongpressEnabled = isLongpressEnabled;
     }
 
     @Override
     public void onLongPress(MotionEvent motionEvent) {
-        if (!isLongpressEnabled) {
+        if (!isLongpressEnabled || mIgnoreLongPress) {
             return;
         }
 
         GeckoEvent e = GeckoEvent.createLongPressEvent(motionEvent);
         GeckoAppShell.sendEventToGecko(e);
     }
 
     @Override
index 33adefc60ab0140afa15f9e279591fc890c0415e..33faec0e29187f0bab8959c93cd3f4a12c7764ba
GIT binary patch
literal 253
zc%17D@N?(olHy`uVBq!ia0vp^CLqkh3?$9N*lqwRg8-ipS0Md=D#QP2%>QR_{GY-8
zf0gn7X)OPzv;03^0hPM9@&CO||Gzx_|L^br|Ns9SG7JDpaFzu51v5yb9Xj{<+xOpD
zfu|LKg2kRLjv*GOlM@)4W=b?PDkKy|w=xEaI2iRTOaGtTaQh&WgXXh+|051?GyAcz
zUHyJz$v;-cr8WPOS)&_*w%%B}uF%LQqCfW4^%Yk@aKfZRlh1w><*Votv0)Xw_UJX=
rjvq-0MaOl%c!_X`PBq@h#Ky)jJEvsnhT1bA$1-@j`njxgN@xNAclT;}
index 65e2d3e4855ea6975b038a24dcd0a33144007a69..04d6d800f4e865639b5902bdddb7416df4ada74d
GIT binary patch
literal 219
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|l3?zm1T2})ptpJ}8S0MerkKzAxhW|6z{?Fk2
zKZ65Ao^AVorVS)DmEr%Fr~kh^`~Uxcg}qV*P>8c6$S;^dT6)#nckh1v=JQ|10~8GR
zba4#PIG>!5kT9c@MWTQ~ZE~o`fk;LTm8e>A&K&+nDG3{PbTf4|I$5r8infqcTjMIa
z%T&umrBz~&;i72=WsDg+t|h1lyBR+@GDWAs_GIBX7e-+ThShzCk^_sT83L_f@O1Ta
JS?83{1OV=vP00WN
index 11b3e2f242c603aa5924f59a2bb6b9ef0029c609..acb1884d790886fe70c4d699be87fd4a8e0cae8e
GIT binary patch
literal 313
zc%17D@N?(olHy`uVBq!ia0vp^E+EXo3?%u_CN}^nn*g5>S0Merf#LrYFq#ZwK+seW
z2~q%Lth4=pyz2kGjsNd$`v2wW|9^k~|NozH^pQJIn!6;(FPMQ%yMOYOJy))N`Wv%4
z<Q7nPp{I*uNX4zUHxBYOD+ss-vTS7%7yS9ZUU#Esto4N@-8(ni|2BW7XLsh0%4@Up
zhyOfVvvc;dh7|_>@yV`tZcWLE;+{~nl3&}B@iY5I#&4UstjamQIms+Z>zKmw&dJPT
z)drsR+#ds$crfNCSuf~f7726_bVU&VgkQ@0E`ApMe}as0=KI3hlAhOr#``t9|2#B!
g^wn_3jft6i898zkD~!v6{(=1H>FVdQ&MBb@0DQ)V4gdfE
index 02a55e12a51a13772bf95c7d37cb18d8c5fd6174..d304f3c5dfcb25def307619e71dae91157346575
GIT binary patch
literal 431
zc%17D@N?(olHy`uVBq!ia0vp^DIm<j3?$ucQqzExUw}`DE0F#_nc@E=FgXQ=rlOKl
z89>T_jOlFumudfBuJivw|NjdU{$HB%|MA}cZ?FFU_4)t5zyJULH{{4(2(q#y$S;^d
zO2#icZ}zsEpFaQj_u{+EW(EdEW=|K#kcwMxZ*7!nGT>o(AU1)?E7WDd`~Qp7E;5$B
zo%%HI)s+9*C8u}J?}>cG=X<p!uVdz=@`Wq^aonGm{A^D2|J*CiEKAmZKlV~cap#<}
z?*@<UbhA%QDV_Ol*^5{=(>-;oU+-DE=H|22b%y^PqfF;%R?l0tC4b4Ka#MAOwA$pU
zioO5(c^h-gCGScIJ^A7>!N<r-;?l{?g9_a}?|3e5-<mWb)Q!K*#gj?Zi$$_iK+&m%
zLr}@30~;fVqp)Df5u?re+o$ms&Dy}7_Gi_e6PNbK)E@jP7jxuBxR=}xFV1~3&Fg<Y
m;P$@p>T1H()yhUqk973CZ+UJ1llHv?6sDf8elF{r5}E+s6vXWS
--- a/mobile/android/config/mozconfigs/common
+++ b/mobile/android/config/mozconfigs/common
@@ -32,16 +32,17 @@ ac_add_options --with-android-gnu-compil
 ac_add_options --with-android-version=9
 ac_add_options --with-system-zlib
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 
 # Treat warnings as errors in directories with FAIL_ON_WARNINGS.
 ac_add_options --enable-warnings-as-errors
 
 ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-fennec-geoloc-api.key
+ac_add_options --with-adjust-sdk-keyfile=/builds/adjust-sdk.token
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 # Use ccache
 . "$topsrcdir/build/mozconfig.cache"
 
 HOST_CC="$topsrcdir/gcc/bin/gcc"
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -63,17 +63,17 @@ dependencies {
 
     compile project(':branding')
     compile project(':preprocessed_code')
     compile project(':preprocessed_resources')
     compile project(':thirdparty')
 }
 
 android.libraryVariants.all { variant ->
-    variant.checkManifest.dependsOn generateCodeAndResources
+    variant.checkManifest.dependsOn rootProject.generateCodeAndResources
 }
 
 apply plugin: 'idea'
 
 idea {
     module {
         excludeDirs += file('org/mozilla/gecko/resources')
         excludeDirs += file('org/mozilla/gecko/tests')
--- a/mobile/android/gradle/build.gradle
+++ b/mobile/android/gradle/build.gradle
@@ -21,27 +21,26 @@ buildscript {
         classpath 'com.android.tools.build:gradle:1.0.0'
     }
 }
 
 repositories {
     jcenter()
 }
 
-subprojects {
-    task generateCodeAndResources(type:Exec) {
-        workingDir "${topobjdir}"
+task generateCodeAndResources(type:Exec) {
+    workingDir "${topobjdir}"
 
-        commandLine "${topsrcdir}/mach"
-        args 'build'
-        args 'mobile/android/base/gradle-targets'
+    commandLine mozconfig.substs.GMAKE
+    args '-C'
+    args "${topobjdir}/mobile/android/base"
+    args 'gradle-targets'
 
-        // Only show the output if something went wrong.
-        ignoreExitValue = true
-        standardOutput = new ByteArrayOutputStream()
-        errorOutput = standardOutput
-        doLast {
-            if (execResult.exitValue != 0) {
-                throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
-            }
+    // Only show the output if something went wrong.
+    ignoreExitValue = true
+    standardOutput = new ByteArrayOutputStream()
+    errorOutput = standardOutput
+    doLast {
+        if (execResult.exitValue != 0) {
+            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
         }
     }
 }
--- a/mobile/android/gradle/omnijar/build.gradle
+++ b/mobile/android/gradle/omnijar/build.gradle
@@ -1,32 +1,31 @@
 apply plugin: 'java'
 
 /**
  * This task runs when any input file is newer than the omnijar.
  */
 task buildOmnijar(type:Exec) {
-    dependsOn generateCodeAndResources
+    dependsOn rootProject.generateCodeAndResources
 
     // Depend on all the Gecko resources.
     inputs.sourceDir 'src/main/java/locales'
     inputs.sourceDir 'src/main/java/chrome'
     inputs.sourceDir 'src/main/java/components'
     inputs.sourceDir 'src/main/java/modules'
     inputs.sourceDir 'src/main/java/themes'
 
     // Produce a single output file.
     outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
 
     workingDir "${topobjdir}"
 
-    commandLine "${topsrcdir}/mach"
-    args 'build'
+    commandLine mozconfig.substs.GMAKE
     args '-C'
-    args 'mobile/android/base'
+    args "${topobjdir}/mobile/android/base"
     args 'gradle-omnijar'
 
     // Only show the output if something went wrong.
     ignoreExitValue = true
     standardOutput = new ByteArrayOutputStream()
     errorOutput = standardOutput
     doLast {
         if (execResult.exitValue != 0) {
--- a/mobile/android/gradle/preprocessed_code/build.gradle
+++ b/mobile/android/gradle/preprocessed_code/build.gradle
@@ -1,9 +1,9 @@
-apply plugin: 'android-library'
+apply plugin: 'com.android.library'
 
 android {
     compileSdkVersion 21
     buildToolsVersion "21.1.1"
 
     defaultConfig {
         targetSdkVersion 21
         minSdkVersion 9
@@ -32,17 +32,17 @@ android {
                     exclude 'AdjustHelper.java'
                 }
             }
         }
     }
 }
 
 android.libraryVariants.all { variant ->
-    variant.checkManifest.dependsOn generateCodeAndResources
+    variant.checkManifest.dependsOn rootProject.generateCodeAndResources
 }
 
 
 dependencies {
     if (mozconfig.substs.MOZ_INSTALL_TRACKING) {
         compile project(':thirdparty_adjust_sdk')
     }
 }
--- a/mobile/android/gradle/preprocessed_resources/build.gradle
+++ b/mobile/android/gradle/preprocessed_resources/build.gradle
@@ -19,14 +19,14 @@ android {
     }
 
     lintOptions {
         abortOnError false
     }
 }
 
 android.libraryVariants.all { variant ->
-    variant.checkManifest.dependsOn generateCodeAndResources
+    variant.checkManifest.dependsOn rootProject.generateCodeAndResources
 }
 
 dependencies {
     compile project(':branding')
 }
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -174,17 +174,17 @@ pref("dom.undo_manager.enabled", false);
 
 // Whether URL,nsLocation,Link::GetHash should be percent encoded
 // in setter and percent decoded in getter (old behaviour = true)
 pref("dom.url.encode_decode_hash", true);
 
 // Whether to run add-on code in different compartments from browser code. This
 // causes a separate compartment for each (addon, global) combination, which may
 // significantly increase the number of compartments in the system.
-#ifdef NIGHTLY_BUILD
+#ifdef E10S_TESTING_ONLY
 pref("dom.compartment_per_addon", true);
 #else
 pref("dom.compartment_per_addon", false);
 #endif
 
 // Fastback caching - if this pref is negative, then we calculate the number
 // of content viewers to cache based on the amount of available memory.
 pref("browser.sessionhistory.max_total_viewers", -1);
@@ -766,50 +766,54 @@ pref("toolkit.autocomplete.richBoundaryC
 pref("toolkit.osfile.log", false);
 
 pref("toolkit.scrollbox.smoothScroll", true);
 pref("toolkit.scrollbox.scrollIncrement", 20);
 pref("toolkit.scrollbox.verticalScrollDistance", 3);
 pref("toolkit.scrollbox.horizontalScrollDistance", 5);
 pref("toolkit.scrollbox.clickToScroll.scrollDelay", 150);
 
+// Telemetry settings.
+// Server to submit telemetry pings to.
 pref("toolkit.telemetry.server", "https://incoming.telemetry.mozilla.org");
 // Telemetry server owner. Please change if you set toolkit.telemetry.server to a different server
 pref("toolkit.telemetry.server_owner", "Mozilla");
 // Information page about telemetry (temporary ; will be about:telemetry in the end)
 pref("toolkit.telemetry.infoURL", "https://www.mozilla.org/legal/privacy/firefox.html#telemetry");
 // Determines whether full SQL strings are returned when they might contain sensitive info
 // i.e. dynamically constructed SQL strings or SQL executed by addons against addon DBs
 pref("toolkit.telemetry.debugSlowSql", false);
+// Whether to use the unified telemetry behavior, requires a restart.
+pref("toolkit.telemetry.unified", true);
 
 // Identity module
 pref("toolkit.identity.enabled", false);
 pref("toolkit.identity.debug", false);
 
 // AsyncShutdown delay before crashing in case of shutdown freeze
 pref("toolkit.asyncshutdown.timeout.crash", 60000);
 
 // Enable deprecation warnings.
 pref("devtools.errorconsole.deprecation_warnings", true);
 
 // Disable debugging chrome
 #ifdef MOZ_DEV_EDITION
-pref("devtools.chrome.enabled", true);
+sticky_pref("devtools.chrome.enabled", true);
 #else
-pref("devtools.chrome.enabled", false);
+sticky_pref("devtools.chrome.enabled", false);
 #endif
 
 // Disable remote debugging protocol logging
 pref("devtools.debugger.log", false);
 pref("devtools.debugger.log.verbose", false);
 // Disable remote debugging connections
 #ifdef MOZ_DEV_EDITION
-pref("devtools.debugger.remote-enabled", true);
+sticky_pref("devtools.debugger.remote-enabled", true);
 #else
-pref("devtools.debugger.remote-enabled", false);
+sticky_pref("devtools.debugger.remote-enabled", false);
 #endif
 pref("devtools.debugger.remote-port", 6000);
 // Force debugger server binding on the loopback interface
 pref("devtools.debugger.force-local", true);
 // Display a prompt when a new connection starts to accept/reject it
 pref("devtools.debugger.prompt-connection", true);
 // Block tools from seeing / interacting with certified apps
 pref("devtools.debugger.forbid-certified-apps", true);
@@ -2429,17 +2433,17 @@ pref("dom.ipc.plugins.flash.subprocess.c
 pref("dom.ipc.plugins.reportCrashURL", true);
 
 // How long we wait before unloading an idle plugin process.
 // Defaults to 30 seconds.
 pref("dom.ipc.plugins.unloadTimeoutSecs", 30);
 
 // Asynchronous plugin initialization should only be enabled on non-e10s
 // channels until some remaining bugs are resolved.
-#ifdef NIGHTLY_BUILD
+#ifdef E10S_TESTING_ONLY
 pref("dom.ipc.plugins.asyncInit", false);
 #else
 pref("dom.ipc.plugins.asyncInit", true);
 #endif
 
 pref("dom.ipc.processCount", 1);
 
 // Enable caching of Moz2D Path objects for SVG geometry elements
@@ -4109,16 +4113,17 @@ pref("browser.history.allowPushState", t
 pref("browser.history.allowReplaceState", true);
 pref("browser.history.allowPopState", true);
 pref("browser.history.maxStateObjectSize", 655360);
 
 // XPInstall prefs
 pref("xpinstall.whitelist.required", true);
 // Only Firefox requires add-on signatures
 pref("xpinstall.signatures.required", false);
+pref("xpinstall.signatures.infoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
 pref("extensions.alwaysUnpack", false);
 pref("extensions.minCompatiblePlatformVersion", "2.0");
 
 pref("network.buffer.cache.count", 24);
 pref("network.buffer.cache.size",  32768);
 
 // Desktop Notification
 pref("notification.feature.enabled", false);
--- a/modules/libpref/prefapi.cpp
+++ b/modules/libpref/prefapi.cpp
@@ -135,17 +135,18 @@ struct CallbackNode {
     struct CallbackNode*    next;
 };
 
 /* -- Prototypes */
 static nsresult pref_DoCallback(const char* changed_pref);
 
 enum {
     kPrefSetDefault = 1,
-    kPrefForceSet = 2
+    kPrefForceSet = 2,
+    kPrefStickyDefault = 4,
 };
 static nsresult pref_HashPref(const char *key, PrefValue value, PrefType type, uint32_t flags);
 
 #define PREF_HASHTABLE_INITIAL_LENGTH   1024
 
 nsresult PREF_Init()
 {
     if (!gHashTable.IsInitialized()) {
@@ -335,17 +336,18 @@ pref_savePref(PLDHashTable *table, PLDHa
 
     // where we're getting our pref from
     PrefValue* sourcePref;
 
     if (PREF_HAS_USER_VALUE(pref) &&
         (pref_ValueChanged(pref->defaultPref,
                            pref->userPref,
                            (PrefType) PREF_TYPE(pref)) ||
-         !(pref->flags & PREF_HAS_DEFAULT))) {
+         !(pref->flags & PREF_HAS_DEFAULT) ||
+         pref->flags & PREF_STICKY_DEFAULT)) {
         sourcePref = &pref->userPref;
     } else {
         if (argData->saveTypes == SAVE_ALL_AND_DEFAULTS) {
             prefPrefix.AssignLiteral("pref(\"");
             sourcePref = &pref->defaultPref;
         }
         else
             // do not save default prefs that haven't changed
@@ -773,28 +775,32 @@ nsresult pref_HashPref(const char *key, 
     {
         if (!PREF_IS_LOCKED(pref))
         {       /* ?? change of semantics? */
             if (pref_ValueChanged(pref->defaultPref, value, type) ||
                 !(pref->flags & PREF_HAS_DEFAULT))
             {
                 pref_SetValue(&pref->defaultPref, &pref->flags, value, type);
                 pref->flags |= PREF_HAS_DEFAULT;
+                if (flags & kPrefStickyDefault)
+                    pref->flags |= PREF_STICKY_DEFAULT;
                 if (!PREF_HAS_USER_VALUE(pref))
                     valueChanged = true;
             }
             // What if we change the default to be the same as the user value?
             // Should we clear the user value?
         }
     }
     else
     {
-        /* If new value is same as the default value, then un-set the user value.
+        /* If new value is same as the default value and it's not a "sticky"
+           pref, then un-set the user value.
            Otherwise, set the user value only if it has changed */
         if ((pref->flags & PREF_HAS_DEFAULT) &&
+            !(pref->flags & PREF_STICKY_DEFAULT) &&
             !pref_ValueChanged(pref->defaultPref, value, type) &&
             !(flags & kPrefForceSet))
         {
             if (PREF_HAS_USER_VALUE(pref))
             {
                 /* XXX should we free a user-set string value if there is one? */
                 pref->flags &= ~PREF_USERSET;
                 if (!PREF_IS_LOCKED(pref)) {
@@ -997,12 +1003,17 @@ static nsresult pref_DoCallback(const ch
 
     return rv;
 }
 
 void PREF_ReaderCallback(void       *closure,
                          const char *pref,
                          PrefValue   value,
                          PrefType    type,
-                         bool        isDefault)
+                         bool        isDefault,
+                         bool        isStickyDefault)
 {
-    pref_HashPref(pref, value, type, isDefault ? kPrefSetDefault : kPrefForceSet);
+    uint32_t flags = isDefault ? kPrefSetDefault : kPrefForceSet;
+    if (isDefault && isStickyDefault) {
+        flags |= kPrefStickyDefault;
+    }
+    pref_HashPref(pref, value, type, flags);
 }
--- a/modules/libpref/prefapi.h
+++ b/modules/libpref/prefapi.h
@@ -56,16 +56,18 @@ void        PREF_CleanupPrefs();
 // Preference flags, including the native type of the preference
 // </font>
 */
 
 typedef enum { PREF_INVALID = 0,
                PREF_LOCKED = 1, PREF_USERSET = 2, PREF_CONFIG = 4, PREF_REMOTE = 8,
                PREF_LILOCAL = 16, PREF_STRING = 32, PREF_INT = 64, PREF_BOOL = 128,
                PREF_HAS_DEFAULT = 256,
+               // pref is default pref with "sticky" semantics
+               PREF_STICKY_DEFAULT = 512,
                PREF_VALUETYPE_MASK = (PREF_STRING | PREF_INT | PREF_BOOL)
              } PrefType;
 
 /*
 // <font color=blue>
 // Set the various types of preferences.  These functions take a dotted
 // notation of the preference name (e.g. "browser.startup.homepage").  
 // Note that this will cause the preference to be saved to the file if
@@ -178,14 +180,15 @@ nsresult PREF_UnregisterCallback( const 
 
 /*
  * Used by nsPrefService as the callback function of the 'pref' parser
  */
 void PREF_ReaderCallback( void *closure,
                           const char *pref,
                           PrefValue   value,
                           PrefType    type,
-                          bool        isDefault);
+                          bool        isDefault,
+                          bool        isStickyDefault);
 
 #ifdef __cplusplus
 }
 #endif
 #endif
--- a/modules/libpref/prefread.cpp
+++ b/modules/libpref/prefread.cpp
@@ -39,16 +39,17 @@ enum {
 };
 
 #define UTF16_ESC_NUM_DIGITS    4
 #define HEX_ESC_NUM_DIGITS      2
 #define BITS_PER_HEX_DIGIT      4
 
 static const char kUserPref[] = "user_pref";
 static const char kPref[] = "pref";
+static const char kPrefSticky[] = "sticky_pref";
 static const char kTrue[] = "true";
 static const char kFalse[] = "false";
 
 /**
  * pref_GrowBuf
  * 
  * this function will increase the size of the buffer owned
  * by the given pref parse state.  We currently use a simple
@@ -124,17 +125,18 @@ pref_DoCallback(PrefParseState *ps)
         value.intVal = atoi(ps->vb);
         break;
     case PREF_BOOL:
         value.boolVal = (ps->vb == kTrue);
         break;
     default:
         break;
     }
-    (*ps->reader)(ps->closure, ps->lb, value, ps->vtype, ps->fdefault);
+    (*ps->reader)(ps->closure, ps->lb, value, ps->vtype, ps->fdefault,
+                  ps->fstickydefault);
     return true;
 }
 
 void
 PREF_InitParseState(PrefParseState *ps, PrefReader reader, void *closure)
 {
     memset(ps, 0, sizeof(*ps));
     ps->reader = reader;
@@ -183,27 +185,30 @@ PREF_ParseBuf(PrefParseState *ps, const 
         switch (state) {
         /* initial state */
         case PREF_PARSE_INIT:
             if (ps->lbcur != ps->lb) { /* reset state */
                 ps->lbcur = ps->lb;
                 ps->vb    = nullptr;
                 ps->vtype = PREF_INVALID;
                 ps->fdefault = false;
+                ps->fstickydefault = false;
             }
             switch (c) {
             case '/':       /* begin comment block or line? */
                 state = PREF_PARSE_COMMENT_MAYBE_START;
                 break;
             case '#':       /* accept shell style comments */
                 state = PREF_PARSE_UNTIL_EOL;
                 break;
             case 'u':       /* indicating user_pref */
             case 'p':       /* indicating pref */
-                ps->smatch = (c == 'u' ? kUserPref : kPref);
+            case 's':       /* indicating sticky_pref */
+                ps->smatch = (c == 'u' ? kUserPref :
+                             (c == 's' ? kPrefSticky : kPref));
                 ps->sindex = 1;
                 ps->nextstate = PREF_PARSE_UNTIL_OPEN_PAREN;
                 state = PREF_PARSE_MATCH_STRING;
                 break;
             /* else skip char */
             }
             break;
 
@@ -237,17 +242,18 @@ PREF_ParseBuf(PrefParseState *ps, const 
             }
             else
                 *ps->lbcur++ = c;
             break;
 
         /* name parsing */
         case PREF_PARSE_UNTIL_NAME:
             if (c == '\"' || c == '\'') {
-                ps->fdefault = (ps->smatch == kPref);
+                ps->fdefault = (ps->smatch == kPref || ps->smatch == kPrefSticky);
+                ps->fstickydefault = (ps->smatch == kPrefSticky);
                 ps->quotechar = c;
                 ps->nextstate = PREF_PARSE_UNTIL_COMMA; /* return here when done */
                 state = PREF_PARSE_QUOTED_STRING;
             }
             else if (c == '/') {       /* allow embedded comment */
                 ps->nextstate = state; /* return here when done with comment */
                 state = PREF_PARSE_COMMENT_MAYBE_START;
             }
--- a/modules/libpref/prefread.h
+++ b/modules/libpref/prefread.h
@@ -21,22 +21,25 @@ extern "C" {
  * @param pref
  *        preference name
  * @param val
  *        preference value
  * @param type
  *        preference type (PREF_STRING, PREF_INT, or PREF_BOOL)
  * @param defPref
  *        preference type (true: default, false: user preference)
+ * @param stickyPref
+ *        default preference marked as a "sticky" pref
  */
 typedef void (*PrefReader)(void       *closure,
                            const char *pref,
                            PrefValue   val,
                            PrefType    type,
-                           bool        defPref);
+                           bool        defPref,
+                           bool        stickyPref);
 
 /* structure fields are private */
 typedef struct PrefParseState {
     PrefReader  reader;
     void       *closure;
     int         state;      /* PREF_PARSE_...                */
     int         nextstate;  /* sometimes used...             */
     const char *smatch;     /* string to match               */
@@ -47,16 +50,17 @@ typedef struct PrefParseState {
     char        esctmp[6];  /* raw escape to put back if err */
     char        quotechar;  /* char delimiter for quotations */
     char       *lb;         /* line buffer (only allocation) */
     char       *lbcur;      /* line buffer cursor            */
     char       *lbend;      /* line buffer end               */
     char       *vb;         /* value buffer (ptr into lb)    */
     PrefType    vtype;      /* PREF_STRING,INT,BOOL          */
     bool        fdefault;   /* true if (default) pref     */
+    bool        fstickydefault; /* true if (sticky) pref     */
 } PrefParseState;
 
 /**
  * PREF_InitParseState
  *
  * Called to initialize a PrefParseState instance.
  * 
  * @param ps
new file mode 100644
--- /dev/null
+++ b/modules/libpref/test/unit/data/testPrefSticky.js
@@ -0,0 +1,2 @@
+pref("testPref.unsticky.bool", true);
+sticky_pref("testPref.sticky.bool", false);
new file mode 100644
--- /dev/null
+++ b/modules/libpref/test/unit/data/testPrefStickyUser.js
@@ -0,0 +1,5 @@
+// testPrefSticky.js defined this pref as a sticky_pref(). Once a sticky
+// pref has been changed, it's written as a user_pref().
+// So this test file reflects that scenario.
+// Note the default in testPrefSticky.js is also false.
+user_pref("testPref.sticky.bool", false);
new file mode 100644
--- /dev/null
+++ b/modules/libpref/test/unit/test_stickyprefs.js
@@ -0,0 +1,170 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/  */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const ps = Services.prefs;
+
+// Once we fetch the profile directory the xpcshell test harness will send
+// a profile-before-change notification at shutdown. This causes the prefs
+// service to flush the prefs file - and the prefs file it uses ends up being
+// testPrefSticky*.js in the test dir. This upsets things in confusing ways :)
+// We avoid this by ensuring our "temp" prefs.js is the current prefs file.
+do_get_profile();
+do_register_cleanup(saveAndReload);
+
+// A little helper to reset the service and load some pref files
+function resetAndLoad(filenames) {
+  ps.resetPrefs();
+  for (let filename of filenames) {
+    ps.readUserPrefs(do_get_file(filename));
+  }
+}
+
+// A little helper that saves the current state to a file in the profile
+// dir, then resets the service and re-reads the file it just saved.
+// Used to test what gets actually written - things the pref service decided
+// not to write don't exist at all after this call.
+function saveAndReload() {
+  let file = do_get_profile();
+  file.append("prefs.js");
+  ps.savePrefFile(file);
+
+  // Now reset the pref service and re-read what we saved.
+  ps.resetPrefs();
+  ps.readUserPrefs(file);
+}
+
+function run_test() {
+  run_next_test();
+}
+
+// A sticky pref should not be written if the value is unchanged.
+add_test(function notWrittenWhenUnchanged() {
+  resetAndLoad(["data/testPrefSticky.js"]);
+  Assert.strictEqual(ps.getBoolPref("testPref.unsticky.bool"), true);
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false);
+
+  // write prefs - but we haven't changed the sticky one, so it shouldn't be written.
+  saveAndReload();
+  // sticky should not have been written to the new file.
+  try {
+    ps.getBoolPref("testPref.sticky.bool");
+    Assert.ok(false, "expected failure reading this pref");
+  } catch (ex) {
+    Assert.ok(ex, "exception reading regular pref");
+  }
+  run_next_test();
+});
+
+// Loading a sticky_pref then a user_pref for the same pref means it should
+// always be written.
+add_test(function writtenOnceLoadedWithoutChange() {
+  // Load the same pref file *as well as* a pref file that has a user_pref for
+  // our sticky with the default value. It should be re-written without us
+  // touching it.
+  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  // reset and re-read what we just wrote - it should be written.
+  saveAndReload();
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false,
+                     "user_pref was written with default value");
+  run_next_test();
+});
+
+// If a sticky pref is explicicitly changed, even to the default, it is written.
+add_test(function writtenOnceLoadedWithChangeNonDefault() {
+  // Load the same pref file *as well as* a pref file that has a user_pref for
+  // our sticky - then change the pref. It should be written.
+  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  // Set a new val and check we wrote it.
+  ps.setBoolPref("testPref.sticky.bool", false);
+  saveAndReload();
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false,
+                     "user_pref was written with custom value");
+  run_next_test();
+});
+
+// If a sticky pref is changed to the non-default value, it is written.
+add_test(function writtenOnceLoadedWithChangeNonDefault() {
+  // Load the same pref file *as well as* a pref file that has a user_pref for
+  // our sticky - then change the pref. It should be written.
+  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  // Set a new val and check we wrote it.
+  ps.setBoolPref("testPref.sticky.bool", true);
+  saveAndReload();
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), true,
+                     "user_pref was written with custom value");
+  run_next_test();
+});
+
+// Test that prefHasUserValue always returns true whenever there is a sticky
+// value, even when that value matches the default. This is mainly for
+// about:config semantics - prefs with a sticky value always remain bold and
+// always offer "reset" (which fully resets and drops the sticky value as if
+// the pref had never changed.)
+add_test(function hasUserValue() {
+  // sticky pref without user value.
+  resetAndLoad(["data/testPrefSticky.js"]);
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false);
+  Assert.ok(!ps.prefHasUserValue("testPref.sticky.bool"),
+            "should not initially reflect a user value");
+
+  ps.setBoolPref("testPref.sticky.bool", false);
+  Assert.ok(ps.prefHasUserValue("testPref.sticky.bool"),
+            "should reflect a user value after set to default");
+
+  ps.setBoolPref("testPref.sticky.bool", true);
+  Assert.ok(ps.prefHasUserValue("testPref.sticky.bool"),
+            "should reflect a user value after change to non-default");
+
+  ps.clearUserPref("testPref.sticky.bool");
+  Assert.ok(!ps.prefHasUserValue("testPref.sticky.bool"),
+            "should reset to no user value");
+  ps.setBoolPref("testPref.sticky.bool", false, "expected default");
+
+  // And make sure the pref immediately reflects a user value after load.
+  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  Assert.strictEqual(ps.getBoolPref("testPref.sticky.bool"), false);
+  Assert.ok(ps.prefHasUserValue("testPref.sticky.bool"),
+            "should have a user value when loaded value is the default");
+  run_next_test();
+});
+
+// Test that clearUserPref removes the "sticky" value.
+add_test(function clearUserPref() {
+  // load things such that we have a sticky value which is the same as the
+  // default.
+  resetAndLoad(["data/testPrefSticky.js", "data/testPrefStickyUser.js"]);
+  ps.clearUserPref("testPref.sticky.bool");
+
+  // Once we save prefs the sticky pref should no longer be written.
+  saveAndReload();
+  try {
+    ps.getBoolPref("testPref.sticky.bool");
+    Assert.ok(false, "expected failure reading this pref");
+  } catch (ex) {
+    Assert.ok(ex, "pref doesn't have a sticky value");
+  }
+  run_next_test();
+});
+
+// Test that a pref observer gets a notification fired when a sticky pref
+// has it's value changed to the same value as the default. The reason for
+// this behaviour is that later we might have other code that cares about a
+// pref being sticky (IOW, we notify due to the "state" of the pref changing
+// even if the value has not)
+add_test(function observerFires() {
+  // load things so there's no sticky value.
+  resetAndLoad(["data/testPrefSticky.js"]);
+
+  function observe(subject, topic, data) {
+    Assert.equal(data, "testPref.sticky.bool");
+    ps.removeObserver("testPref.sticky.bool", observe);
+    run_next_test();
+  }
+  ps.addObserver("testPref.sticky.bool", observe, false);
+
+  ps.setBoolPref("testPref.sticky.bool", ps.getBoolPref("testPref.sticky.bool"));
+  // and the observer will fire triggering the next text.
+});
--- a/modules/libpref/test/unit/xpcshell.ini
+++ b/modules/libpref/test/unit/xpcshell.ini
@@ -6,12 +6,14 @@ support-files =
   data/testPref.js
   extdata/testExt.js
 
 [test_warnings.js]
 [test_bug345529.js]
 [test_bug506224.js]
 [test_bug577950.js]
 [test_bug790374.js]
+[test_stickyprefs.js]
+support-files = data/testPrefSticky.js data/testPrefStickyUser.js
 [test_changeType.js]
 [test_dirtyPrefs.js]
 [test_extprefs.js]
 [test_libPrefs.js]
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -1063,16 +1063,22 @@ FxAccountsInternal.prototype = {
     log.debug("Notifying observers of " + topic);
     Services.obs.notifyObservers(null, topic, data);
   },
 
   // XXX - pollEmailStatus should maybe be on the AccountState object?
   pollEmailStatus: function pollEmailStatus(currentState, sessionToken, why) {
     log.debug("entering pollEmailStatus: " + why);
     if (why == "start") {
+      if (this.currentTimer) {
+        log.debug("pollEmailStatus starting while existing timer is running");
+        clearTimeout(this.currentTimer);
+        this.currentTimer = null;
+      }
+
       // If we were already polling, stop and start again.  This could happen
       // if the user requested the verification email to be resent while we
       // were already polling for receipt of an earlier email.
       this.pollStartDate = Date.now();
       if (!currentState.whenVerifiedDeferred) {
         currentState.whenVerifiedDeferred = Promise.defer();
         // This deferred might not end up with any handlers (eg, if sync
         // is yet to start up.)  This might cause "A promise chain failed to
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -34,21 +34,16 @@ Services.prefs.setCharPref("identity.fxa
 
 
 const PROFILE_SERVER_URL = "http://example.com/v1";
 const CONTENT_URL = "http://accounts.example.com/";
 
 Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", PROFILE_SERVER_URL);
 Services.prefs.setCharPref("identity.fxaccounts.settings.uri", CONTENT_URL);
 
-
-function run_test() {
-  run_next_test();
-}
-
 /*
  * The FxAccountsClient communicates with the remote Firefox
  * Accounts auth server.  Mock the server calls, with a little
  * lag time to simulate some latency.
  *
  * We add the _verified attribute to mock the change in verification
  * state on the FXA server.
  */
@@ -259,52 +254,46 @@ add_task(function* test_getCertificate()
       do_check_eq(err, "Error: OFFLINE");
     }
   );
 });
 
 
 // Sanity-check that our mocked client is working correctly
 add_test(function test_client_mock() {
-  do_test_pending();
-
   let fxa = new MockFxAccounts();
   let client = fxa.internal.fxAccountsClient;
   do_check_eq(client._verified, false);
   do_check_eq(typeof client.signIn, "function");
 
   // The recoveryEmailStatus function eventually fulfills its promise
   client.recoveryEmailStatus()
     .then(response => {
       do_check_eq(response.verified, false);
-      do_test_finished();
       run_next_test();
     });
 });
 
 // Sign in a user, and after a little while, verify the user's email.
 // Right after signing in the user, we should get the 'onlogin' notification.
 // Polling should detect that the email is verified, and eventually
 // 'onverified' should be observed
 add_test(function test_verification_poll() {
-  do_test_pending();
-
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("francine");
   let login_notification_received = false;
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
     log.debug("test_verification_poll observed onverified");
     // Once email verification is complete, we will observe onverified
     fxa.internal.getUserAccountData().then(user => {
       // And confirm that the user's state has changed
       do_check_eq(user.verified, true);
       do_check_eq(user.email, test_user.email);
       do_check_true(login_notification_received);
-      do_test_finished();
       run_next_test();
     });
   });
 
   makeObserver(ONLOGIN_NOTIFICATION, function() {
     log.debug("test_verification_poll observer onlogin");
     login_notification_received = true;
   });
@@ -321,18 +310,16 @@ add_test(function test_verification_poll
     });
   });
 });
 
 // Sign in the user, but never verify the email.  The check-email
 // poll should time out.  No verifiedlogin event should be observed, and the
 // internal whenVerified promise should be rejected
 add_test(function test_polling_timeout() {
-  do_test_pending();
-
   // This test could be better - the onverified observer might fire on
   // somebody else's stack, and we're not making sure that we're not receiving
   // such a message. In other words, this tests either failure, or success, but
   // not both.
 
   let fxa = new MockFxAccounts();
   let test_user = getTestUser("carol");
 
@@ -346,25 +333,23 @@ add_test(function test_polling_timeout()
 
   fxa.setSignedInUser(test_user).then(() => {
     p.then(
       (success) => {
         do_throw("this should not succeed");
       },
       (fail) => {
         removeObserver();
-        do_test_finished();
-        run_next_test();
+        fxa.signOut().then(run_next_test);
       }
     );
   });
 });
 
 add_test(function test_getKeys() {
-  do_test_pending();
   let fxa = new MockFxAccounts();
   let user = getTestUser("eusebius");
 
   // Once email has been verified, we will be able to get keys
   user.verified = true;
 
   fxa.setSignedInUser(user).then(() => {
     fxa.getSignedInUser().then((user) => {
@@ -379,36 +364,32 @@ add_test(function test_getKeys() {
         fxa.getSignedInUser().then((user) => {
           // Now we should have keys
           do_check_eq(fxa.internal.isUserEmailVerified(user), true);
           do_check_eq(!!user.verified, true);
           do_check_eq(user.kA, expandHex("11"));
           do_check_eq(user.kB, expandHex("66"));
           do_check_eq(user.keyFetchToken, undefined);
           do_check_eq(user.unwrapBKey, undefined);
-          do_test_finished();
           run_next_test();
         });
       });
     });
   });
 });
 
 //  fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
 add_test(function test_fetchAndUnwrapKeys_no_token() {
-  do_test_pending();
-
   let fxa = new MockFxAccounts();
   let user = getTestUser("lettuce.protheroe");
   delete user.keyFetchToken
 
   makeObserver(ONLOGOUT_NOTIFICATION, function() {
     log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
     fxa.internal.getUserAccountData().then(user => {
-      do_test_finished();
       run_next_test();
     });
   });
 
   fxa.setSignedInUser(user).then(
     user => {
       return fxa.internal.fetchAndUnwrapKeys();
     }
@@ -419,29 +400,26 @@ add_test(function test_fetchAndUnwrapKey
     }
   )
 });
 
 // Alice (User A) signs up but never verifies her email.  Then Bob (User B)
 // signs in with a verified email.  Ensure that no sign-in events are triggered
 // on Alice's behalf.  In the end, Bob should be the signed-in user.
 add_test(function test_overlapping_signins() {
-  do_test_pending();
-
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   let bob = getTestUser("bob");
 
   makeObserver(ONVERIFIED_NOTIFICATION, function() {
     log.debug("test_overlapping_signins observed onverified");
     // Once email verification is complete, we will observe onverified
     fxa.internal.getUserAccountData().then(user => {
       do_check_eq(user.email, bob.email);
       do_check_eq(user.verified, true);
-      do_test_finished();
       run_next_test();
     });
   });
 
   // Alice is the user signing in; her email is unverified.
   fxa.setSignedInUser(alice).then(() => {
     log.debug("Alice signing in ...");
     fxa.internal.getUserAccountData().then(user => {
@@ -609,17 +587,17 @@ add_test(function test_accountStatus() {
             (result) => {
                // FxAccounts.accountStatus() should match Client.accountStatus()
                do_check_true(result);
                fxa.internal.fxAccountsClient._deletedOnServer = true;
                fxa.accountStatus().then(
                  (result) => {
                    do_check_false(result);
                    fxa.internal.fxAccountsClient._deletedOnServer = false;
-                   run_next_test();
+                   fxa.signOut().then(run_next_test);
                  }
                );
             }
           )
         }
       );
     }
   );
@@ -662,48 +640,44 @@ add_test(function test_resend_email() {
         fxa.internal.abortExistingFlow();
         run_next_test();
       });
     });
   });
 });
 
 add_test(function test_sign_out() {
-  do_test_pending();
   let fxa = new MockFxAccounts();
   let remoteSignOutCalled = false;
   let client = fxa.internal.fxAccountsClient;
   client.signOut = function() { remoteSignOutCalled = true; return Promise.resolve(); };
   makeObserver(ONLOGOUT_NOTIFICATION, function() {
     log.debug("test_sign_out_with_remote_error observed onlogout");
     // user should be undefined after sign out
     fxa.internal.getUserAccountData().then(user => {
       do_check_eq(user, null);
       do_check_true(remoteSignOutCalled);
-      do_test_finished();
       run_next_test();
     });
   });
   fxa.signOut();
 });
 
 add_test(function test_sign_out_with_remote_error() {
-  do_test_pending();
   let fxa = new MockFxAccounts();
   let client = fxa.internal.fxAccountsClient;
   let remoteSignOutCalled = false;
   // Force remote sign out to trigger an error
   client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
   makeObserver(ONLOGOUT_NOTIFICATION, function() {
     log.debug("test_sign_out_with_remote_error observed onlogout");
     // user should be undefined after sign out
     fxa.internal.getUserAccountData().then(user => {
       do_check_eq(user, null);
       do_check_true(remoteSignOutCalled);
-      do_test_finished();
       run_next_test();
     });
   });
   fxa.signOut();
 });
 
 add_test(function test_getOAuthToken() {
   let fxa = new MockFxAccounts();
@@ -843,67 +817,67 @@ add_task(function* test_getOAuthTokenCac
 
 Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
 add_test(function test_getOAuthToken_invalid_param() {
   let fxa = new MockFxAccounts();
 
   fxa.getOAuthToken()
     .then(null, err => {
        do_check_eq(err.message, "INVALID_PARAMETER");
-       run_next_test();
+       fxa.signOut().then(run_next_test);
     });
 });
 
 add_test(function test_getOAuthToken_invalid_scope_array() {
   let fxa = new MockFxAccounts();
 
   fxa.getOAuthToken({scope: []})
     .then(null, err => {
        do_check_eq(err.message, "INVALID_PARAMETER");
-       run_next_test();
+       fxa.signOut().then(run_next_test);
     });
 });
 
 add_test(function test_getOAuthToken_misconfigure_oauth_uri() {
   let fxa = new MockFxAccounts();
 
   Services.prefs.deleteBranch("identity.fxaccounts.remote.oauth.uri");
 
   fxa.getOAuthToken()
     .then(null, err => {
        do_check_eq(err.message, "INVALID_PARAMETER");
        // revert the pref
        Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", "https://example.com/v1");
-       run_next_test();
+       fxa.signOut().then(run_next_test);
     });
 });
 
 add_test(function test_getOAuthToken_no_account() {
   let fxa = new MockFxAccounts();
 
   fxa.internal.currentAccountState.getUserAccountData = function () {
     return Promise.resolve(null);
   };
 
   fxa.getOAuthToken({ scope: "profile" })
     .then(null, err => {
        do_check_eq(err.message, "NO_ACCOUNT");
-       run_next_test();
+       fxa.signOut().then(run_next_test);
     });
 });
 
 add_test(function test_getOAuthToken_unverified() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
   fxa.setSignedInUser(alice).then(() => {
     fxa.getOAuthToken({ scope: "profile" })
       .then(null, err => {
          do_check_eq(err.message, "UNVERIFIED_ACCOUNT");
-         run_next_test();
+         fxa.signOut().then(run_next_test);
       });
   });
 });
 
 add_test(function test_getOAuthToken_network_error() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
   alice.verified = true;
@@ -1066,49 +1040,49 @@ add_test(function test_getSignedInUserPr
     let accountState = fxa.internal.currentAccountState;
     accountState.getProfile = function () {
       return Promise.reject("boom");
     };
 
     fxa.getSignedInUserProfile()
       .catch(error => {
          do_check_eq(error.message, "UNKNOWN_ERROR");
-         run_next_test();
+         fxa.signOut().then(run_next_test);
       });
   });
 
 });
 
 add_test(function test_getSignedInUserProfile_unverified_account() {
   let fxa = new MockFxAccounts();
   let alice = getTestUser("alice");
 
   fxa.setSignedInUser(alice).then(() => {
     let accountState = fxa.internal.currentAccountState;
 
     fxa.getSignedInUserProfile()
       .catch(error => {
          do_check_eq(error.message, "UNVERIFIED_ACCOUNT");
-         run_next_test();
+         fxa.signOut().then(run_next_test);
       });
   });
 
 });
 
 add_test(function test_getSignedInUserProfile_no_account_data() {
   let fxa = new MockFxAccounts();
 
   fxa.internal.getSignedInUser = function () {
     return Promise.resolve(null);
   };
 
   fxa.getSignedInUserProfile()
     .catch(error => {
        do_check_eq(error.message, "NO_ACCOUNT");
-       run_next_test();
+       fxa.signOut().then(run_next_test);
     });
 
 });
 
 /*
  * End of tests.
  * Utility functions follow.
  */
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -65,22 +65,16 @@ TEST_HARNESS_FILES.testing.mochitest += 
     'pywebsocket_wrapper.py',
     'redirect.html',
     'runtests.py',
     'runtestsb2g.py',
     'runtestsremote.py',
     'server.js',
 ]
 
-if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
-    TEST_HARNESS_FILES.testing.mochitest += [
-        '/mobile/android/base/tests/robocop.ini',
-        '/mobile/android/base/tests/robocop_autophone.ini',
-    ]
-
 TEST_HARNESS_FILES.testing.mochitest.pywebsocket += [
     'pywebsocket/standalone.py',
 ]
 
 TEST_HARNESS_FILES.testing.mochitest.pywebsocket.mod_pywebsocket += [
     'pywebsocket/mod_pywebsocket/__init__.py',
     'pywebsocket/mod_pywebsocket/_stream_base.py',
     'pywebsocket/mod_pywebsocket/_stream_hixie75.py',
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -487,27 +487,25 @@ def main(args):
     else:
         dm = droid.DroidSUT(
             options.deviceIP,
             options.devicePort,
             deviceRoot=options.remoteTestRoot)
     auto.setDeviceManager(dm)
     options = parser.verifyRemoteOptions(options, auto)
 
+    if options is None:
+        raise ValueError("Invalid options specified, use --help for a list of valid options")
+
     mochitest = MochiRemote(auto, dm, options)
 
     log = mochitest.log
     message_logger.logger = log
     mochitest.message_logger = message_logger
 
-    if (options is None):
-        log.error(
-            "Invalid options specified, use --help for a list of valid options")
-        return 1
-
     productPieces = options.remoteProductName.split('.')
     if (productPieces is not None):
         auto.setProduct(productPieces[0])
     else:
         auto.setProduct(options.remoteProductName)
     auto.setAppName(options.remoteappname)
 
     options = parser.verifyOptions(options, mochitest)
--- a/testing/mozbase/mozlog/mozlog/structured/commandline.py
+++ b/testing/mozbase/mozlog/mozlog/structured/commandline.py
@@ -174,16 +174,18 @@ def setup_logging(suite, args, defaults=
 
     logger = StructuredLogger(suite)
     # Keep track of any options passed for formatters.
     formatter_options = defaultdict(lambda: formatter_option_defaults.copy())
     # Keep track of formatters and list of streams specified.
     formatters = defaultdict(list)
     found = False
     found_stdout_logger = False
+    if args is None:
+        args = {}
     if not hasattr(args, 'iteritems'):
         args = vars(args)
 
     if defaults is None:
         if sys.__stdout__.isatty():
             defaults = {"mach": sys.stdout}
         else:
             defaults = {"raw": sys.stdout}
--- a/testing/mozharness/mozharness.json
+++ b/testing/mozharness/mozharness.json
@@ -1,4 +1,4 @@
 {
     "repo": "https://hg.mozilla.org/build/mozharness",
-    "revision": "fa7ea67fc5a2"
+    "revision": "ca580fcfa627"
 }
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -304,18 +304,19 @@ user_pref("media.decoder.heuristic.dorma
 // Don't prompt about e10s
 user_pref("browser.displayedE10SPrompt.1", 5);
 // Don't use auto-enabled e10s
 user_pref("browser.tabs.remote.autostart.1", false);
 user_pref("browser.tabs.remote.autostart.2", false);
 // Don't forceably kill content processes after a timeout
 user_pref("dom.ipc.tabs.shutdownTimeoutSecs", 0);
 
-// Avoid performing Readinglist Intro during tests.
+// Avoid performing Reading List and Reader Mode intros during tests.
 user_pref("browser.readinglist.introShown", true);
+user_pref("browser.reader.detectedFirstArticle", true);
 
 // Don't let PAC generator to set PAC, as mochitest framework has its own PAC
 // rules during testing.
 user_pref("network.proxy.pac_generator", false);
 
 // Make tests run consistently on DevEdition (which has a lightweight theme
 // selected by default).
 user_pref("lightweightThemes.selectedThemeID", "");
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -62,17 +62,16 @@ this.PlacesDBUtils = {
       }
 
       if (aTasks.callback) {
         let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback);
         aTasks.callback.call(scope, aTasks.messages);
       }
 
       // Notify observers that maintenance finished.
-      Services.prefs.setIntPref("places.database.lastMaintenance", parseInt(Date.now() / 1000));
       Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC, null);
     }
   },
 
   _isShuttingDown : false,
   shutdown: function PDBU_shutdown() {
     PlacesDBUtils._isShuttingDown = true;
   },
@@ -89,17 +88,22 @@ this.PlacesDBUtils = {
   maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope)
   {
     let tasks = new Tasks([
       this.checkIntegrity
     , this.checkCoherence
     , this._refreshUI
     ]);
     tasks._telemetryStart = Date.now();
-    tasks.callback = aCallback;
+    tasks.callback = function() {
+      Services.prefs.setIntPref("places.database.lastMaintenance",
+                                parseInt(Date.now() / 1000));
+      if (aCallback)
+        aCallback();
+    }
     tasks.scope = aScope;
     this._executeTasks(tasks);
   },
 
   /**
    * Executes integrity check, common and advanced maintenance tasks (like
    * expiration and vacuum).  Will also collect statistics on the database.
    *
--- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
@@ -1274,24 +1274,27 @@ add_task(function test_preventive_mainte
 
   // Get current bookmarks max ID for cleanup
   let stmt = mDBConn.createStatement("SELECT MAX(id) FROM moz_bookmarks");
   stmt.executeStep();
   defaultBookmarksMaxId = stmt.getInt32(0);
   stmt.finalize();
   do_check_true(defaultBookmarksMaxId > 0);
 
-  for ([, test] in Iterator(tests)) {
+  for (let [, test] in Iterator(tests)) {
     dump("\nExecuting test: " + test.name + "\n" + "*** " + test.desc + "\n");
     yield test.setup();
 
     let promiseMaintenanceFinished =
         promiseTopicObserved(FINISHED_MAINTENANCE_NOTIFICATION_TOPIC);
-    PlacesDBUtils.maintenanceOnIdle();
+    Services.prefs.clearUserPref("places.database.lastMaintenance");
+    let callbackInvoked = false;
+    PlacesDBUtils.maintenanceOnIdle(() => callbackInvoked = true);
     yield promiseMaintenanceFinished;
+    do_check_true(callbackInvoked);
 
     // Check the lastMaintenance time has been saved.
     do_check_neq(Services.prefs.getIntPref("places.database.lastMaintenance"), null);
 
     yield test.check();
 
     cleanDatabase();
   }
--- a/toolkit/components/places/tests/unit/test_telemetry.js
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -121,19 +121,21 @@ add_task(function test_execute()
     let validate = histograms[histogramId];
     let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
     validate(snapshot.sum);
     do_check_true(snapshot.counts.reduce(function(a, b) a + b) > 0);
   }
 });
 
 add_test(function test_healthreport_callback() {
+  Services.prefs.clearUserPref("places.database.lastMaintenance");
   PlacesDBUtils.telemetry(null, function onResult(data) {
     do_check_neq(data, null);
 
     do_check_eq(Object.keys(data).length, 2);
     do_check_eq(data.PLACES_PAGES_COUNT, 1);
     do_check_eq(data.PLACES_BOOKMARKS_COUNT, 1);
 
+    do_check_true(!Services.prefs.prefHasUserValue("places.database.lastMaintenance"));
     run_next_test();
   });
 });
 
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3040,25 +3040,27 @@
   },
   "PLACES_PAGES_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 1000,
     "high": "150000",
     "n_buckets": 20,
     "extended_statistics_ok": true,
+    "releaseChannelCollection": "opt-out",
     "description": "PLACES: Number of unique pages"
   },
   "PLACES_BOOKMARKS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 100,
     "high": "8000",
     "n_buckets": 15,
     "extended_statistics_ok": true,
+    "releaseChannelCollection": "opt-out",
     "description": "PLACES: Number of bookmarks"
   },
   "PLACES_TAGS_COUNT": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "200",
     "n_buckets": 10,
     "extended_statistics_ok": true,
@@ -4858,22 +4860,24 @@
     "n_values": 10,
     "keyed": true,
     "description": "Longest blocking operation performed by the add-on (log2(duration in ms), keyed by add-on, updated every 15s by default)"
   },
   "SEARCH_COUNTS": {
     "expires_in_version": "never",
     "kind": "count",
     "keyed": true,
+    "releaseChannelCollection": "opt-out",
     "description": "Record the search counts for search engines"
   },
   "SEARCH_DEFAULT_ENGINE": {
     "expires_in_version": "never",
     "kind": "flag",
     "keyed": true,
+    "releaseChannelCollection": "opt-out",
     "description": "Record the default search engine."
   },
   "SEARCH_SERVICE_INIT_MS": {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": "1000",
     "n_buckets": 15,
     "extended_statistics_ok": true,
@@ -7491,25 +7495,25 @@
     "kind": "boolean",
     "description": "Stores 1 if generating a call URL succeeded, and 0 if it failed."
   },
   "LOOP_CLIENT_CALL_URL_SHARED": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Stores 1 every time the URL is copied or shared."
   },
-  "LOOP_TWO_WAY_MEDIA_CONN_LENGTH": {
+  "LOOP_TWO_WAY_MEDIA_CONN_LENGTH_1": {
     "alert_emails": ["firefox-dev@mozilla.org", "dmose@mozilla.com"],
     "expires_in_version": "43",
     "kind": "enumerated",
     "n_values": 8,
     "releaseChannelCollection": "opt-out",
     "description": "Connection length for bi-directionally connected media (0=SHORTER_THAN_10S, 1=BETWEEN_10S_AND_30S, 2=BETWEEN_30S_AND_5M, 3=MORE_THAN_5M)"
   },
-  "LOOP_SHARING_STATE_CHANGE": {
+  "LOOP_SHARING_STATE_CHANGE_1": {
     "alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
     "expires_in_version": "43",
     "kind": "enumerated",
     "n_values": 8,
     "releaseChannelCollection": "opt-out",
     "description": "Number of times the sharing feature has been enabled and disabled (0=WINDOW_ENABLED, 1=WINDOW_DISABLED, 2=BROWSER_ENABLED, 3=BROWSER_DISABLED)"
   },
   "E10S_AUTOSTART": {
--- a/toolkit/components/telemetry/Telemetry.cpp
+++ b/toolkit/components/telemetry/Telemetry.cpp
@@ -1101,23 +1101,26 @@ GetSubsessionHistogram(Histogram& existi
 nsresult
 HistogramAdd(Histogram& histogram, int32_t value, uint32_t dataset)
 {
   // Check if we are allowed to record the data.
   if (!CanRecordDataset(dataset)) {
     return NS_OK;
   }
 
-  histogram.Add(value);
 #if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
   if (Histogram* subsession = GetSubsessionHistogram(histogram)) {
     subsession->Add(value);
   }
 #endif
 
+  // It is safe to add to the histogram now: the subsession histogram was already
+  // cloned from this so we won't add the sample twice.
+  histogram.Add(value);
+
   return NS_OK;
 }
 
 nsresult
 HistogramAdd(Histogram& histogram, int32_t value)
 {
   uint32_t dataset = nsITelemetry::DATASET_RELEASE_CHANNEL_OPTIN;
   // We only really care about the dataset of the histogram if we are not recording
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -16,42 +16,54 @@ Cu.import("resource://gre/modules/debug.
 Cu.import("resource://gre/modules/Services.jsm", this);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/DeferredTask.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetryController::";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_BRANCH_LOG = PREF_BRANCH + "log.";
 const PREF_SERVER = PREF_BRANCH + "server";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
 const PREF_LOG_LEVEL = PREF_BRANCH_LOG + "level";
 const PREF_LOG_DUMP = PREF_BRANCH_LOG + "dump";
 const PREF_CACHED_CLIENTID = PREF_BRANCH + "cachedClientID";
 const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_SESSIONS_BRANCH = "datareporting.sessions.";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
 
 const PING_FORMAT_VERSION = 4;
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 // The number of days to keep pings serialised on the disk in case of failures.
 const DEFAULT_RETENTION_DAYS = 14;
 // Timeout after which we consider a ping submission failed.
 const PING_SUBMIT_TIMEOUT_MS = 2 * 60 * 1000;
 
+// We treat pings before midnight as happening "at midnight" with this tolerance.
+const MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
+// We try to spread "midnight" pings out over this interval.
+const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * 60 * 1000;
+const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
+
 XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
                                   "resource://gre/modules/ClientID.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
                                    "@mozilla.org/base/telemetry;1",
                                    "nsITelemetry");
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStorage",
@@ -110,20 +122,39 @@ function generateUUID() {
  * Determine if the ping has new ping format or a legacy one.
  */
 function isNewPingFormat(aPing) {
   return ("id" in aPing) && ("application" in aPing) &&
          ("version" in aPing) && (aPing.version >= 2);
 }
 
 /**
+ * Takes a date and returns it trunctated to a date with daily precision.
+ */
+function truncateToDays(date) {
+  return new Date(date.getFullYear(),
+                  date.getMonth(),
+                  date.getDate(),
+                  0, 0, 0, 0);
+}
+
+function tomorrow(date) {
+  let d = new Date(date);
+  d.setDate(d.getDate() + 1);
+  return d;
+}
+
+/**
  * This is a policy object used to override behavior for testing.
  */
 let Policy = {
   now: () => new Date(),
+  midnightPingFuzzingDelay: () => MIDNIGHT_FUZZING_DELAY_MS,
+  setPingSendTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+  clearPingSendTimeout: (id) => clearTimeout(id),
 }
 
 this.EXPORTED_SYMBOLS = ["TelemetryController"];
 
 this.TelemetryController = Object.freeze({
   Constants: Object.freeze({
     PREF_ENABLED: PREF_ENABLED,
     PREF_LOG_LEVEL: PREF_LOG_LEVEL,
@@ -335,16 +366,19 @@ let Impl = {
   // The previous build ID, if this is the first run with a new build.
   // Undefined if this is not the first run, or the previous build ID is unknown.
   _previousBuildID: undefined,
   _clientID: null,
   // A task performing delayed initialization
   _delayedInitTask: null,
   // The deferred promise resolved when the initialization task completes.
   _delayedInitTaskDeferred: null,
+  // Timer for scheduled ping sends.
+  _pingSendTimer: null,
+
   // The session recorder, shared with FHR and the Data Reporting Service.
   _sessionRecorder: null,
   // This is a public barrier Telemetry clients can use to add blockers to the shutdown
   // of TelemetryController.
   // After this barrier, clients can not submit Telemetry pings anymore.
   _shutdownBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for clients."),
   // This is a private barrier blocked by pending async ping activity (sending & saving).
   _connectionsBarrier: new AsyncShutdown.Barrier("TelemetryController: Waiting for pending ping activity"),
@@ -479,16 +513,41 @@ let Impl = {
     return TelemetryStorage.addPendingPingFromFile(aPingPath).then(() => {
         if (aRemoveOriginal) {
           return OS.File.remove(aPingPath);
         }
       }, error => this._log.error("addPendingPingFromFile - Unable to add the pending ping", error));
   },
 
   /**
+   * This helper calculates the next time that we can send pings at.
+   * Currently this mostly redistributes ping sends around midnight to avoid submission
+   * spikes around local midnight for daily pings.
+   *
+   * @param now Date The current time.
+   * @return Number The next time (ms from UNIX epoch) when we can send pings.
+   */
+  _getNextPingSendTime: function(now) {
+    const todayDate = truncateToDays(now);
+    const tomorrowDate = tomorrow(todayDate);
+    const nextMidnightRangeStart = tomorrowDate.getTime() - MIDNIGHT_TOLERANCE_MS;
+    const currentMidnightRangeEnd = todayDate.getTime() - MIDNIGHT_TOLERANCE_MS + Policy.midnightPingFuzzingDelay();
+
+    if (now.getTime() < currentMidnightRangeEnd) {
+      return currentMidnightRangeEnd;
+    }
+
+    if (now.getTime() >= nextMidnightRangeStart) {
+      return nextMidnightRangeStart + Policy.midnightPingFuzzingDelay();
+    }
+
+    return now.getTime();
+  },
+
+  /**
    * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
    * environment data, client id and some general info.
    * Depending on configuration, the ping will be sent to the server (immediately or later)
    * and archived locally.
    *
    * @param {String} aType The type of the ping.
    * @param {Object} aPayload The actual data payload for the ping.
    * @param {Object} [aOptions] Options object.
@@ -504,22 +563,33 @@ let Impl = {
                     ", aOptions: " + JSON.stringify(aOptions));
 
     const pingData = this.assemblePing(aType, aPayload, aOptions);
     this._log.trace("submitExternalPing - ping assembled, id: " + pingData.id);
 
     // Always persist the pings if we are allowed to.
     let archivePromise = TelemetryArchive.promiseArchivePing(pingData)
       .catch(e => this._log.error("submitExternalPing - Failed to archive ping " + pingData.id, e));
-
     let p = [ archivePromise ];
 
-    if (!this._initialized) {
-      // We are still initializing and should not send yet, add this to the pending pings.
-      this._log.trace("submitExternalPing - still initializing, ping is pending");
+    // Check if we can send pings now.
+    const now = Policy.now();
+    const nextPingSendTime = this._getNextPingSendTime(now);
+    const throttled = (nextPingSendTime > now.getTime());
+
+    // We can't send pings now, schedule a later send.
+    if (throttled) {
+      this._log.trace("submitExternalPing - throttled, delaying ping send to " + new Date(nextPingSendTime));
+      this._reschedulePingSendTimer(nextPingSendTime);
+    }
+
+    if (!this._initialized || throttled) {
+      // We can't send because we are still initializing or throttled, add this to the pending pings.
+      this._log.trace("submitExternalPing - ping is pending, initialized: " + this._initialized +
+                      ", throttled: " + throttled);
       p.push(TelemetryStorage.addPendingPing(pingData));
     } else {
       // Try to send the ping, persist it if sending it fails.
       this._log.trace("submitExternalPing - already initialized, ping will be sent");
       p.push(this.doPing(pingData, false)
                  .catch(() => TelemetryStorage.savePing(pingData, true)));
       p.push(this.sendPersistedPings());
     }
@@ -536,16 +606,26 @@ let Impl = {
    */
   sendPersistedPings: function sendPersistedPings() {
     this._log.trace("sendPersistedPings - Can send: " + this._canSend());
     if (!this._canSend()) {
       this._log.trace("sendPersistedPings - Telemetry is not allowed to send pings.");
       return Promise.resolve();
     }
 
+    // Check if we can send pings now - otherwise schedule a later send.
+    const now = Policy.now();
+    const nextPingSendTime = this._getNextPingSendTime(now);
+    if (nextPingSendTime > now.getTime()) {
+      this._log.trace("sendPersistedPings - delaying ping send to " + new Date(nextPingSendTime));
+      this._reschedulePingSendTimer(nextPingSendTime);
+      return Promise.resolve();
+    }
+
+    // We can send now.
     let pingsIterator = Iterator(this.popPayloads());
     let p = [for (data of pingsIterator) this.doPing(data, true).catch((e) => {
       this._log.error("sendPersistedPings - doPing rejected", e);
     })];
 
     let promise = Promise.all(p);
     this._trackPendingPingTask(promise);
     return promise;
@@ -811,35 +891,31 @@ let Impl = {
   },
 
   /**
    * Perform telemetry initialization for either chrome or content process.
    * @return {Boolean} True if Telemetry is allowed to record at least base (FHR) data,
    *                   false otherwise.
    */
   enableTelemetryRecording: function enableTelemetryRecording() {
+    const enabled = Preferences.get(PREF_ENABLED, false);
+
     // Enable base Telemetry recording, if needed.
-#if !defined(MOZ_WIDGET_ANDROID)
-    Telemetry.canRecordBase = Preferences.get(PREF_FHR_ENABLED, false);
-#else
-    // FHR recording is always "enabled" on Android (data upload is not).
-    Telemetry.canRecordBase = true;
-#endif
+    Telemetry.canRecordBase = enabled || IS_UNIFIED_TELEMETRY;
 
 #ifdef MOZILLA_OFFICIAL
     if (!Telemetry.isOfficialTelemetry && !this._testMode) {
       // We can't send data; no point in initializing observers etc.
       // Only do this for official builds so that e.g. developer builds
       // still enable Telemetry based on prefs.
       Telemetry.canRecordExtended = false;
       this._log.config("enableTelemetryRecording - Can't send data, disabling extended Telemetry recording.");
     }
 #endif
 
-    let enabled = Preferences.get(PREF_ENABLED, false);
     this._server = Preferences.get(PREF_SERVER, undefined);
     if (!enabled || !Telemetry.canRecordBase) {
       // Turn off extended telemetry recording if disabled by preferences or if base/telemetry
       // telemetry recording is off.
       Telemetry.canRecordExtended = false;
       this._log.config("enableTelemetryRecording - Disabling extended Telemetry recording.");
     }
 
@@ -869,26 +945,23 @@ let Impl = {
 
     if (this._initialized && !this._testMode) {
       this._log.error("setupTelemetry - already initialized");
       return Promise.resolve();
     }
 
     // Only initialize the session recorder if FHR is enabled.
     // TODO: move this after the |enableTelemetryRecording| block and drop the
-    // PREF_FHR_ENABLED check after bug 1137252 lands.
-    if (!this._sessionRecorder && Preferences.get(PREF_FHR_ENABLED, true)) {
+    // PREF_FHR_ENABLED check once we permanently switch over to unified Telemetry.
+    if (!this._sessionRecorder &&
+        (Preferences.get(PREF_FHR_ENABLED, true) || IS_UNIFIED_TELEMETRY)) {
       this._sessionRecorder = new SessionRecorder(PREF_SESSIONS_BRANCH);
       this._sessionRecorder.onStartup();
     }
 
-    // Initialize some probes that are kept in their own modules
-    this._thirdPartyCookies = new ThirdPartyCookieProbe();
-    this._thirdPartyCookies.init();
-
     if (!this.enableTelemetryRecording()) {
       this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
       return Promise.resolve();
     }
 
     // For very short session durations, we may never load the client
     // id from disk.
     // We try to cache it in prefs to avoid this, even though this may
@@ -956,17 +1029,21 @@ let Impl = {
       }
     }
     this._pendingPingRequests.clear();
 
     // Now do an orderly shutdown.
     try {
       // First wait for clients processing shutdown.
       yield this._shutdownBarrier.wait();
-      // Then wait for any outstanding async ping activity.
+
+      // Then clear scheduled ping sends...
+      this._clearPingSendTimer();
+
+      // ... and wait for any outstanding async ping activity.
       yield this._connectionsBarrier.wait();
     } finally {
       // Reset state.
       this._initialized = false;
       this._initStarted = false;
     }
   }),
 
@@ -1026,36 +1103,51 @@ let Impl = {
 
   get clientID() {
     return this._clientID;
   },
 
   /**
    * Check if pings can be sent to the server. If FHR is not allowed to upload,
    * pings are not sent to the server (Telemetry is a sub-feature of FHR).
+   * If unified telemetry is off, don't send pings if Telemetry is disabled.
    * @return {Boolean} True if pings can be send to the servers, false otherwise.
    */
   _canSend: function() {
     return (Telemetry.isOfficialTelemetry || this._testMode) &&
-           Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
+           Preferences.get(PREF_FHR_UPLOAD_ENABLED, false) &&
+           (IS_UNIFIED_TELEMETRY || Preferences.get(PREF_ENABLED));
   },
 
   /**
    * Get an object describing the current state of this module for AsyncShutdown diagnostics.
    */
   _getState: function() {
     return {
       initialized: this._initialized,
       initStarted: this._initStarted,
       haveDelayedInitTask: !!this._delayedInitTask,
       shutdownBarrier: this._shutdownBarrier.state,
       connectionsBarrier: this._connectionsBarrier.state,
     };
   },
 
+  _reschedulePingSendTimer: function(timestamp) {
+    this._clearPingSendTimer();
+    const interval = timestamp - Policy.now();
+    this._pingSendTimer = Policy.setPingSendTimeout(() => this.sendPersistedPings(), interval);
+  },
+
+  _clearPingSendTimer: function() {
+    if (this._pingSendTimer) {
+      Policy.clearPingSendTimeout(this._pingSendTimer);
+      this._pingSendTimer = null;
+    }
+  },
+
   /**
    * Allows waiting for TelemetryControllers delayed initialization to complete.
    * This will complete before TelemetryController is shutting down.
    * @return {Promise} Resolved when delayed TelemetryController initialization completed.
    */
   promiseInitialized: function() {
     return this._delayedInitTaskDeferred.promise;
   },
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -55,25 +55,31 @@ const MIN_SUBSESSION_LENGTH_MS = 10 * 60
 
 const LOGGER_NAME = "Toolkit.Telemetry";
 const LOGGER_PREFIX = "TelemetrySession::";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_PREVIOUS_BUILDID = PREF_BRANCH + "previousBuildID";
 const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
 const PREF_ASYNC_PLUGIN_INIT = "dom.ipc.plugins.asyncInit";
+const PREF_UNIFIED = PREF_BRANCH + "unified";
+
 
 const MESSAGE_TELEMETRY_PAYLOAD = "Telemetry:Payload";
 const MESSAGE_TELEMETRY_GET_CHILD_PAYLOAD = "Telemetry:GetChildPayload";
 
 const DATAREPORTING_DIRECTORY = "datareporting";
 const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
 
 const SESSION_STATE_FILE_NAME = "session-state.json";
 
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
+
 // Maximum number of content payloads that we are willing to store.
 const MAX_NUM_CONTENT_PAYLOADS = 10;
 
 // Do not gather data more than once a minute
 const TELEMETRY_INTERVAL = 60000;
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
@@ -1334,22 +1340,18 @@ let Impl = {
 
   /**
    * Get the current session's payload using the provided
    * simpleMeasurements and info, which are typically obtained by a call
    * to |this.getSimpleMeasurements| and |this.getMetadata|,
    * respectively.
    */
   assemblePayloadWithMeasurements: function(simpleMeasurements, info, reason, clearSubsession) {
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
-    const isSubsession = !this._isClassicReason(reason);
-#else
-    const isSubsession = false;
-    clearSubsession = false;
-#endif
+    const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason);
+    clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession;
     this._log.trace("assemblePayloadWithMeasurements - reason: " + reason +
                     ", submitting subsession data: " + isSubsession);
 
     // Payload common to chrome and content processes.
     let payloadObj = {
       ver: PAYLOAD_VERSION,
       simpleMeasurements: simpleMeasurements,
       histograms: this.getHistograms(isSubsession, clearSubsession),
@@ -1476,16 +1478,21 @@ let Impl = {
       return this._delayedInitTaskDeferred.promise;
     }
 
     if (this._initialized && !testing) {
       this._log.error("setupChromeProcess - already initialized");
       return Promise.resolve();
     }
 
+    if (!Telemetry.canRecordBase && !testing) {
+      this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
+      return Promise.resolve();
+    }
+
     // Generate a unique id once per session so the server can cope with duplicate
     // submissions, orphaning and other oddities. The id is shared across subsessions.
     this._sessionId = Policy.generateSessionUUID();
     this.startNewSubsession();
     // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
     // the very same value for |_sessionStartDate|.
     this._sessionStartDate = this._subsessionStartDate;
 
@@ -1498,21 +1505,16 @@ let Impl = {
     let previousBuildId = Preferences.get(PREF_PREVIOUS_BUILDID, null);
     let thisBuildID = Services.appinfo.appBuildID;
     // If there is no previousBuildId preference, we send null to the server.
     if (previousBuildId != thisBuildID) {
       this._previousBuildId = previousBuildId;
       Preferences.set(PREF_PREVIOUS_BUILDID, thisBuildID);
     }
 
-    if (!Telemetry.canRecordBase && !testing) {
-      this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
-      return Promise.resolve();
-    }
-
     TelemetryController.shutdown.addBlocker("TelemetrySession: shutting down",
                                       () => this.shutdownChromeProcess(),
                                       () => this._getState());
 
     Services.obs.addObserver(this, "sessionstore-windows-restored", false);
 #ifdef MOZ_WIDGET_ANDROID
     Services.obs.addObserver(this, "application-background", false);
 #endif
@@ -1536,33 +1538,36 @@ let Impl = {
           yield this._saveSessionData(this._getSessionDataObject()).catch(() =>
             this._log.error("setupChromeProcess - Could not write session data to disk."));
         }
         this.attachObservers();
         this.gatherMemory();
 
         Telemetry.asyncFetchTelemetryData(function () {});
 
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
-        // Check for a previously written aborted session ping.
-        yield this._checkAbortedSessionPing();
+        if (IS_UNIFIED_TELEMETRY) {
+          // Check for a previously written aborted session ping.
+          yield this._checkAbortedSessionPing();
 
-        TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
-                                                    (reason, data) => this._onEnvironmentChange(reason, data));
-        // Write the first aborted-session ping as early as possible. Just do that
-        // if we are not testing, since calling Telemetry.reset() will make a previous
-        // aborted ping a pending ping.
-        if (!testing) {
-          yield this._saveAbortedSessionPing();
+          // Write the first aborted-session ping as early as possible. Just do that
+          // if we are not testing, since calling Telemetry.reset() will make a previous
+          // aborted ping a pending ping.
+          if (!testing) {
+            yield this._saveAbortedSessionPing();
+          }
+
+          TelemetryEnvironment.registerChangeListener(ENVIRONMENT_CHANGE_LISTENER,
+                                 (reason, data) => this._onEnvironmentChange(reason, data));
+
+          // Start the scheduler.
+          // We skip this if unified telemetry is off, so we don't
+          // trigger the new unified ping types.
+          TelemetryScheduler.init();
         }
 
-        // Start the scheduler.
-        TelemetryScheduler.init();
-#endif
-
         this._delayedInitTaskDeferred.resolve();
       } catch (e) {
         this._delayedInitTaskDeferred.reject(e);
       } finally {
         this._delayedInitTask = null;
         this._delayedInitTaskDeferred = null;
       }
     }.bind(this), testing ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY);
@@ -1658,33 +1663,33 @@ let Impl = {
   },
 
   /**
    * Save both the "saved-session" and the "shutdown" pings to disk.
    */
   savePendingPings: function savePendingPings() {
     this._log.trace("savePendingPings");
 
-#ifndef MOZ_WIDGET_ANDROID
+    if (!IS_UNIFIED_TELEMETRY) {
+      return this.savePendingPingsClassic();
+    }
+
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
       addEnvironment: true,
       overwrite: true,
     };
 
     let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false);
     // Make sure we try to save the pending pings, even though we failed saving the shutdown
     // ping.
     return TelemetryController.addPendingPing(getPingType(shutdownPayload), shutdownPayload, options)
                         .then(() => this.savePendingPingsClassic(),
                               () => this.savePendingPingsClassic());
-#else
-    return this.savePendingPingsClassic();
-#endif
   },
 
   /**
    * Save the "saved-session" ping and make TelemetryController save all the pending pings to disk.
    */
   savePendingPingsClassic: function savePendingPingsClassic() {
     this._log.trace("savePendingPingsClassic");
     let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
@@ -1864,35 +1869,39 @@ let Impl = {
    * This tells TelemetrySession to uninitialize and save any pending pings.
    * @param testing Optional. If true, always saves the ping whether Telemetry
    *                can send pings or not, which is used for testing.
    */
   shutdownChromeProcess: function(testing = false) {
     this._log.trace("shutdownChromeProcess - testing: " + testing);
 
     let cleanup = () => {
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
-      TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
-      TelemetryScheduler.shutdown();
-#endif
+      if (IS_UNIFIED_TELEMETRY) {
+        TelemetryEnvironment.unregisterChangeListener(ENVIRONMENT_CHANGE_LISTENER);
+        TelemetryScheduler.shutdown();
+      }
       this.uninstall();
 
       let reset = () => {
         this._initStarted = false;
         this._initialized = false;
       };
 
       if (Telemetry.isOfficialTelemetry || testing) {
-        return this.savePendingPings()
-                .then(() => this._stateSaveSerializer.flushTasks())
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
-                .then(() => this._abortedSessionSerializer
-                                .enqueueTask(() => this._removeAbortedSessionPing()))
-#endif
-                .then(reset);
+        return Task.spawn(function*() {
+          yield this.savePendingPings();
+          yield this._stateSaveSerializer.flushTasks();
+
+          if (IS_UNIFIED_TELEMETRY) {
+            yield this._abortedSessionSerializer
+                      .enqueueTask(() => this._removeAbortedSessionPing());
+          }
+
+          reset();
+        }.bind(this));
       }
 
       reset();
       return Promise.resolve();
     };
 
     // We can be in one the following states here:
     // 1) setupChromeProcess was never called
@@ -1928,23 +1937,21 @@ let Impl = {
 
     let options = {
       retentionDays: RETENTION_DAYS,
       addClientId: true,
       addEnvironment: true,
     };
 
     let promise = TelemetryController.submitExternalPing(getPingType(payload), payload, options);
-#if !defined(MOZ_WIDGET_GONK) && !defined(MOZ_WIDGET_ANDROID)
     // If required, also save the payload as an aborted session.
-    if (saveAsAborted) {
+    if (saveAsAborted && IS_UNIFIED_TELEMETRY) {
       let abortedPromise = this._saveAbortedSessionPing(payload);
       promise = promise.then(() => abortedPromise);
     }
-#endif
     return promise;
   },
 
   /**
    * Loads session data from the session data file.
    * @return {Promise<boolean>} A promise which is resolved with a true argument when
    *                            loading has completed, with false otherwise.
    */
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -438,18 +438,17 @@ let TelemetryStorageImpl = {
    */
   addPendingPingFromFile: function(pingPath) {
     // Pings in the saved ping directory need to have the ping id or slug (old format) as
     // the file name. We load the ping content, check that it is valid, and use it to save
     // the ping file with the correct file name.
     return this.loadPingFile(pingPath).then(ping => {
       // Since we read a ping successfully, update the related histogram.
       Telemetry.getHistogramById("READ_SAVED_PING_SUCCESS").add(1);
-      this.addPendingPing(ping);
-      return this.savePing(ping, false);
+      return this.addPendingPing(ping);
     });
   },
 
   /**
    * Add a ping to the saved pings directory so that it gets saved
    * and sent along with other pings.
    * Note: that the original ping file will not be modified.
    *
--- a/toolkit/components/telemetry/docs/index.rst
+++ b/toolkit/components/telemetry/docs/index.rst
@@ -14,8 +14,9 @@ Client-side, this consists of:
 
 .. toctree::
    :maxdepth: 2
 
    pings
    common-ping
    environment
    main-ping
+   preferences
--- a/toolkit/components/telemetry/docs/pings.rst
+++ b/toolkit/components/telemetry/docs/pings.rst
@@ -40,18 +40,8 @@ Archiving
 When archiving is enabled through the relative preference, pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in `<profile-dir>/datareporting/archived`.
 
 To allow for cheaper lookup of archived pings, storage follows a specific naming scheme for both the directory and the ping file name: `<YYYY-MM>/<timestamp>.<UUID>.<type>.json`.
 
 * ``<YYYY-MM>`` - The subdirectory name, generated from the ping creation date.
 * ``<timestamp>`` - Timestamp of the ping creation date.
 * ``<UUID>`` - The ping identifier.
 * ``<type>`` - The ping type.
-
-Preferences
-===========
-
-Telemetry behaviour is controlled through the following preferences:
-
-* ``datareporting.healthreport.service.enabled`` - If true, records base Telemetry data. Otherwise, completely disables telemetry recording.
-* ``toolkit.telemetry.enabled`` - If true, record the extended Telemetry data. Please note that base Telemetry data needs to be enabled as well and we need to be in an official build or in test mode. This preference is controlled through the `Preferences` dialog.
-* ``datareporting.healthreport.uploadEnabled`` - Send the data we record if user has consented to FHR. This preference is controlled through the `Preferences` dialog.
-* ``toolkit.telemetry.archive.enabled`` - Allow pings to be archived locally.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/preferences.rst
@@ -0,0 +1,41 @@
+Preferences
+===========
+
+Telemetry behaviour is controlled through the preferences listed here.
+
+*Note:* On official builds (which define ``MOZILLA_OFFICIAL``), Telemetry is only initialized when ``MOZ_TELEMETRY_REPORTING`` is defined.
+Sending only happens on official builds with ``MOZ_TELEMETRY_REPORTING`` defined.
+
+``toolkit.telemetry.unified``
+
+  This controls whether unified behavior is enabled. If true:
+
+  * Telemetry is always enabled and recording *base* data.
+  * Telemetry will send additional ``main`` pings.
+
+``toolkit.telemetry.enabled``
+
+  If ``unified`` is off, this controls whether the Telemetry module is enabled.
+  If ``unified`` is on, this controls whether to record *extended* data.
+  This preference is controlled through the `Preferences` dialog.
+
+``datareporting.healthreport.uploadEnabled``
+
+  Send the data we record if user has consented to FHR. This preference is controlled through the `Preferences` dialog.
+
+``toolkit.telemetry.archive.enabled``
+
+  Allow pings to be archived locally. This can only be enabled if ``unified`` is on.
+
+``toolkit.telemetry.server``
+
+  The server Telemetry pings are sent to.
+
+``toolkit.telemetry.log.level``
+
+  This sets the Telemetry logging verbosity per ``Log.jsm``, with ``Trace`` or ``0`` being the most verbose and the default being ``Warn``.
+  By default logging goes only the console service.
+
+``toolkit.telemetry.log.dump``
+
+  Sets whether to dump Telemetry log messages to ``stdout`` too.
--- a/toolkit/components/telemetry/tests/unit/head.js
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -139,27 +139,41 @@ function fakeSchedulerTimer(set, clear) 
  * Fake the current date.
  * This passes all received arguments to a new Date constructor and
  * uses the resulting date to fake the time in Telemetry modules.
  *
  * @return Date The new faked date.
  */
 function fakeNow(...args) {
   const date = new Date(...args);
+  const modules = [
+    Cu.import("resource://gre/modules/TelemetrySession.jsm"),
+    Cu.import("resource://gre/modules/TelemetryEnvironment.jsm"),
+    Cu.import("resource://gre/modules/TelemetryController.jsm"),
+  ];
 
-  let ping = Cu.import("resource://gre/modules/TelemetryController.jsm");
-  ping.Policy.now = () => date;
-  let session = Cu.import("resource://gre/modules/TelemetrySession.jsm");
-  session.Policy.now = () => date;
-  let environment = Cu.import("resource://gre/modules/TelemetryEnvironment.jsm");
-  environment.Policy.now = () => date;
+  for (let m of modules) {
+    m.Policy.now = () => date;
+  }
 
   return new Date(date);
 }
 
+// Fake the timeout functions for TelemetryController sending.
+function fakePingSendTimer(set, clear) {
+  let ping = Cu.import("resource://gre/modules/TelemetryController.jsm");
+  ping.Policy.setPingSendTimeout = set;
+  ping.Policy.clearPingSendTimeout = clear;
+}
+
+function fakeMidnightPingFuzzingDelay(delayMs) {
+  let ping = Cu.import("resource://gre/modules/TelemetryController.jsm");
+  ping.Policy.midnightPingFuzzingDelay = () => delayMs;
+}
+
 // Return a date that is |offset| ms in the future from |date|.
 function futureDate(date, offset) {
   return new Date(date.getTime() + offset);
 }
 
 function truncateToDays(aMsec) {
   return Math.floor(aMsec / MILLISECONDS_PER_DAY);
 }
@@ -174,8 +188,11 @@ function promiseRejects(promise) {
 Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
 TelemetryController.initLogging();
 
 // Telemetry archiving should be on.
 Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
 
 // Avoid timers interrupting test behavior.
 fakeSchedulerTimer(() => {}, () => {});
+fakePingSendTimer(() => {}, () => {});
+// Make pind sending predictable.
+fakeMidnightPingFuzzingDelay(0);
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -145,27 +145,22 @@ add_task(function* asyncSetup() {
   // checking the client id before the async ping setup is finished.
   let promisePingSetup = TelemetryController.reset();
   do_check_eq(TelemetryController.clientID, gClientID);
   yield promisePingSetup;
 });
 
 // Ensure that not overwriting an existing file fails silently
 add_task(function* test_overwritePing() {
-  let ping = {id: "foo"}
+  let ping = {id: "foo"};
   yield TelemetryStorage.savePing(ping, true);
   yield TelemetryStorage.savePing(ping, false);
   yield TelemetryStorage.cleanupPingFile(ping);
 });
 
-// Sends a ping to a non existing server.
-add_task(function* test_noServerPing() {
-  yield sendPing(false, false);
-});
-
 // Checks that a sent ping is correctly received by a dummy http server.
 add_task(function* test_simplePing() {
   startWebserver();
 
   yield sendPing(false, false);
   let request = yield gRequestIterator.next();
 
   // Check that we have a version query parameter in the URL.
@@ -260,16 +255,80 @@ add_task(function* test_archivePings() {
   pingId = yield sendPing(true, true);
 
   // Check that we archive pings when successfully sending them.
   yield gRequestIterator.next();
   ping = yield TelemetryArchive.promiseArchivedPingById(pingId);
   Assert.equal(ping.id, pingId, "TelemetryController must archive pings if FHR is enabled.");
 });
 
+// Test that we fuzz the submission time around midnight properly
+// to avoid overloading the telemetry servers.
+add_task(function* test_midnightPingSendFuzzing() {
+  const fuzzingDelay = 60 * 60 * 1000;
+  fakeMidnightPingFuzzingDelay(fuzzingDelay);
+  let now = new Date(2030, 5, 1, 11, 00, 0);
+  fakeNow(now);
+
+  let pingSendTimerCallback = null;
+  let pingSendTimeout = null;
+  fakePingSendTimer((callback, timeout) => {
+    pingSendTimerCallback = callback;
+    pingSendTimeout = timeout;
+  }, () => {});
+
+  gRequestIterator = Iterator(new Request());
+  yield TelemetryController.reset();
+
+  // A ping submitted shortly before midnight should not get sent yet.
+  now = new Date(2030, 5, 1, 23, 55, 0);
+  fakeNow(now);
+  registerPingHandler((req, res) => {
+    Assert.ok(false, "No ping should be received yet.");
+  });
+  yield sendPing(true, true);
+
+  Assert.ok(!!pingSendTimerCallback);
+  Assert.deepEqual(futureDate(now, pingSendTimeout), new Date(2030, 5, 2, 0, 45, 0));
+
+  // A ping after midnight within the fuzzing delay should also not get sent.
+  now = new Date(2030, 5, 2, 0, 40, 0);
+  fakeNow(now);
+  pingSendTimeout = null;
+  yield sendPing(true, true);
+  Assert.deepEqual(futureDate(now, pingSendTimeout), new Date(2030, 5, 2, 0, 45, 0));
+
+  // The Request constructor restores the previous ping handler.
+  gRequestIterator = Iterator(new Request());
+
+  // Setting the clock to after the fuzzing delay, we should trigger the two ping sends
+  // with the timer callback.
+  now = futureDate(now, pingSendTimeout);
+  fakeNow(now);
+  yield pingSendTimerCallback();
+  let requests = [];
+  requests.push(yield gRequestIterator.next());
+  requests.push(yield gRequestIterator.next());
+  for (let req of requests) {
+    let ping = decodeRequestPayload(req);
+    checkPingFormat(ping, TEST_PING_TYPE, true, true);
+  }
+
+  // Moving the clock further we should still send pings immediately.
+  now = futureDate(now, 5 * 60 * 1000);
+  yield sendPing(true, true);
+  let request = yield gRequestIterator.next();
+  let ping = decodeRequestPayload(request);
+  checkPingFormat(ping, TEST_PING_TYPE, true, true);
+
+  // Clean-up.
+  fakeMidnightPingFuzzingDelay(0);
+  fakePingSendTimer(() => {}, () => {});
+});
+
 add_task(function* stopServer(){
   gHttpServer.stop(do_test_finished);
 });
 
 // An iterable sequence of http requests
 function Request() {
   let defers = [];
   let current = 0;
--- a/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
+++ b/toolkit/components/telemetry/tests/unit/test_nsITelemetry.js
@@ -702,16 +702,37 @@ function test_datasets()
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG"));
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
   registered = Telemetry.registeredKeyedHistograms(RELEASE_CHANNEL_OPTOUT, []);
   registered = new Set(registered);
   Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG"));
   Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
 }
 
+function test_instantiate() {
+  if (gIsAndroid) {
+    // We don't support subsessions yet on Android.
+    return;
+  }
+
+  const ID = "TELEMETRY_TEST_COUNT";
+  let h = Telemetry.getHistogramById(ID);
+
+  // Instantiate the subsession histogram through |add| and make sure they match.
+  // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
+  // |add| will not instantiate the histogram.
+  h.add(1);
+  let snapshot = h.snapshot();
+  let subsession = Telemetry.snapshotSubsessionHistograms();
+  Assert.equal(snapshot.sum, subsession[ID].sum,
+               "Histogram and subsession histogram sum must match.");
+  // Clear the histogram, so we don't void the assumptions from the other tests.
+  h.clear();
+}
+
 function test_subsession() {
   if (gIsAndroid) {
     // We don't support subsessions yet on Android.
     return;
   }
 
   const ID = "TELEMETRY_TEST_COUNT";
   const FLAG = "TELEMETRY_TEST_FLAG";
@@ -848,16 +869,19 @@ function test_keyed_subsession() {
 function generateUUID() {
   let str = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator).generateUUID().toString();
   // strip {}
   return str.substring(1, str.length - 1);
 }
 
 function run_test()
 {
+  // This MUST be the very first test of this file.
+  test_instantiate();
+
   let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR]
   for each (let histogram_type in kinds) {
     let [min, max, bucket_count] = [1, INT_MAX - 1, 10]
     test_histogram(histogram_type, "test::"+histogram_type, min, max, bucket_count);
 
     const nh = Telemetry.newHistogram;
     expect_fail(function () nh("test::min", "never", histogram_type, 0, max, bucket_count));
     expect_fail(function () nh("test::bucket_count", "never", histogram_type, min, max, 1));
--- a/toolkit/components/timermanager/nsUpdateTimerManager.js
+++ b/toolkit/components/timermanager/nsUpdateTimerManager.js
@@ -287,16 +287,20 @@ TimerManager.prototype = {
     }
   },
 
   /**
    * See nsIUpdateTimerManager.idl
    */
   registerTimer: function TM_registerTimer(id, callback, interval) {
     LOG("TimerManager:registerTimer - id: " + id);
+    if (id in this._timers && callback != this._timers[id].callback) {
+      LOG("TimerManager:registerTimer - Ignoring second registration for " + id);
+      return;
+    }
     let prefLastUpdate = PREF_APP_UPDATE_LASTUPDATETIME_FMT.replace(/%ID%/, id);
     // Initialize the last update time to 0 when the preference isn't set so
     // the timer will be notified soon after a new profile's first use.
     let lastUpdateTime = getPref("getIntPref", prefLastUpdate, 0);
     let now = Math.round(Date.now() / 1000);
     if (lastUpdateTime > now)
       lastUpdateTime = 0;
     if (lastUpdateTime == 0)
--- a/toolkit/devtools/event-emitter.js
+++ b/toolkit/devtools/event-emitter.js
@@ -20,16 +20,26 @@
 }).call(this, function (require, exports, module) {
 
 this.EventEmitter = function EventEmitter() {};
 module.exports = EventEmitter;
 
 const { Cu, components } = require("chrome");
 const Services = require("Services");
 const promise = require("promise");
+let loggingEnabled = true;
+
+if (!isWorker) {
+  loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
+  Services.prefs.addObserver("devtools.dump.emit", {
+    observe: () => {
+      loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
+    }
+  }, false);
+}
 
 /**
  * Decorate an object with event emitter functionality.
  *
  * @param Object aObjectToDecorate
  *        Bind all public methods of EventEmitter to
  *        the aObjectToDecorate object.
  */
@@ -142,66 +152,66 @@ EventEmitter.prototype = {
           Cu.reportError(msg);
           dump(msg + "\n");
         }
       }
     }
   },
 
   logEvent: function(aEvent, args) {
-    let logging = isWorker ? true : Services.prefs.getBoolPref("devtools.dump.emit");
+    if (!loggingEnabled) {
+      return;
+    }
 
-    if (logging) {
-      let caller, func, path;
-      if (!isWorker) {
-        caller = components.stack.caller.caller;
-        func = caller.name;
-        let file = caller.filename;
-        if (file.includes(" -> ")) {
-          file = caller.filename.split(/ -> /)[1];
-        }
-        path = file + ":" + caller.lineNumber;
+    let caller, func, path;
+    if (!isWorker) {
+      caller = components.stack.caller.caller;
+      func = caller.name;
+      let file = caller.filename;
+      if (file.includes(" -> ")) {
+        file = caller.filename.split(/ -> /)[1];
       }
+      path = file + ":" + caller.lineNumber;
+    }
 
-      let argOut = "(";
-      if (args.length === 1) {
-        argOut += aEvent;
-      }
+    let argOut = "(";
+    if (args.length === 1) {
+      argOut += aEvent;
+    }
 
-      let out = "EMITTING: ";
+    let out = "EMITTING: ";
 
-      // We need this try / catch to prevent any dead object errors.
-      try {
-        for (let i = 1; i < args.length; i++) {
-          if (i === 1) {
-            argOut = "(" + aEvent + ", ";
-          } else {
-            argOut += ", ";
-          }
+    // We need this try / catch to prevent any dead object errors.
+    try {
+      for (let i = 1; i < args.length; i++) {
+        if (i === 1) {
+          argOut = "(" + aEvent + ", ";
+        } else {
+          argOut += ", ";
+        }
 
-          let arg = args[i];
-          argOut += arg;
+        let arg = args[i];
+        argOut += arg;
 
-          if (arg && arg.nodeName) {
-            argOut += " (" + arg.nodeName;
-            if (arg.id) {
-              argOut += "#" + arg.id;
-            }
-            if (arg.className) {
-              argOut += "." + arg.className;
-            }
-            argOut += ")";
+        if (arg && arg.nodeName) {
+          argOut += " (" + arg.nodeName;
+          if (arg.id) {
+            argOut += "#" + arg.id;
           }
+          if (arg.className) {
+            argOut += "." + arg.className;
+          }
+          argOut += ")";
         }
-      } catch(e) {
-        // Object is dead so the toolbox is most likely shutting down,
-        // do nothing.
       }
+    } catch(e) {
+      // Object is dead so the toolbox is most likely shutting down,
+      // do nothing.
+    }
 
-      argOut += ")";
-      out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
+    argOut += ")";
+    out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
 
-      dump(out);
-    }
+    dump(out);
   },
 };
 
 });
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -2246,22 +2246,23 @@ function resolveURIToLocalPath(aURI) {
  *        Optional. For sourcemapped urls, the original url this is representing.
  * @param Debugger.Source generatedSource
  *        Optional, passed in when aSourceMap is also passed in. The generated
  *        source object that introduced this source.
  * @param String contentType
  *        Optional. The content type of this source, if immediately available.
  */
 function SourceActor({ source, thread, originalUrl, generatedSource,
-                       contentType }) {
+                       isInlineSource, contentType }) {
   this._threadActor = thread;
   this._originalUrl = originalUrl;
   this._source = source;
   this._generatedSource = generatedSource;
   this._contentType = contentType;
+  this._isInlineSource = isInlineSource;
 
   this.onSource = this.onSource.bind(this);
   this._invertSourceMap = this._invertSourceMap.bind(this);
   this._encodeAndSetSourceMapURL = this._encodeAndSetSourceMapURL.bind(this);
   this._getSourceText = this._getSourceText.bind(this);
 
   this._mapSourceToAddon();
 
@@ -2281,18 +2282,24 @@ SourceActor.prototype = {
   actorPrefix: "source",
 
   _oldSourceMap: null,
   _init: null,
   _addonID: null,
   _addonPath: null,
 
   get isSourceMapped() {
-    return this._originalURL || this._generatedSource ||
-           this.threadActor.sources.isPrettyPrinted(this.url);
+    return !this.isInlineSource && (
+      this._originalURL || this._generatedSource ||
+        this.threadActor.sources.isPrettyPrinted(this.url)
+    );
+  },
+
+  get isInlineSource() {
+    return this._isInlineSource;
   },
 
   get threadActor() { return this._threadActor; },
   get sources() { return this._threadActor.sources; },
   get dbg() { return this.threadActor.dbg; },
   get scripts() { return this.threadActor.scripts; },
   get source() { return this._source; },
   get generatedSource() { return this._generatedSource; },
@@ -2405,20 +2412,23 @@ SourceActor.prototype = {
       // non-JS sources to get the contentType from the headers.
       if (this.source &&
           this.source.text !== "[no source]" &&
           this._contentType &&
           this._contentType.indexOf('javascript') !== -1) {
         return toResolvedContent(this.source.text);
       }
       else {
-        // XXX bug 865252: Don't load from the cache if this is a source mapped
-        // source because we can't guarantee that the cache has the most up to date
-        // content for this source like we can if it isn't source mapped.
-        let sourceFetched = fetch(this.url, { loadFromCache: !this.source });
+        // Only load the HTML page source from cache (which exists when
+        // there are inline sources). Otherwise, we can't trust the
+        // cache because we are most likely here because we are
+        // fetching the original text for sourcemapped code, and the
+        // page hasn't requested it before (if it has, it was a
+        // previous debugging session).
+        let sourceFetched = fetch(this.url, { loadFromCache: this.isInlineSource });
 
         // Record the contentType we just learned during fetching
         return sourceFetched.then(result => {
           this._contentType = result.contentType;
           return result;
         });
       }
     });
--- a/toolkit/devtools/server/actors/utils/TabSources.js
+++ b/toolkit/devtools/server/actors/utils/TabSources.js
@@ -151,16 +151,17 @@ TabSources.prototype = {
       }
     }
 
     let actor = new SourceActor({
       thread: this._thread,
       source: source,
       originalUrl: originalUrl,
       generatedSource: generatedSource,
+      isInlineSource: isInlineSource,
       contentType: contentType
     });
 
     let sourceActorStore = this._thread.sourceActorStore;
     var id = sourceActorStore.getReusableActorId(source, originalUrl);
     if (id) {
       actor.actorID = id;
     }
--- a/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
+++ b/toolkit/locales/en-US/chrome/mozapps/extensions/extensions.properties
@@ -18,16 +18,20 @@ uninstallNotice=%S has been removed.
 # #1 is the number of reviews
 numReviews=#1 review;#1 reviews
 
 #LOCALIZATION NOTE (dateUpdated) %S is the date the addon was last updated
 dateUpdated=Updated %S
 
 #LOCALIZATION NOTE (notification.incompatible) %1$S is the add-on name, %2$S is brand name, %3$S is application version
 notification.incompatible=%1$S is incompatible with %2$S %3$S.
+#LOCALIZATION NOTE (notification.unsigned, notification.unsignedAndDisabled) %1$S is the add-on name, %2$S is brand name
+notification.unsignedAndDisabled=%1$S could not be verified for use in %2$S and has been disabled.
+notification.unsigned=%1$S could not be verified for use in %2$S. Proceed with caution.
+notification.unsigned.link=More Information
 #LOCALIZATION NOTE (notification.blocked) %1$S is the add-on name
 notification.blocked=%1$S has been disabled due to security or stability issues.
 notification.blocked.link=More Information
 #LOCALIZATION NOTE (notification.softblocked) %1$S is the add-on name
 notification.softblocked=%1$S is known to cause security or stability issues.
 notification.softblocked.link=More Information
 #LOCALIZATION NOTE (notification.outdated) %1$S is the add-on name
 notification.outdated=An important update is available for %1$S.
@@ -69,16 +73,20 @@ installVerifying=Verifying
 installInstalling=Installing
 installEnablePending=Restart to enable
 installDisablePending=Restart to disable
 installFailed=Error installing
 installCancelled=Install cancelled
 
 #LOCALIZATION NOTE (details.notification.incompatible) %1$S is the add-on name, %2$S is brand name, %3$S is application version
 details.notification.incompatible=%1$S is incompatible with %2$S %3$S.
+#LOCALIZATION NOTE (details.notification.unsigned, details.notification.unsignedAndDisabled) %1$S is the add-on name, %2$S is brand name
+details.notification.unsignedAndDisabled=%1$S could not be verified for use in %2$S and has been disabled.
+details.notification.unsigned=%1$S could not be verified for use in %2$S. Proceed with caution.
+details.notification.unsigned.link=More Information
 #LOCALIZATION NOTE (details.notification.blocked) %1$S is the add-on name
 details.notification.blocked=%1$S has been disabled due to security or stability issues.
 details.notification.blocked.link=More Information
 #LOCALIZATION NOTE (details.notification.softblocked) %1$S is the add-on name
 details.notification.softblocked=%1$S is known to cause security or stability issues.
 details.notification.softblocked.link=More Information
 #LOCALIZATION NOTE (details.notification.outdated) %1$S is the add-on name
 details.notification.outdated=An important update is available for %1$S.
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -2634,16 +2634,17 @@ var gListView = {
 
   hide: function gListView_hide() {
     gEventManager.unregisterInstallListener(this);
     doPendingUninstalls(this._listBox);
   },
 
   showEmptyNotice: function gListView_showEmptyNotice(aShow) {
     this._emptyNotice.hidden = !aShow;
+    this._listBox.hidden = aShow;
   },
 
   onSortChanged: function gListView_onSortChanged(aSortBy, aAscending) {
     sortList(this._listBox, aSortBy, aAscending);
   },
 
   onExternalInstall: function gListView_onExternalInstall(aAddon, aExistingAddon, aRequiresRestart) {
     // The existing list item will take care of upgrade installs
@@ -2780,31 +2781,32 @@ var gDetailView = {
     var version = document.getElementById("detail-version");
     if (shouldShowVersionNumber(aAddon)) {
       version.hidden = false;
       version.value = aAddon.version;
     } else {
       version.hidden = true;
     }
 
+    var screenshotbox = document.getElementById("detail-screenshot-box");
     var screenshot = document.getElementById("detail-screenshot");
     if (aAddon.screenshots && aAddon.screenshots.length > 0) {
       if (aAddon.screenshots[0].thumbnailURL) {
         screenshot.src = aAddon.screenshots[0].thumbnailURL;
         screenshot.width = aAddon.screenshots[0].thumbnailWidth;
         screenshot.height = aAddon.screenshots[0].thumbnailHeight;
       } else {
         screenshot.src = aAddon.screenshots[0].url;
         screenshot.width = aAddon.screenshots[0].width;
         screenshot.height = aAddon.screenshots[0].height;
       }
       screenshot.setAttribute("loading", "true");
-      screenshot.hidden = false;
+      screenshotbox.hidden = false;
     } else {
-      screenshot.hidden = true;
+      screenshotbox.hidden = true;
     }
 
     var desc = document.getElementById("detail-desc");
     desc.textContent = aAddon.description;
 
     var fullDesc = document.getElementById("detail-fulldesc");
     if (aAddon.fullDescription) {
       // The following is part of an awful hack to include the licenses for GMP
@@ -3068,16 +3070,27 @@ var gDetailView = {
         document.getElementById("detail-error").textContent = gStrings.ext.formatStringFromName(
           "details.notification.blocked",
           [this._addon.name], 1
         );
         var errorLink = document.getElementById("detail-error-link");
         errorLink.value = gStrings.ext.GetStringFromName("details.notification.blocked.link");
         errorLink.href = this._addon.blocklistURL;
         errorLink.hidden = false;
+      } else if (this._addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+        let msgType = this._addon.appDisabled ? "error" : "warning";
+        this.node.setAttribute("notification", msgType);
+        document.getElementById("detail-" + msgType).textContent = gStrings.ext.formatStringFromName(
+          "details.notification.unsigned" + (this._addon.appDisabled ? "AndDisabled" : ""),
+          [this._addon.name, gStrings.brandShortName], 2
+        );
+        var infoLink = document.getElementById("detail-" + msgType + "-link");
+        infoLink.value = gStrings.ext.GetStringFromName("details.notification.unsigned.link");
+        infoLink.href = Services.prefs.getCharPref("xpinstall.signatures.infoURL");
+        infoLink.hidden = false;
       } else if (!this._addon.isCompatible && (AddonManager.checkCompatibility ||
         (this._addon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) {
         this.node.setAttribute("notification", "warning");
         document.getElementById("detail-warning").textContent = gStrings.ext.formatStringFromName(
           "details.notification.incompatible",
           [this._addon.name, gStrings.brandShortName, gStrings.appVersion], 3
         );
         document.getElementById("detail-warning-link").hidden = true;
@@ -3332,16 +3345,17 @@ var gDetailView = {
   onPropertyChanged: function gDetailView_onPropertyChanged(aProperties) {
     if (aProperties.indexOf("applyBackgroundUpdates") != -1) {
       this._autoUpdate.value = this._addon.applyBackgroundUpdates;
       let hideFindUpdates = AddonManager.shouldAutoUpdate(this._addon);
       document.getElementById("detail-findUpdates-btn").hidden = hideFindUpdates;
     }
 
     if (aProperties.indexOf("appDisabled") != -1 ||
+        aProperties.indexOf("signedState") != -1 ||
         aProperties.indexOf("userDisabled") != -1)
       this.updateState();
   },
 
   onExternalInstall: function gDetailView_onExternalInstall(aAddon, aExistingAddon, aNeedsRestart) {
     // Only care about upgrades for the currently displayed add-on
     if (!aExistingAddon || aExistingAddon.id != this._addon.id)
       return;
@@ -3450,18 +3464,18 @@ var gUpdatesView = {
       if (!aIsRefresh && gViewController && aRequest &&
           aRequest != gViewController.currentViewRequest)
         return;
 
       if (aIsRefresh) {
         self.showEmptyNotice(false);
         self._updateSelected.hidden = true;
 
-        while (self._listBox.itemCount > 0)
-          self._listBox.removeItemAt(0);
+        while (self._listBox.childNodes.length > 0)
+          self._listBox.removeChild(self._listBox.firstChild);
       }
 
       var elements = [];
 
       for (let install of aInstallsList) {
         if (!self.isManualUpdate(install))
           continue;
 
@@ -3485,16 +3499,17 @@ var gUpdatesView = {
       self._categoryItem.badgeCount = self._listBox.itemCount;
 
       gViewController.notifyViewChanged();
     });
   },
 
   showEmptyNotice: function gUpdatesView_showEmptyNotice(aShow) {
     this._emptyNotice.hidden = !aShow;
+    this._listBox.hidden = aShow;
   },
 
   isManualUpdate: function gUpdatesView_isManualUpdate(aInstall, aOnlyAvailable) {
     var isManual = aInstall.existingAddon &&
                    !AddonManager.shouldAutoUpdate(aInstall.existingAddon);
     if (isManual && aOnlyAvailable)
       return isInState(aInstall, "available");
     return isManual;
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -782,17 +782,17 @@
                     oncommand="document.getBindingParent(this).restart();"/>
         <xul:button anonid="undo-btn" class="button-link"
                     label="&addon.undoAction.label;"
                     tooltipText="&addon.undoAction.tooltip;"
                     oncommand="document.getBindingParent(this).undo();"/>
         <xul:spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
       </xul:hbox>
 
-      <xul:hbox class="content-container">
+      <xul:hbox class="content-container" align="center">
         <xul:vbox class="icon-container">
           <xul:image anonid="icon" class="icon"/>
         </xul:vbox>
         <xul:vbox class="content-inner-container" flex="1">
           <xul:hbox class="basicinfo-container">
               <xul:hbox class="name-container">
                 <xul:label anonid="name" class="name" crop="end" flex="1"
                            xbl:inherits="value=name,tooltiptext=name"/>
@@ -837,87 +837,87 @@
                             tooltiptext="&cmd.showReleaseNotes.tooltip;"
                             showlabel="&cmd.showReleaseNotes.label;"
                             showtooltip="&cmd.showReleaseNotes.tooltip;"
                             hidelabel="&cmd.hideReleaseNotes.label;"
                             hidetooltip="&cmd.hideReleaseNotes.tooltip;"
                             oncommand="document.getBindingParent(this).toggleReleaseNotes();"/>
               </xul:hbox>
             </xul:vbox>
-            <xul:vbox class="status-control-wrapper">
-              <xul:hbox class="status-container">
-                <xul:hbox anonid="checking-update" hidden="true">
-                  <xul:image class="spinner"/>
-                  <xul:label value="&addon.checkingForUpdates.label;"/>
-                </xul:hbox>
-                <xul:vbox anonid="update-available" class="update-available"
-                          hidden="true">
-                  <xul:checkbox anonid="include-update" class="include-update"
-                                label="&addon.includeUpdate.label;" checked="true"
-                                oncommand="document.getBindingParent(this).onIncludeUpdateChanged();"/>
-                  <xul:hbox class="update-info-container">
-                    <xul:label class="update-available-notice"
-                               value="&addon.updateAvailable.label;"/>
-                    <xul:button anonid="update-btn" class="addon-control update"
-                                label="&addon.updateNow.label;"
-                                tooltiptext="&addon.updateNow.tooltip;"
-                                oncommand="document.getBindingParent(this).upgrade();"/>
-                  </xul:hbox>
-                </xul:vbox>
-                <xul:hbox anonid="install-status" class="install-status"
-                          hidden="true"/>
-              </xul:hbox>
-              <xul:hbox anonid="control-container" class="control-container">
-                <xul:button anonid="preferences-btn"
-                            class="addon-control preferences"
-#ifdef XP_WIN
-                            label="&cmd.showPreferencesWin.label;"
-                            tooltiptext="&cmd.showPreferencesWin.tooltip;"
-#else
-                            label="&cmd.showPreferencesUnix.label;"
-                            tooltiptext="&cmd.showPreferencesUnix.tooltip;"
-#endif
-                            oncommand="document.getBindingParent(this).showPreferences();"/>
-                            <!-- label="&cmd.debugAddon.label;" -->
-                <xul:button anonid="debug-btn" class="addon-control debug"
-                            label="&cmd.debugAddon.label;"
-                            oncommand="document.getBindingParent(this).debug();"/>
-
-                <xul:button anonid="enable-btn"  class="addon-control enable"
-                            label="&cmd.enableAddon.label;"
-                            oncommand="document.getBindingParent(this).userDisabled = false;"/>
-                <xul:button anonid="disable-btn" class="addon-control disable"
-                            label="&cmd.disableAddon.label;"
-                            oncommand="document.getBindingParent(this).userDisabled = true;"/>
-                <xul:button anonid="remove-btn" class="addon-control remove"
-                            label="&cmd.uninstallAddon.label;"
-                            oncommand="document.getBindingParent(this).uninstall();"/>
-                <xul:menulist anonid="state-menulist"
-                              class="addon-control state"
-                              tooltiptext="&cmd.stateMenu.tooltip;">
-                  <xul:menupopup>
-                    <xul:menuitem anonid="ask-to-activate-menuitem"
-                                  class="addon-control"
-                                  label="&cmd.askToActivate.label;"
-                                  tooltiptext="&cmd.askToActivate.tooltip;"
-                                  oncommand="document.getBindingParent(this).userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;"/>
-                    <xul:menuitem anonid="always-activate-menuitem"
-                                  class="addon-control"
-                                  label="&cmd.alwaysActivate.label;"
-                                  tooltiptext="&cmd.alwaysActivate.tooltip;"
-                                  oncommand="document.getBindingParent(this).userDisabled = false;"/>
-                    <xul:menuitem anonid="never-activate-menuitem"
-                                  class="addon-control"
-                                  label="&cmd.neverActivate.label;"
-                                  tooltiptext="&cmd.neverActivate.tooltip;"
-                                  oncommand="document.getBindingParent(this).userDisabled = true;"/>
-                  </xul:menupopup>
-                </xul:menulist>
+          </xul:hbox>
+        </xul:vbox>
+        <xul:vbox class="status-control-wrapper">
+          <xul:hbox class="status-container">
+            <xul:hbox anonid="checking-update" hidden="true">
+              <xul:image class="spinner"/>
+              <xul:label value="&addon.checkingForUpdates.label;"/>
+            </xul:hbox>
+            <xul:vbox anonid="update-available" class="update-available"
+                      hidden="true">
+              <xul:checkbox anonid="include-update" class="include-update"
+                            label="&addon.includeUpdate.label;" checked="true"
+                            oncommand="document.getBindingParent(this).onIncludeUpdateChanged();"/>
+              <xul:hbox class="update-info-container">
+                <xul:label class="update-available-notice"
+                           value="&addon.updateAvailable.label;"/>
+                <xul:button anonid="update-btn" class="addon-control update"
+                            label="&addon.updateNow.label;"
+                            tooltiptext="&addon.updateNow.tooltip;"
+                            oncommand="document.getBindingParent(this).upgrade();"/>
               </xul:hbox>
             </xul:vbox>
+            <xul:hbox anonid="install-status" class="install-status"
+                      hidden="true"/>
+          </xul:hbox>
+          <xul:hbox anonid="control-container" class="control-container">
+            <xul:button anonid="preferences-btn"
+                        class="addon-control preferences"
+#ifdef XP_WIN
+                        label="&cmd.showPreferencesWin.label;"
+                        tooltiptext="&cmd.showPreferencesWin.tooltip;"
+#else
+                        label="&cmd.showPreferencesUnix.label;"
+                        tooltiptext="&cmd.showPreferencesUnix.tooltip;"
+#endif
+                        oncommand="document.getBindingParent(this).showPreferences();"/>
+                        <!-- label="&cmd.debugAddon.label;" -->
+            <xul:button anonid="debug-btn" class="addon-control debug"
+                        label="&cmd.debugAddon.label;"
+                        oncommand="document.getBindingParent(this).debug();"/>
+
+            <xul:button anonid="enable-btn"  class="addon-control enable"
+                        label="&cmd.enableAddon.label;"
+                        oncommand="document.getBindingParent(this).userDisabled = false;"/>
+            <xul:button anonid="disable-btn" class="addon-control disable"
+                        label="&cmd.disableAddon.label;"
+                        oncommand="document.getBindingParent(this).userDisabled = true;"/>
+            <xul:button anonid="remove-btn" class="addon-control remove"
+                        label="&cmd.uninstallAddon.label;"
+                        oncommand="document.getBindingParent(this).uninstall();"/>
+            <xul:menulist anonid="state-menulist"
+                          class="addon-control state"
+                          tooltiptext="&cmd.stateMenu.tooltip;">
+              <xul:menupopup>
+                <xul:menuitem anonid="ask-to-activate-menuitem"
+                              class="addon-control"
+                              label="&cmd.askToActivate.label;"
+                              tooltiptext="&cmd.askToActivate.tooltip;"
+                              oncommand="document.getBindingParent(this).userDisabled = AddonManager.STATE_ASK_TO_ACTIVATE;"/>
+                <xul:menuitem anonid="always-activate-menuitem"
+                              class="addon-control"
+                              label="&cmd.alwaysActivate.label;"
+                              tooltiptext="&cmd.alwaysActivate.tooltip;"
+                              oncommand="document.getBindingParent(this).userDisabled = false;"/>
+                <xul:menuitem anonid="never-activate-menuitem"
+                              class="addon-control"
+                              label="&cmd.neverActivate.label;"
+                              tooltiptext="&cmd.neverActivate.tooltip;"
+                              oncommand="document.getBindingParent(this).userDisabled = true;"/>
+              </xul:menupopup>
+            </xul:menulist>
           </xul:hbox>
         </xul:vbox>
       </xul:hbox>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         this._installStatus.mControl = this;
@@ -1242,16 +1242,28 @@
               this.setAttribute("notification", "error");
               this._error.textContent = gStrings.ext.formatStringFromName(
                 "notification.blocked",
                 [this.mAddon.name], 1
               );
               this._errorLink.value = gStrings.ext.GetStringFromName("notification.blocked.link");
               this._errorLink.href = this.mAddon.blocklistURL;
               this._errorLink.hidden = false;
+            } else if (!isUpgrade &&
+                       this.mAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+              this.setAttribute("notification", this.mAddon.appDisabled ? "error" : "warning");
+              let msg = this.mAddon.appDisabled ? this._error : this._warning;
+              msg.textContent = gStrings.ext.formatStringFromName(
+                "notification.unsigned" + (this.mAddon.appDisabled ? "AndDisabled" : ""),
+                [this.mAddon.name, gStrings.brandShortName], 2
+              );
+              let infoLink = this.mAddon.appDisabled ? this._errorLink : this._warningLink;
+              infoLink.value = gStrings.ext.GetStringFromName("notification.unsigned.link");
+              infoLink.href = Services.prefs.getCharPref("xpinstall.signatures.infoURL");
+              infoLink.hidden = false;
             } else if ((!isUpgrade && !this.mAddon.isCompatible) && (AddonManager.checkCompatibility
             || (this.mAddon.blocklistState != Ci.nsIBlocklistService.STATE_SOFTBLOCKED))) {
               this.setAttribute("notification", "warning");
               this._warning.textContent = gStrings.ext.formatStringFromName(
                 "notification.incompatible",
                 [this.mAddon.name, gStrings.brandShortName, gStrings.appVersion], 3
               );
               this._warningLink.hidden = true;
@@ -1642,16 +1654,17 @@
           this._updateState();
         ]]></body>
       </method>
 
       <method name="onPropertyChanged">
         <parameter name="aProperties"/>
         <body><![CDATA[
           if (aProperties.indexOf("appDisabled") != -1 ||
+              aProperties.indexOf("signedState") != -1 ||
               aProperties.indexOf("userDisabled") != -1)
             this._updateState();
         ]]></body>
       </method>
 
       <method name="onNoUpdateAvailable">
         <body><![CDATA[
           this._showStatus("none");
--- a/toolkit/mozapps/extensions/content/extensions.xul
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -114,218 +114,357 @@
     <command id="cmd_neverActivateItem"/>
   </commandset>
 
   <keyset>
     <!-- XXXunf Disabled until bug 371900 is fixed. -->
     <key id="focusSearch" key="&search.commandkey;" modifiers="accel"
          disabled="true"/>
   </keyset>
+  <hbox flex="1">
+    <vbox>
+      <hbox id="nav-header"
+            align="center"
+            pack="center">
+        <toolbarbutton id="back-btn"
+                       class="nav-button header-button"
+                       command="cmd_back"
+                       tooltiptext="&cmd.back.tooltip;"
+                       hidden="true"
+                       disabled="true"/>
+        <toolbarbutton id="forward-btn"
+                       class="nav-button header-button"
+                       command="cmd_forward"
+                       tooltiptext="&cmd.forward.tooltip;"
+                       hidden="true"
+                       disabled="true"/>
+      </hbox>
+      <!-- category list -->
+      <richlistbox id="categories" flex="1">
+        <richlistitem id="category-search" value="addons://search/"
+                      class="category"
+                      name="&view.search.label;" priority="0"
+                      tooltiptext="&view.search.label;" disabled="true"/>
+        <richlistitem id="category-discover" value="addons://discover/"
+                      class="category"
+                      name="&view.discover.label;" priority="1000"
+                      tooltiptext="&view.discover.label;"/>
+        <richlistitem id="category-availableUpdates" value="addons://updates/available"
+                      class="category"
+                      name="&view.availableUpdates.label;" priority="100000"
+                      tooltiptext="&view.availableUpdates.label;"
+                      disabled="true"/>
+        <richlistitem id="category-recentUpdates" value="addons://updates/recent"
+                      class="category"
+                      name="&view.recentUpdates.label;" priority="101000"
+                      tooltiptext="&view.recentUpdates.label;" disabled="true"/>
+      </richlistbox>
+    </vbox>
+    <vbox flex="1">
+      <!-- main header -->
+      <hbox id="header" align="center">
+        <spacer flex="1"/>
+        <hbox id="updates-container" align="center">
+          <image class="spinner"/>
+          <label id="updates-noneFound" hidden="true"
+                 value="&updates.noneFound.label;"/>
+          <button id="updates-manualUpdatesFound-btn" class="button-link"
+                  hidden="true" label="&updates.manualUpdatesFound.label;"
+                  command="cmd_goToAvailableUpdates"/>
+          <label id="updates-progress" hidden="true"
+                 value="&updates.updating.label;"/>
+          <label id="updates-installed" hidden="true"
+                 value="&updates.installed.label;"/>
+          <label id="updates-downloaded" hidden="true"
+                 value="&updates.downloaded.label;"/>
+          <button id="updates-restart-btn" class="button-link" hidden="true"
+                  label="&updates.restart.label;"
+                  command="cmd_restartApp"/>
+        </hbox>
+        <toolbarbutton id="header-utils-btn" class="header-button" type="menu"
+                tooltiptext="&toolsMenu.tooltip;">
+          <menupopup id="utils-menu">
+            <menuitem id="utils-updateNow"
+                      label="&updates.checkForUpdates.label;"
+                      accesskey="&updates.checkForUpdates.accesskey;"
+                      command="cmd_findAllUpdates"/>
+            <menuitem id="utils-viewUpdates"
+                      label="&updates.viewUpdates.label;"
+                      accesskey="&updates.viewUpdates.accesskey;"
+                      command="cmd_goToRecentUpdates"/>
+            <menuseparator id="utils-installFromFile-separator"/>
+            <menuitem id="utils-installFromFile"
+                      label="&installAddonFromFile.label;"
+                      accesskey="&installAddonFromFile.accesskey;"
+                      command="cmd_installFromFile"/>
+            <menuseparator/>
+            <menuitem id="utils-autoUpdateDefault"
+                      label="&updates.updateAddonsAutomatically.label;"
+                      accesskey="&updates.updateAddonsAutomatically.accesskey;"
+                      type="checkbox" autocheck="false"
+                      command="cmd_toggleAutoUpdateDefault"/>
+            <menuitem id="utils-resetAddonUpdatesToAutomatic"
+                      label="&updates.resetUpdatesToAutomatic.label;"
+                      accesskey="&updates.resetUpdatesToAutomatic.accesskey;"
+                      command="cmd_resetAddonAutoUpdate"/>
+            <menuitem id="utils-resetAddonUpdatesToManual"
+                      label="&updates.resetUpdatesToManual.label;"
+                      accesskey="&updates.resetUpdatesToManual.accesskey;"
+                      command="cmd_resetAddonAutoUpdate"/>
+          </menupopup>
+        </toolbarbutton>
+        <textbox id="header-search" type="search" searchbutton="true"
+                 searchbuttonlabel="&search.buttonlabel;"
+                 placeholder="&search.placeholder;"/>
+      </hbox>
+      <box id="view-port-container" class="main-content" flex="1">
+        <!-- view port -->
+        <deck id="view-port" flex="1" selectedIndex="0">
 
-  <!-- main header -->
-  <hbox id="header" align="center">
-    <toolbarbutton id="back-btn" class="nav-button header-button" command="cmd_back"
-            tooltiptext="&cmd.back.tooltip;" hidden="true" disabled="true"/>
-    <toolbarbutton id="forward-btn" class="nav-button header-button" command="cmd_forward"
-            tooltiptext="&cmd.forward.tooltip;" hidden="true" disabled="true"/>
-    <spacer flex="1"/>
-    <hbox id="updates-container" align="center">
-      <image class="spinner"/>
-      <label id="updates-noneFound" hidden="true"
-             value="&updates.noneFound.label;"/>
-      <button id="updates-manualUpdatesFound-btn" class="button-link"
-              hidden="true" label="&updates.manualUpdatesFound.label;"
-              command="cmd_goToAvailableUpdates"/>
-      <label id="updates-progress" hidden="true"
-             value="&updates.updating.label;"/>
-      <label id="updates-installed" hidden="true"
-             value="&updates.installed.label;"/>
-      <label id="updates-downloaded" hidden="true"
-             value="&updates.downloaded.label;"/>
-      <button id="updates-restart-btn" class="button-link" hidden="true"
-              label="&updates.restart.label;"
-              command="cmd_restartApp"/>
-    </hbox>
-    <toolbarbutton id="header-utils-btn" class="header-button" type="menu"
-            tooltiptext="&toolsMenu.tooltip;">
-      <menupopup id="utils-menu">
-        <menuitem id="utils-updateNow"
-                  label="&updates.checkForUpdates.label;"
-                  accesskey="&updates.checkForUpdates.accesskey;"
-                  command="cmd_findAllUpdates"/>
-        <menuitem id="utils-viewUpdates"
-                  label="&updates.viewUpdates.label;"
-                  accesskey="&updates.viewUpdates.accesskey;"
-                  command="cmd_goToRecentUpdates"/>
-        <menuseparator id="utils-installFromFile-separator"/>
-        <menuitem id="utils-installFromFile"
-                  label="&installAddonFromFile.label;"
-                  accesskey="&installAddonFromFile.accesskey;"
-                  command="cmd_installFromFile"/>
-        <menuseparator/>
-        <menuitem id="utils-autoUpdateDefault"
-                  label="&updates.updateAddonsAutomatically.label;"
-                  accesskey="&updates.updateAddonsAutomatically.accesskey;"
-                  type="checkbox" autocheck="false"
-                  command="cmd_toggleAutoUpdateDefault"/>
-        <menuitem id="utils-resetAddonUpdatesToAutomatic"
-                  label="&updates.resetUpdatesToAutomatic.label;"
-                  accesskey="&updates.resetUpdatesToAutomatic.accesskey;"
-                  command="cmd_resetAddonAutoUpdate"/>
-        <menuitem id="utils-resetAddonUpdatesToManual"
-                  label="&updates.resetUpdatesToManual.label;"
-                  accesskey="&updates.resetUpdatesToManual.accesskey;"
-                  command="cmd_resetAddonAutoUpdate"/>
-      </menupopup>
-    </toolbarbutton>
-    <textbox id="header-search" type="search" searchbutton="true"
-             searchbuttonlabel="&search.buttonlabel;"
-             placeholder="&search.placeholder;"/>
-  </hbox>
+          <!-- discover view -->
+          <deck id="discover-view" flex="1" class="view-pane" selectedIndex="0" tabindex="0">
+            <vbox id="discover-loading" align="center" pack="stretch" flex="1" class="alert-container">
+              <spacer class="alert-spacer-before"/>
+              <hbox class="alert loading" align="center">
+                <image/>
+                <label value="&loading.label;"/>
+              </hbox>
+              <spacer class="alert-spacer-after"/>
+            </vbox>
+            <vbox id="discover-error" align="center" pack="stretch" flex="1" class="alert-container">
+              <spacer class="alert-spacer-before"/>
+              <hbox>
+                <spacer class="discover-spacer-before"/>
+                <hbox class="alert" align="center">
+                  <image class="discover-logo"/>
+                  <vbox flex="1" align="stretch">
+                    <label class="discover-title">&discover.title;</label>
+                    <description class="discover-description">&discover.description2;</description>
+                    <description class="discover-footer">&discover.footer;</description>
+                  </vbox>
+                </hbox>
+                <spacer class="discover-spacer-after"/>
+              </hbox>
+              <spacer class="alert-spacer-after"/>
+            </vbox>
+            <browser id="discover-browser" type="content" flex="1"
+                     disablehistory="true" homepage="about:blank"/>
+          </deck>
 
-  <hbox flex="1">
-
-    <!-- category list -->
-    <richlistbox id="categories">
-      <richlistitem id="category-search" value="addons://search/"
-                    class="category"
-                    name="&view.search.label;" priority="0"
-                    tooltiptext="&view.search.label;" disabled="true"/>
-      <richlistitem id="category-discover" value="addons://discover/"
-                    class="category"
-                    name="&view.discover.label;" priority="1000"
-                    tooltiptext="&view.discover.label;"/>
-      <richlistitem id="category-availableUpdates" value="addons://updates/available"
-                    class="category"
-                    name="&view.availableUpdates.label;" priority="100000"
-                    tooltiptext="&view.availableUpdates.label;"
-                    disabled="true"/>
-      <richlistitem id="category-recentUpdates" value="addons://updates/recent"
-                    class="category"
-                    name="&view.recentUpdates.label;" priority="101000"
-                    tooltiptext="&view.recentUpdates.label;" disabled="true"/>
-    </richlistbox>
-
-    <box id="view-port-container" class="main-content" flex="1">
-
-      <!-- view port -->
-      <deck id="view-port" flex="1" selectedIndex="0">
-
-        <!-- discover view -->
-        <deck id="discover-view" flex="1" class="view-pane" selectedIndex="0" tabindex="0">
-          <vbox id="discover-loading" align="center" pack="stretch" flex="1" class="alert-container">
-            <spacer class="alert-spacer-before"/>
-            <hbox class="alert loading" align="center">
-              <image/>
-              <label value="&loading.label;"/>
+          <!-- search view -->
+          <vbox id="search-view" flex="1" class="view-pane" tabindex="0">
+            <hbox class="view-header global-warning-container" align="center">
+              <!-- global warnings -->
+              <hbox class="global-warning" flex="1">
+                <hbox class="global-warning-safemode" flex="1" align="center"
+                      tooltiptext="&warning.safemode.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.safemode.label;"/>
+                </hbox>
+                <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+                      tooltiptext="&warning.checkcompatibility.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.checkcompatibility.label;"/>
+                </hbox>
+                <button class="button-link global-warning-checkcompatibility"
+                        label="&warning.checkcompatibility.enable.label;"
+                        tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+                        command="cmd_enableCheckCompatibility"/>
+                <hbox class="global-warning-updatesecurity" flex="1" align="center"
+                      tooltiptext="&warning.updatesecurity.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.updatesecurity.label;"/>
+                </hbox>
+                <button class="button-link global-warning-updatesecurity"
+                        label="&warning.updatesecurity.enable.label;"
+                        tooltiptext="&warning.updatesecurity.enable.tooltip;"
+                        command="cmd_enableUpdateSecurity"/>
+                <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+              </hbox>
+              <spacer flex="1"/>
+              <hbox id="search-sorters" class="sort-controls"
+                    showrelevance="true" sortby="relevancescore" ascending="false"/>
             </hbox>
-            <spacer class="alert-spacer-after"/>
+            <hbox id="search-filter" align="center">
+              <label id="search-filter-label" value="&search.filter2.label;"/>
+              <radiogroup id="search-filter-radiogroup" orient="horizontal"
+                          align="center" persist="value" value="remote">
+                <radio id="search-filter-local" class="search-filter-radio"
+                       label="&search.filter2.installed.label;" value="local"
+                       tooltiptext="&search.filter2.installed.tooltip;"/>
+                <radio id="search-filter-remote" class="search-filter-radio"
+                       label="&search.filter2.available.label;" value="remote"
+                       tooltiptext="&search.filter2.available.tooltip;"/>
+              </radiogroup>
+            </hbox>
+            <vbox id="search-loading" class="alert-container"
+                  flex="1" hidden="true">
+              <spacer class="alert-spacer-before"/>
+              <hbox class="alert loading" align="center">
+                <image/>
+                <label value="&loading.label;"/>
+              </hbox>
+              <spacer class="alert-spacer-after"/>
+            </vbox>
+            <vbox id="search-list-empty" class="alert-container"
+                  flex="1" hidden="true">
+              <spacer class="alert-spacer-before"/>
+              <vbox class="alert">
+                <label value="&listEmpty.search.label;"/>
+                <button class="discover-button"
+                        id="discover-button-search"
+                        label="&listEmpty.button.label;"
+                        command="cmd_goToDiscoverPane"/>
+              </vbox>
+              <spacer class="alert-spacer-after"/>
+            </vbox>
+            <richlistbox id="search-list" class="list" flex="1">
+              <hbox pack="center">
+                <label id="search-allresults-link" class="text-link"/>
+              </hbox>
+            </richlistbox>
           </vbox>
-          <vbox id="discover-error" align="center" pack="stretch" flex="1" class="alert-container">
-            <spacer class="alert-spacer-before"/>
-            <hbox>
-              <spacer class="discover-spacer-before"/>
-              <hbox class="alert" align="center">
-                <image class="discover-logo"/>
-                <vbox flex="1" align="stretch">
-                  <label class="discover-title">&discover.title;</label>
-                  <description class="discover-description">&discover.description2;</description>
-                  <description class="discover-footer">&discover.footer;</description>
-                </vbox>
+
+          <!-- list view -->
+          <vbox id="list-view" flex="1" class="view-pane" align="stretch" tabindex="0">
+            <hbox class="view-header global-warning-container">
+              <!-- global warnings -->
+              <hbox class="global-warning" flex="1">
+                <hbox class="global-warning-safemode" flex="1" align="center"
+                      tooltiptext="&warning.safemode.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.safemode.label;"/>
+                </hbox>
+                <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+                      tooltiptext="&warning.checkcompatibility.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.checkcompatibility.label;"/>
+                </hbox>
+                <button class="button-link global-warning-checkcompatibility"
+                        label="&warning.checkcompatibility.enable.label;"
+                        tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+                        command="cmd_enableCheckCompatibility"/>
+                <hbox class="global-warning-updatesecurity" flex="1" align="center"
+                      tooltiptext="&warning.updatesecurity.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.updatesecurity.label;"/>
+                </hbox>
+                <button class="button-link global-warning-updatesecurity"
+                        label="&warning.updatesecurity.enable.label;"
+                        tooltiptext="&warning.updatesecurity.enable.tooltip;"
+                        command="cmd_enableUpdateSecurity"/>
+                <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
               </hbox>
-              <spacer class="discover-spacer-after"/>
             </hbox>
-            <spacer class="alert-spacer-after"/>
-          </vbox>
-          <browser id="discover-browser" type="content" flex="1"
-                   disablehistory="true" homepage="about:blank"/>
-        </deck>
-
-        <!-- search view -->
-        <vbox id="search-view" flex="1" class="view-pane" tabindex="0">
-          <hbox class="view-header global-warning-container" align="center">
-            <!-- global warnings -->
-            <hbox class="global-warning" flex="1">
-              <hbox class="global-warning-safemode" flex="1" align="center"
-                    tooltiptext="&warning.safemode.label;">
-                <image class="warning-icon"/>
-                <label class="global-warning-text" flex="1" crop="end"
-                       value="&warning.safemode.label;"/>
+            <hbox class="view-header global-info-container plugin-info-container">
+              <hbox class="global-info" flex="1" align="center">
+                <button class="button-link global-info-plugincheck"
+                        label="&info.plugincheck.label;"
+                        tooltiptext="&info.plugincheck.tooltip;"
+                        command="cmd_pluginCheck"/>
+                <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
               </hbox>
-              <hbox class="global-warning-checkcompatibility" flex="1" align="center"
-                    tooltiptext="&warning.checkcompatibility.label;">
-                <image class="warning-icon"/>
-                <label class="global-warning-text" flex="1" crop="end"
-                       value="&warning.checkcompatibility.label;"/>
-              </hbox>
-              <button class="button-link global-warning-checkcompatibility"
-                      label="&warning.checkcompatibility.enable.label;"
-                      tooltiptext="&warning.checkcompatibility.enable.tooltip;"
-                      command="cmd_enableCheckCompatibility"/>
-              <hbox class="global-warning-updatesecurity" flex="1" align="center"
-                    tooltiptext="&warning.updatesecurity.label;">
-                <image class="warning-icon"/>
-                <label class="global-warning-text" flex="1" crop="end"
-                       value="&warning.updatesecurity.label;"/>
+            </hbox>
+            <hbox class="view-header global-info-container experiment-info-container">
+              <hbox class="global-info" flex="1" align="center">
+                <label value="&experiment.info.label;"/>
+                <button id="experiments-learn-more"
+                        label="&experiment.info.learnmore;"
+                        tooltiptext="&experiment.info.learnmore;"
+                        accesskey="&experiment.info.learnmore.accesskey;"
+                        command="cmd_experimentsLearnMore"/>
+                <button id="experiments-change-telemetry"
+                        label="&experiment.info.changetelemetry;"
+                        tooltiptext="&experiment.info.changetelemetry;"
+                        accesskey="&experiment.info.changetelemetry.accesskey;"
+                        command="cmd_experimentsOpenTelemetryPreferences"/>
+                <spacer flex="5000"/> <!-- Necessary to allow the message to wrap. -->
               </hbox>
-              <button class="button-link global-warning-updatesecurity"
-                      label="&warning.updatesecurity.enable.label;"
-                      tooltiptext="&warning.updatesecurity.enable.tooltip;"
-                      command="cmd_enableUpdateSecurity"/>
-              <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
             </hbox>
-            <spacer flex="1"/>
-            <hbox id="search-sorters" class="sort-controls"
-                  showrelevance="true" sortby="relevancescore" ascending="false"/>
-          </hbox>
-          <hbox id="search-filter" align="center">
-            <label id="search-filter-label" value="&search.filter2.label;"/>
-            <radiogroup id="search-filter-radiogroup" orient="horizontal"
-                        align="center" persist="value" value="remote">
-              <radio id="search-filter-local" class="search-filter-radio"
-                     label="&search.filter2.installed.label;" value="local"
-                     tooltiptext="&search.filter2.installed.tooltip;"/>
-              <radio id="search-filter-remote" class="search-filter-radio"
-                     label="&search.filter2.available.label;" value="remote"
-                     tooltiptext="&search.filter2.available.tooltip;"/>
-            </radiogroup>
-          </hbox>
-          <vbox id="search-loading" class="alert-container"
-                flex="1" hidden="true">
-            <spacer class="alert-spacer-before"/>
-            <hbox class="alert loading" align="center">
-              <image/>
-              <label value="&loading.label;"/>
-            </hbox>
-            <spacer class="alert-spacer-after"/>
+            <vbox id="addon-list-empty" class="alert-container"
+                  flex="1" hidden="true">
+              <spacer class="alert-spacer-before"/>
+              <vbox class="alert">
+                <label value="&listEmpty.installed.label;"/>
+                <button class="discover-button"
+                        id="discover-button-install"
+                        label="&listEmpty.button.label;"
+                        command="cmd_goToDiscoverPane"/>
+              </vbox>
+              <spacer class="alert-spacer-after"/>
+            </vbox>
+            <richlistbox id="addon-list" class="list" flex="1"/>
           </vbox>
-          <vbox id="search-list-empty" class="alert-container"
-                flex="1" hidden="true">
-            <spacer class="alert-spacer-before"/>
-            <vbox class="alert">
-              <label value="&listEmpty.search.label;"/>
-              <button class="discover-button"
-                      id="discover-button-search"
-                      label="&listEmpty.button.label;"
-                      command="cmd_goToDiscoverPane"/>
+          <!-- updates view -->
+          <vbox id="updates-view" flex="1" class="view-pane" tabindex="0">
+            <hbox class="view-header global-warning-container" align="center">
+              <!-- global warnings -->
+              <hbox class="global-warning" flex="1">
+                <hbox class="global-warning-safemode" flex="1" align="center"
+                      tooltiptext="&warning.safemode.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.safemode.label;"/>
+                </hbox>
+                <hbox class="global-warning-checkcompatibility" flex="1" align="center"
+                      tooltiptext="&warning.checkcompatibility.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.checkcompatibility.label;"/>
+                </hbox>
+                <button class="button-link global-warning-checkcompatibility"
+                        label="&warning.checkcompatibility.enable.label;"
+                        tooltiptext="&warning.checkcompatibility.enable.tooltip;"
+                        command="cmd_enableCheckCompatibility"/>
+                <hbox class="global-warning-updatesecurity" flex="1" align="center"
+                      tooltiptext="&warning.updatesecurity.label;">
+                  <image class="warning-icon"/>
+                  <label class="global-warning-text" flex="1" crop="end"
+                         value="&warning.updatesecurity.label;"/>
+                </hbox>
+                <button class="button-link global-warning-updatesecurity"
+                        label="&warning.updatesecurity.enable.label;"
+                        tooltiptext="&warning.updatesecurity.enable.tooltip;"
+                        command="cmd_enableUpdateSecurity"/>
+                <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
+              </hbox>
+              <spacer flex="1"/>
+              <hbox id="updates-sorters" class="sort-controls" sortby="updateDate"
+                    ascending="false"/>
+            </hbox>
+            <vbox id="updates-list-empty" class="alert-container"
+                  flex="1" hidden="true">
+              <spacer class="alert-spacer-before"/>
+              <vbox class="alert">
+                <label id="empty-availableUpdates-msg" value="&listEmpty.availableUpdates.label;"/>
+                <label id="empty-recentUpdates-msg" value="&listEmpty.recentUpdates.label;"/>
+                <button label="&listEmpty.findUpdates.label;"
+                        command="cmd_findAllUpdates"/>
+              </vbox>
+              <spacer class="alert-spacer-after"/>
             </vbox>
-            <spacer class="alert-spacer-after"/>
+            <hbox id="update-actions" pack="center">
+              <button id="update-selected-btn" hidden="true"
+                      label="&updates.updateSelected.label;"
+                      tooltiptext="&updates.updateSelected.tooltip;"/>
+            </hbox>
+            <richlistbox id="updates-list" class="list" flex="1"/>
           </vbox>
-          <richlistbox id="search-list" class="list" flex="1">
-            <hbox pack="center">
-              <label id="search-allresults-link" class="text-link"/>
-            </hbox>
-          </richlistbox>
-        </vbox>
 
-        <!-- list view -->
-        <vbox id="list-view" flex="1" class="view-pane" align="stretch" tabindex="0">
-          <hbox class="view-header global-warning-container">
+          <!-- detail view -->
+          <scrollbox id="detail-view" flex="1" class="view-pane addon-view" orient="vertical" tabindex="0"
+                     role="document">
             <!-- global warnings -->
-            <hbox class="global-warning" flex="1">
+            <hbox class="global-warning-container global-warning">
               <hbox class="global-warning-safemode" flex="1" align="center"
                     tooltiptext="&warning.safemode.label;">
                 <image class="warning-icon"/>
                 <label class="global-warning-text" flex="1" crop="end"
                        value="&warning.safemode.label;"/>
               </hbox>
               <hbox class="global-warning-checkcompatibility" flex="1" align="center"
                     tooltiptext="&warning.checkcompatibility.label;">
@@ -344,345 +483,213 @@
                        value="&warning.updatesecurity.label;"/>
               </hbox>
               <button class="button-link global-warning-updatesecurity"
                       label="&warning.updatesecurity.enable.label;"
                       tooltiptext="&warning.updatesecurity.enable.tooltip;"
                       command="cmd_enableUpdateSecurity"/>
               <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
             </hbox>
-          </hbox>
-          <hbox class="view-header global-info-container plugin-info-container">
-            <hbox class="global-info" flex="1" align="center">
-              <button class="button-link global-info-plugincheck"
-                      label="&info.plugincheck.label;"
-                      tooltiptext="&info.plugincheck.tooltip;"
-                      command="cmd_pluginCheck"/>
-              <spacer flex="5000"/> <!-- Necessary to allow the message to wrap -->
-            </hbox>
-          </hbox>
-          <hbox class="view-header global-info-container experiment-info-container">
-            <hbox class="global-info" flex="1" align="center">
-              <label value="&experiment.info.label;"/>
-              <button id="experiments-learn-more"
-                      label="&experiment.info.learnmore;"
-                      tooltiptext="&experiment.info.learnmore;"
-                      accesskey="&experiment.info.learnmore.accesskey;"
-                      command="cmd_experimentsLearnMore"/>
-              <button id="experiments-change-telemetry"
-                      label="&experiment.info.changetelemetry;"
-                      tooltiptext="&experiment.info.changetelemetry;"
-                      accesskey="&experiment.info.changetelemetry.accesskey;"
-                      command="cmd_experimentsOpenTelemetryPreferences"/>
-              <spacer flex="5000"/> <!-- Necessary to allow the message to wrap. -->
-            </hbox>
-          </hbox>
-