Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 27 Aug 2015 11:56:51 -0400
changeset 292199 ca086f9ef8bca2d6cdfa79bfc4c854f56a59859e
parent 292175 638ac65d9b3f49590d00ad6d18e45930c4fdc7ea (current diff)
parent 292198 49a82a658d1cd759afb1822944f06eed867e064d (diff)
child 292200 e329de85724b047842e94f32599edcb7534d8bd2
child 292219 cbb955b0495c9f5a6775a2029f2ac9187d54c93c
child 292263 d8bef3a664a7020437275c547b5f021b7a3aba1c
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone43.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 CLOSED TREE
browser/base/content/test/newtab/head.js
mobile/android/base/resources/color-large-v11/tab_item_title.xml
mobile/android/base/resources/drawable-large-hdpi-v11/tablet_tab_close.png
mobile/android/base/resources/drawable-large-hdpi-v11/tablet_tab_close_active.png
mobile/android/base/resources/drawable-large-v11/tab_item_close_button.xml
mobile/android/base/resources/drawable-large-xhdpi-v11/tablet_tab_close.png
mobile/android/base/resources/drawable-large-xhdpi-v11/tablet_tab_close_active.png
mobile/android/base/resources/drawable-large-xxhdpi-v11/tablet_tab_close.png
mobile/android/base/resources/drawable-large-xxhdpi-v11/tablet_tab_close_active.png
mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml
mobile/android/base/resources/layout/tabs_item_cell.xml
mobile/android/base/resources/layout/tabs_item_row.xml
mobile/android/base/resources/values-land/layout.xml
mobile/android/base/resources/values-large-v11/layout.xml
--- a/.gitignore
+++ b/.gitignore
@@ -47,18 +47,18 @@ parser/html/java/htmlparser/
 parser/html/java/javaparser/
 
 # Ignore the files and directory that Eclipse IDE creates
 .project
 .cproject
 .settings/
 
 # Python virtualenv artifacts.
-python/psutil/*.so
-python/psutil/*.pyd
+python/psutil/**/*.so
+python/psutil/**/*.pyd
 python/psutil/build/
 
 # Ignore chrome.manifest files from the devtools loader
 browser/devtools/chrome.manifest
 toolkit/devtools/chrome.manifest
 
 # Tag files generated by GNU Global
 GTAGS
--- a/browser/base/content/newtab/grid.js
+++ b/browser/base/content/newtab/grid.js
@@ -4,16 +4,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 #endif
 
 /**
  * Define various fixed dimensions
  */
 const GRID_BOTTOM_EXTRA = 7; // title's line-height extends 7px past the margin
 const GRID_WIDTH_EXTRA = 1; // provide 1px buffer to allow for rounding error
+const SPONSORED_TAG_BUFFER = 2; // 2px buffer to clip off top of sponsored tag
 
 /**
  * This singleton represents the grid that contains all sites.
  */
 let gGrid = {
   /**
    * The DOM node of the grid.
    */
@@ -171,16 +172,25 @@ let gGrid = {
       '       class="newtab-control newtab-control-block"/>' +
       '<span class="newtab-suggested"/>';
 
     this._siteFragment = document.createDocumentFragment();
     this._siteFragment.appendChild(site);
   },
 
   /**
+   * Test a tile at a given position for being pinned or history
+   * @param position Position in sites array
+   */
+  _isHistoricalTile: function Grid_isHistoricalTile(aPos) {
+    let site = this.sites[aPos];
+    return site && (site.isPinned() || site.link && site.link.type == "history");
+  },
+
+  /**
    * Make sure the correct number of rows and columns are visible
    */
   _resizeGrid: function Grid_resizeGrid() {
     // If we're somehow called before the page has finished loading,
     // let's bail out to avoid caching zero heights and widths.
     // We'll be called again when DOMContentLoaded fires.
     // Same goes for the grid if that's not ready yet.
     if (!this.isDocumentLoaded || !this._ready) {
@@ -190,14 +200,55 @@ let gGrid = {
     // Save the cell's computed height/width including margin and border
     if (this._cellMargin === undefined) {
       let refCell = document.querySelector(".newtab-cell");
       this._cellMargin = parseFloat(getComputedStyle(refCell).marginTop);
       this._cellHeight = refCell.offsetHeight + this._cellMargin +
         parseFloat(getComputedStyle(refCell).marginBottom);
       this._cellWidth = refCell.offsetWidth + this._cellMargin;
     }
-    this._node.style.height = this._computeHeight() + "px";
-    this._node.style.maxHeight = this._node.style.height;
+
+    let searchContainer = document.querySelector("#newtab-search-container");
+    // Save search-container margin height
+    if (this._searchContainerMargin  === undefined) {
+      this._searchContainerMargin = parseFloat(getComputedStyle(searchContainer).marginBottom) +
+                                    parseFloat(getComputedStyle(searchContainer).marginTop);
+    }
+
+    // Find the number of rows we can place into view port
+    let availHeight = document.documentElement.clientHeight - this._cellMargin -
+                      searchContainer.offsetHeight - this._searchContainerMargin;
+    let visibleRows = Math.floor(availHeight / this._cellHeight);
+
+    // Find the number of columns that fit into view port
+    let maxGridWidth = gGridPrefs.gridColumns * this._cellWidth + GRID_WIDTH_EXTRA;
+    // available width is current grid width, but no greater than maxGridWidth
+    let availWidth = Math.min(document.querySelector("#newtab-grid").clientWidth,
+                              maxGridWidth);
+    // finally get the number of columns we can fit into view port
+    let gridColumns =  Math.floor(availWidth / this._cellWidth);
+    // walk sites backwords until a pinned or history tile is found or visibleRows reached
+    let tileIndex = Math.min(gGridPrefs.gridRows * gridColumns, this.sites.length) - 1;
+    while (tileIndex >= visibleRows * gridColumns) {
+      if (this._isHistoricalTile(tileIndex)) {
+        break;
+      }
+      tileIndex --;
+    }
+
+    // Compute the actual number of grid rows we will display (potentially
+    // with a scroll bar). tileIndex now points to a historical tile with
+    // heighest index or to the last index of the visible row, if none found
+    // Dividing tileIndex by number of tiles in a column gives the rows
+    let gridRows = Math.floor(tileIndex / gridColumns) + 1;
+
+    // we need to set grid width, for otherwise the scrollbar may shrink
+    // the grid when shown and cause grid layout to be different from
+    // what being computed above. This, in turn, may cause scrollbar shown
+    // for directory tiles, and introduce jitter when grid width is aligned
+    // exactly on the column boundary
+    this._node.style.width = gridColumns * this._cellWidth + "px";
     this._node.style.maxWidth = gGridPrefs.gridColumns * this._cellWidth +
                                 GRID_WIDTH_EXTRA + "px";
+    this._node.style.height = this._computeHeight() + "px";
+    this._node.style.maxHeight = this._computeHeight(gridRows) - SPONSORED_TAG_BUFFER + "px";
   }
 };
--- a/browser/base/content/test/newtab/browser.ini
+++ b/browser/base/content/test/newtab/browser.ini
@@ -44,8 +44,9 @@ support-files =
   ../general/searchSuggestionEngine.xml
   ../general/searchSuggestionEngine.sjs
 [browser_newtab_sponsored_icon_click.js]
 [browser_newtab_undo.js]
 [browser_newtab_unpin.js]
 [browser_newtab_update.js]
 [browser_newtab_bug1145428.js]
 [browser_newtab_bug1178586.js]
+[browser_newtab_bug1194895.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/newtab/browser_newtab_bug1194895.js
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PRELOAD_PREF = "browser.newtab.preload";
+const PREF_NEWTAB_COLUMNS = "browser.newtabpage.columns";
+const PREF_NEWTAB_ROWS = "browser.newtabpage.rows";
+
+function populateDirectoryTiles() {
+  let directoryTiles = [];
+  let i = 0;
+  while (i++ < 14) {
+    directoryTiles.push({
+      directoryId: i,
+      url: "http://example" + i + ".com/",
+      enhancedImageURI: "data:image/png;base64,helloWORLD",
+      title: "dirtitle" + i,
+      type: "affiliate"
+    });
+  }
+  return directoryTiles;
+}
+
+gDirectorySource = "data:application/json," + JSON.stringify({
+  "directory": populateDirectoryTiles()
+});
+
+
+function runTests() {
+  let origEnhanced = NewTabUtils.allPages.enhanced;
+  let origCompareLinks = NewTabUtils.links.compareLinks;
+  registerCleanupFunction(() => {
+    Services.prefs.clearUserPref(PRELOAD_PREF);
+    Services.prefs.clearUserPref(PREF_NEWTAB_ROWS);
+    Services.prefs.clearUserPref(PREF_NEWTAB_COLUMNS);
+    NewTabUtils.allPages.enhanced = origEnhanced;
+    NewTabUtils.links.compareLinks = origCompareLinks;
+  });
+
+  // turn off preload to ensure grid updates on every setLinks
+  Services.prefs.setBoolPref(PRELOAD_PREF, false);
+  // set newtab to have three columns only
+  Services.prefs.setIntPref(PREF_NEWTAB_COLUMNS, 3);
+  Services.prefs.setIntPref(PREF_NEWTAB_ROWS, 5);
+
+  yield addNewTabPageTab();
+  yield customizeNewTabPage("enhanced"); // Toggle enhanced off
+
+  // Testing history tiles
+
+  // two rows of tiles should always fit on any screen
+  yield setLinks("0,1,2,3,4,5");
+  yield addNewTabPageTab();
+
+  // should do not see scrollbar since tiles fit into visible space
+  checkGrid("0,1,2,3,4,5");
+  ok(!hasScrollbar(), "no scrollbar");
+
+  // add enough tiles to cause extra two rows and observe scrollbar
+  yield setLinks("0,1,2,3,4,5,6,7,8,9");
+  yield addNewTabPageTab();
+  checkGrid("0,1,2,3,4,5,6,7,8,9");
+  ok(hasScrollbar(), "document has scrollbar");
+
+  // pin the last tile to make it stay at the bottom of the newtab
+  pinCell(9);
+  // block first 6 tiles, which should not remove the scroll bar
+  // since the last tile is pinned in the nineth position
+  for (let i = 0; i < 6; i++) {
+    yield blockCell(0);
+  }
+  yield addNewTabPageTab();
+  checkGrid("6,7,8,,,,,,,9p");
+  ok(hasScrollbar(), "document has scrollbar when tile is pinned to the last row");
+
+  // unpin the site: this will move tile up and make scrollbar disappear
+  yield unpinCell(9);
+  yield addNewTabPageTab();
+  checkGrid("6,7,8,9");
+  ok(!hasScrollbar(), "no scrollbar when bottom row tile is unpinned");
+
+  // reset everything to clean slate
+  NewTabUtils.restore();
+
+  // Testing directory tiles
+  yield customizeNewTabPage("enhanced"); // Toggle enhanced on
+
+  // setup page with no history tiles to test directory only display
+  yield setLinks([]);
+  yield addNewTabPageTab();
+  ok(!hasScrollbar(), "no scrollbar for directory tiles");
+
+  // introduce one history tile - it should occupy the last
+  // available slot at the bottom of newtab and cause scrollbar
+  yield setLinks("41");
+  yield addNewTabPageTab();
+  ok(hasScrollbar(), "adding low frecency history site causes scrollbar");
+
+  // set PREF_NEWTAB_ROWS to 4, that should clip off the history tile
+  // and remove scroll bar
+  Services.prefs.setIntPref(PREF_NEWTAB_ROWS, 4);
+  yield addNewTabPageTab();
+  ok(!hasScrollbar(), "no scrollbar if history tiles falls past max rows");
+
+  // restore max rows and watch scrollbar re-appear
+  Services.prefs.setIntPref(PREF_NEWTAB_ROWS, 5);
+  yield addNewTabPageTab();
+  ok(hasScrollbar(), "scrollbar is back when max rows allow for bottom history tile");
+
+  // block that history tile, and watch scrollbar disappear
+  yield blockCell(14);
+  yield addNewTabPageTab();
+  ok(!hasScrollbar(), "no scrollbar after bottom history tiles is blocked");
+
+  // Test well-populated user history - newtab has highly-frecent history sites
+  // redefine compareLinks to always choose history tiles first
+  NewTabUtils.links.compareLinks = function (aLink1, aLink2) {
+    if (aLink1.type == aLink2.type) {
+      return aLink2.frecency - aLink1.frecency ||
+             aLink2.lastVisitDate - aLink1.lastVisitDate;
+    }
+    else {
+      if (aLink2.type == "history") {
+        return 1;
+      }
+      else {
+        return -1;
+      }
+    }
+  };
+
+  // add a row of history tiles, directory tiles will be clipped off, hence no scrollbar
+  yield setLinks("31,32,33");
+  yield addNewTabPageTab();
+  ok(!hasScrollbar(), "no scrollbar when directory tiles follow history tiles");
+
+  // fill first four rows with history tiles and observer scrollbar
+  yield setLinks("30,31,32,33,34,35,36,37,38,39");
+  yield addNewTabPageTab();
+  ok(hasScrollbar(), "scrollbar appears when history tiles need extra row");
+
+}
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -780,8 +780,16 @@ function customizeNewTabPage(aTheme) {
 
     let closed = panelOpened(false);
     customizeButton.click();
     yield closed;
   });
 
   promise.then(TestRunner.next);
 }
+
+/**
+ * Reports presence of a scrollbar
+ */
+function hasScrollbar() {
+  let docElement = getContentDocument().documentElement;
+  return docElement.scrollHeight > docElement.clientHeight;
+}
--- a/browser/base/content/test/plugins/browser.ini
+++ b/browser/base/content/test/plugins/browser.ini
@@ -72,9 +72,10 @@ skip-if = (os == 'win' && os_version == 
 skip-if = !e10s
 [browser_globalplugin_crashinfobar.js]
 skip-if = !crashreporter
 [browser_pluginCrashCommentAndURL.js]
 skip-if = !crashreporter
 [browser_pageInfo_plugins.js]
 [browser_pluginCrashReportNonDeterminism.js]
 skip-if = !crashreporter || os == 'linux' # Bug 1152811
+[browser_private_clicktoplay.js]
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/plugins/browser_private_clicktoplay.js
@@ -0,0 +1,235 @@
+let rootDir = getRootDirectory(gTestPath);
+const gTestRoot = rootDir;
+const gHttpTestRoot = rootDir.replace("chrome://mochitests/content/", "http://127.0.0.1:8888/");
+
+let gTestBrowser = null;
+let gNextTest = null;
+let gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+let gPrivateWindow = null;
+let gPrivateBrowser = null;
+
+function pageLoad(aEvent) {
+  // The plugin events are async dispatched and can come after the load event
+  // This just allows the events to fire before we then go on to test the states
+  executeSoon(gNextTest);
+  gNextTest = null;
+}
+
+function prepareTest(nextTest, url, window) {
+  gNextTest = nextTest;
+  if (!window)
+    window = gTestBrowser.contentWindow;
+  window.location = url;
+}
+
+function finishTest() {
+  clearAllPluginPermissions();
+  gTestBrowser.removeEventListener("load", pageLoad, true);
+  gBrowser.removeCurrentTab();
+  if (gPrivateWindow) {
+    gPrivateWindow.close();
+  }
+  window.focus();
+  finish();
+}
+
+function createPrivateWindow(nextTest, url) {
+  gPrivateWindow = OpenBrowserWindow({private: true});
+  ok(!!gPrivateWindow, "should have created a private window.");
+  whenDelayedStartupFinished(gPrivateWindow, function() {
+    gPrivateBrowser = gPrivateWindow.getBrowser().selectedBrowser;
+    gPrivateBrowser.addEventListener("load", pageLoad, true);
+    gNextTest = function() {
+      prepareTest(nextTest, url, gPrivateBrowser.contentWindow);
+    };
+  });
+}
+
+function whenDelayedStartupFinished(aWindow, aCallback) {
+  Services.obs.addObserver(function observer(aSubject, aTopic) {
+    if (aWindow == aSubject) {
+      Services.obs.removeObserver(observer, aTopic);
+      executeSoon(aCallback);
+    }
+  }, "browser-delayed-startup-finished", false);
+}
+
+function test() {
+  waitForExplicitFinish();
+  registerCleanupFunction(function() {
+    clearAllPluginPermissions();
+    getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+    getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+  });
+
+  let newTab = gBrowser.addTab();
+  gBrowser.selectedTab = newTab;
+  gTestBrowser = gBrowser.selectedBrowser;
+  gTestBrowser.addEventListener("load", pageLoad, true);
+
+  Services.prefs.setBoolPref("plugins.click_to_play", true);
+  getTestPlugin().enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+  getTestPlugin("Second Test Plug-in").enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+  gNextTest = test1a;
+}
+
+function test1a() {
+  createPrivateWindow(test1b, gHttpTestRoot + "plugin_test.html");
+}
+
+function test1b() {
+  let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+  ok(popupNotification, "Test 1b, Should have a click-to-play notification");
+
+  let plugin = gPrivateBrowser.contentDocument.getElementById("test");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  ok(!objLoadingContent.activated, "Test 1b, Plugin should not be activated");
+
+  // Check the button status
+  let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+                                                   "Shown");
+  popupNotification.reshow();
+  promiseShown.then(() => {
+    let button1 = gPrivateWindow.PopupNotifications.panel.firstChild._primaryButton;
+    let button2 = gPrivateWindow.PopupNotifications.panel.firstChild._secondaryButton;
+    is(button1.getAttribute("action"), "_singleActivateNow", "Test 1b, Blocked plugin in private window should have a activate now button");
+    ok(button2.hidden, "Test 1b, Blocked plugin in a private window should not have a secondary button")
+
+    gPrivateWindow.close();
+    prepareTest(test2a, gHttpTestRoot + "plugin_test.html");
+  });
+}
+
+function test2a() {
+  // enable test plugin on this site
+  let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+  ok(popupNotification, "Test 2a, Should have a click-to-play notification");
+
+  let plugin = gTestBrowser.contentDocument.getElementById("test");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  ok(!objLoadingContent.activated, "Test 2a, Plugin should not be activated");
+
+  // Simulate clicking the "Allow Now" button.
+  let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                   "Shown");
+  popupNotification.reshow();
+  promiseShown.then(() => {
+    PopupNotifications.panel.firstChild._secondaryButton.click();
+
+    let condition = function() objLoadingContent.activated;
+    waitForCondition(condition, test2b, "Test 2a, Waited too long for plugin to activate");
+  });
+}
+
+function test2b() {
+  createPrivateWindow(test2c, gHttpTestRoot + "plugin_test.html");
+}
+
+function test2c() {
+  let promise = TestUtils.topicObserved("PopupNotifications-updateNotShowing");
+  promise.then(() => {
+    let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+    ok(popupNotification, "Test 2c, Should have a click-to-play notification");
+
+    let plugin = gPrivateBrowser.contentDocument.getElementById("test");
+    let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+    ok(objLoadingContent.activated, "Test 2c, Plugin should be activated");
+
+    // Check the button status
+    let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+                                                     "Shown");
+    popupNotification.reshow();
+    promiseShown.then(() => {
+      let buttonContainer = gPrivateWindow.PopupNotifications.panel.firstChild._buttonContainer;
+      ok(buttonContainer.hidden, "Test 2c, Activated plugin in a private window should not have visible buttons");
+
+      clearAllPluginPermissions();
+      gPrivateWindow.close();
+      prepareTest(test3a, gHttpTestRoot + "plugin_test.html");
+    });
+  });
+}
+
+function test3a() {
+  // enable test plugin on this site
+  let popupNotification = PopupNotifications.getNotification("click-to-play-plugins", gTestBrowser);
+  ok(popupNotification, "Test 3a, Should have a click-to-play notification");
+
+  let plugin = gTestBrowser.contentDocument.getElementById("test");
+  let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+  ok(!objLoadingContent.activated, "Test 3a, Plugin should not be activated");
+
+  // Simulate clicking the "Allow Always" button.
+  let promiseShown = BrowserTestUtils.waitForEvent(PopupNotifications.panel,
+                                                   "Shown");
+  popupNotification.reshow();
+  promiseShown.then(() => {
+    PopupNotifications.panel.firstChild._secondaryButton.click();
+
+    let condition = function() objLoadingContent.activated;
+    waitForCondition(condition, test3b, "Test 3a, Waited too long for plugin to activate");
+  });
+}
+
+function test3b() {
+  createPrivateWindow(test3c, gHttpTestRoot + "plugin_test.html");
+}
+
+function test3c() {
+  let promise = TestUtils.topicObserved("PopupNotifications-updateNotShowing");
+  promise.then(() => {
+    let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+    ok(popupNotification, "Test 3c, Should have a click-to-play notification");
+
+    // Check the button status
+    let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+                                                     "Shown");
+    popupNotification.reshow();
+    promiseShown.then(() => {
+      let buttonContainer = gPrivateWindow.PopupNotifications.panel.firstChild._buttonContainer;
+      ok(buttonContainer.hidden, "Test 3c, Activated plugin in a private window should not have visible buttons");
+
+      prepareTest(test3d, gHttpTestRoot + "plugin_two_types.html", gPrivateBrowser.contentWindow);
+    });
+  });
+}
+
+function test3d() {
+  let popupNotification = gPrivateWindow.PopupNotifications.getNotification("click-to-play-plugins", gPrivateBrowser);
+  ok(popupNotification, "Test 3d, Should have a click-to-play notification");
+
+  // Check the list item status
+  let promiseShown = BrowserTestUtils.waitForEvent(gPrivateWindow.PopupNotifications.panel,
+                                                   "Shown");
+  popupNotification.reshow();
+  promiseShown.then(() => {
+    let doc = gPrivateWindow.document;
+    for (let item of gPrivateWindow.PopupNotifications.panel.firstChild.childNodes) {
+      let allowalways = doc.getAnonymousElementByAttribute(item, "anonid", "allowalways");
+      ok(allowalways, "Test 3d, should have list item for allow always");
+      let allownow = doc.getAnonymousElementByAttribute(item, "anonid", "allownow");
+      ok(allownow, "Test 3d, should have list item for allow now");
+      let block = doc.getAnonymousElementByAttribute(item, "anonid", "block");
+      ok(block, "Test 3d, should have list item for block");
+
+      if (item.action.pluginName === "Test") {
+        is(item.value, "allowalways", "Test 3d, Plugin should bet set to 'allow always'");
+        ok(!allowalways.hidden, "Test 3d, Plugin set to 'always allow' should have a visible 'always allow' action.");
+        ok(allownow.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'allow now' action.");
+        ok(block.hidden, "Test 3d, Plugin set to 'always allow' should have an invisible 'block' action.");
+      } else if (item.action.pluginName === "Second Test") {
+        is(item.value, "block", "Test 3d, Second plugin should bet set to 'block'");
+        ok(allowalways.hidden, "Test 3d, Plugin set to 'block' should have a visible 'always allow' action.");
+        ok(!allownow.hidden, "Test 3d, Plugin set to 'block' should have a visible 'allow now' action.");
+        ok(!block.hidden, "Test 3d, Plugin set to 'block' should have a visible 'block' action.");
+      } else {
+        ok(false, "Test 3d, Unexpected plugin '"+item.action.pluginName+"'");
+      }
+    }
+
+    finishTest();
+  });
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -2397,16 +2397,30 @@ file, You can obtain one at http://mozil
           case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
             warningString = gNavigatorBundle.getString("pluginActivateVulnerable.label");
             linkString = gNavigatorBundle.getString("pluginActivate.riskLabel");
             break;
           }
         }
         document.getAnonymousElementByAttribute(this, "anonid", "center-item-warning-label").value = warningString;
 
+        let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
+        let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);
+
+        if (isWindowPrivate) {
+          // TODO: temporary compromise of hiding some privacy leaks, remove once bug 892487 is fixed
+          let allowalways = document.getAnonymousElementByAttribute(this, "anonid", "allowalways");
+          let block = document.getAnonymousElementByAttribute(this, "anonid", "block");
+          let allownow = document.getAnonymousElementByAttribute(this, "anonid", "allownow");
+
+          allowalways.hidden = curState !== "allowalways";
+          block.hidden       = curState !== "block";
+          allownow.hidden    = curState === "allowalways";
+        }
+
         if (url || linkHandler) {
           link.value = linkString;
           if (url) {
             link.href = url;
           }
           if (linkHandler) {
             link.addEventListener("click", linkHandler, false);
           }
@@ -2572,16 +2586,18 @@ file, You can obtain one at http://mozil
           }
           this._setupLink(null);
         ]]></body>
       </method>
       <method name="_setupSingleState">
         <body><![CDATA[
           var action = this._items[0].action;
           var prePath = action.pluginPermissionPrePath;
+          let chromeWin = window.QueryInterface(Ci.nsIDOMChromeWindow);
+          let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWin);
 
           let label, linkLabel, linkUrl, button1, button2;
 
           if (action.fallbackType == Ci.nsIObjectLoadingContent.PLUGIN_ACTIVE) {
             button1 = {
               label: "pluginBlockNow.label",
               accesskey: "pluginBlockNow.accesskey",
               action: "_singleBlock"
@@ -2610,16 +2626,21 @@ file, You can obtain one at http://mozil
             case Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE:
               label = "pluginEnabledVulnerable.message";
               linkLabel = "pluginActivate.riskLabel"
               break;
 
             default:
               Cu.reportError(Error("Unexpected blocklist state"));
             }
+
+            // TODO: temporary compromise, remove this once bug 892487 is fixed
+            if (isWindowPrivate) {
+              this._buttonContainer.hidden = true;
+            }
           }
           else if (action.pluginTag.enabledState == Ci.nsIPluginTag.STATE_DISABLED) {
             let linkElement =
               document.getAnonymousElementByAttribute(
                          this, "anonid", "click-to-play-plugins-notification-link");
             linkElement.textContent = gNavigatorBundle.getString("pluginActivateDisabled.manage");
             linkElement.setAttribute("onclick", "gPluginHandler.managePlugins()");
 
@@ -2665,16 +2686,22 @@ file, You can obtain one at http://mozil
               label = "pluginActivateVulnerable.message";
               linkLabel = "pluginActivate.riskLabel"
               button1.default = true;
               break;
 
             default:
               Cu.reportError(Error("Unexpected blocklist state"));
             }
+
+            // TODO: temporary compromise, remove this once bug 892487 is fixed
+            if (isWindowPrivate) {
+              button1.default = true;
+              this._secondaryButton.hidden = true;
+            }
           }
           this._setupDescription(label, action.pluginName, prePath);
           this._setupLink(linkLabel, action.detailsLink);
 
           this._primaryButton.label = gNavigatorBundle.getString(button1.label);
           this._primaryButton.accessKey = gNavigatorBundle.getString(button1.accesskey);
           this._primaryButton.setAttribute("action", button1.action);
 
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -486,16 +486,17 @@ loop.roomViews = (function(mozL10n) {
           ), 
           React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")), 
           React.createElement(sharedViews.Checkbox, {
             additionalClass: cx({ hide: !checkboxLabel }), 
             checked: checked, 
             disabled: checked, 
             label: checkboxLabel, 
             onChange: this.handleCheckboxChange, 
+            useEllipsis: true, 
             value: location}), 
           React.createElement("form", {onSubmit: this.handleFormSubmit}, 
             React.createElement("input", {className: "room-context-name", 
               maxLength: this.maxRoomNameLength, 
               onKeyDown: this.handleTextareaKeyDown, 
               placeholder: mozL10n.get("context_edit_name_placeholder"), 
               type: "text", 
               valueLink: this.linkState("newRoomName")}), 
@@ -503,17 +504,17 @@ loop.roomViews = (function(mozL10n) {
               disabled: availableContext && availableContext.url === this.state.newRoomURL, 
               onKeyDown: this.handleTextareaKeyDown, 
               placeholder: "https://", 
               type: "text", 
               valueLink: this.linkState("newRoomURL")}), 
             React.createElement("textarea", {className: "room-context-comments", 
               onKeyDown: this.handleTextareaKeyDown, 
               placeholder: mozL10n.get("context_edit_comments_placeholder"), 
-              rows: "3", type: "text", 
+              rows: "2", type: "text", 
               valueLink: this.linkState("newRoomDescription")})
           ), 
           React.createElement("button", {className: "btn btn-info", 
                   disabled: this.props.savingContext, 
                   onClick: this.handleFormSubmit}, 
             mozL10n.get("context_save_label2")
           ), 
           React.createElement("button", {className: "room-context-btn-close", 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -486,16 +486,17 @@ loop.roomViews = (function(mozL10n) {
           </p>
           <div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
           <sharedViews.Checkbox
             additionalClass={cx({ hide: !checkboxLabel })}
             checked={checked}
             disabled={checked}
             label={checkboxLabel}
             onChange={this.handleCheckboxChange}
+            useEllipsis={true}
             value={location} />
           <form onSubmit={this.handleFormSubmit}>
             <input className="room-context-name"
               maxLength={this.maxRoomNameLength}
               onKeyDown={this.handleTextareaKeyDown}
               placeholder={mozL10n.get("context_edit_name_placeholder")}
               type="text"
               valueLink={this.linkState("newRoomName")} />
@@ -503,17 +504,17 @@ loop.roomViews = (function(mozL10n) {
               disabled={availableContext && availableContext.url === this.state.newRoomURL}
               onKeyDown={this.handleTextareaKeyDown}
               placeholder="https://"
               type="text"
               valueLink={this.linkState("newRoomURL")} />
             <textarea className="room-context-comments"
               onKeyDown={this.handleTextareaKeyDown}
               placeholder={mozL10n.get("context_edit_comments_placeholder")}
-              rows="3" type="text"
+              rows="2" type="text"
               valueLink={this.linkState("newRoomDescription")} />
           </form>
           <button className="btn btn-info"
                   disabled={this.props.savingContext}
                   onClick={this.handleFormSubmit}>
             {mozL10n.get("context_save_label2")}
           </button>
           <button className="room-context-btn-close"
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -519,16 +519,22 @@ html[dir="rtl"] .checkbox {
 .checkbox.disabled {
   border: 1px solid #909090;
 }
 
 .checkbox.checked.disabled {
   background-image: url("../img/check.svg#check-disabled");
 }
 
+.checkbox-label.ellipsis {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+
 /* ContextUrlView classes */
 
 .context-content {
   color: black;
   text-align: left;
 }
 
 html[dir="rtl"] .context-content {
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -892,16 +892,20 @@ body[platform="win"] .share-service-drop
 .context-url-view-wrapper {
   /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
      0.5rem for padding of .text-chat-entry > p */
   padding: calc(18px - 1px - 0.5rem);
   margin-bottom: 0.5em;
   background-color: #dbf7ff;
 }
 
+.showing-room-name > .text-chat-entries > .text-chat-scroller > .context-url-view-wrapper {
+  padding-top: 0;
+}
+
 .room-context {
   background: rgba(0,0,0,.8);
   border-top: 2px solid #444;
   border-bottom: 2px solid #444;
   padding: .5rem;
   position: absolute;
   left: 0;
   bottom: 0;
@@ -940,16 +944,17 @@ body[platform="win"] .share-service-drop
 .room-context > .error-display-area.error {
   margin: 1em 0 .5em 0;
   text-align: center;
   text-shadow: 1px 1px 0 rgba(0,0,0,.3);
 }
 
 .room-context > .checkbox-wrapper {
   margin-bottom: .5em;
+  width: 100%;
 }
 
 .room-context-label {
   margin-bottom: 1em;
 }
 
 .room-context-label,
 .room-context > .checkbox-wrapper > label {
@@ -1633,17 +1638,16 @@ html[dir="rtl"] .text-chat-entry.receive
 }
 
 .text-chat-entry.special.room-name p {
   background: #dbf7ff;
   max-width: 100%;
   /* 18px for indent of .text-chat-arrow, 1px for border of .text-chat-entry > p,
    0.5rem for padding of .text-chat-entry > p */
   padding: calc(18px - 1px - 0.5rem);
-  padding-bottom: 0px;
 }
 
 .text-chat-entry.special > p {
   border: none;
 }
 
 .text-chat-box {
   margin: auto;
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -457,32 +457,30 @@ loop.shared.actions = (function() {
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     SetupRoomInfo: Action.define("setupRoomInfo", {
       // roomContextUrls: Array - Optional.
       // roomDescription: String - Optional.
       // roomName: String - Optional.
-      roomOwner: String,
       roomToken: String,
       roomUrl: String,
       socialShareProviders: Array
     }),
 
     /**
      * Updates the room information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       // description: String - Optional.
       // roomName: String - Optional.
-      roomOwner: String,
       roomUrl: String
       // urls: Array - Optional.
       // See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
     }),
 
     /**
      * Updates the Social API information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -268,17 +268,16 @@ loop.store.ActiveRoomStore = (function()
             return;
           }
 
           this.dispatchAction(new sharedActions.SetupRoomInfo({
             roomToken: actionData.roomToken,
             roomContextUrls: roomData.decryptedContext.urls,
             roomDescription: roomData.decryptedContext.description,
             roomName: roomData.decryptedContext.roomName,
-            roomOwner: roomData.roomOwner,
             roomUrl: roomData.roomUrl,
             socialShareProviders: this._mozLoop.getSocialShareProviders()
           }));
 
           // For the conversation window, we need to automatically
           // join the room.
           this.dispatchAction(new sharedActions.JoinRoom());
         }.bind(this));
@@ -320,17 +319,16 @@ loop.store.ActiveRoomStore = (function()
           this.dispatchAction(new sharedActions.RoomFailure({
             error: err,
             failedJoinRequest: false
           }));
           return;
         }
 
         var roomInfoData = new sharedActions.UpdateRoomInfo({
-          roomOwner: result.roomOwner,
           roomUrl: result.roomUrl
         });
 
         // If we've got this far, then we want to go to the ready state
         // regardless of success of failure. This is because failures of
         // crypto don't stop the user using the room, they just stop
         // us putting up the information.
         roomInfoData.roomState = ROOM_STATES.READY;
@@ -391,17 +389,16 @@ loop.store.ActiveRoomStore = (function()
         console.error("Room info already set up!");
         return;
       }
 
       this.setStoreState({
         roomContextUrls: actionData.roomContextUrls,
         roomDescription: actionData.roomDescription,
         roomName: actionData.roomName,
-        roomOwner: actionData.roomOwner,
         roomState: ROOM_STATES.READY,
         roomToken: actionData.roomToken,
         roomUrl: actionData.roomUrl,
         socialShareProviders: actionData.socialShareProviders
       });
 
       this._onUpdateListener = this._handleRoomUpdate.bind(this);
       this._onDeleteListener = this._handleRoomDelete.bind(this);
@@ -415,17 +412,16 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       var newState = {
-        roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
       };
       // Iterate over the optional fields that _may_ be present on the actionData
       // object.
       Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) {
         if (actionData[field]) {
           newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field];
         }
@@ -451,17 +447,16 @@ loop.store.ActiveRoomStore = (function()
      * @param {String} eventName The name of the event
      * @param {Object} roomData  The new roomData.
      */
     _handleRoomUpdate: function(eventName, roomData) {
       this.dispatchAction(new sharedActions.UpdateRoomInfo({
         urls: roomData.decryptedContext.urls,
         description: roomData.decryptedContext.description,
         roomName: roomData.decryptedContext.roomName,
-        roomOwner: roomData.roomOwner,
         roomUrl: roomData.roomUrl
       }));
     },
 
     /**
      * Handles the deletion of a room, notified by the mozLoop rooms API.
      *
      * @param {String} eventName The name of the event
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -373,32 +373,37 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     render: function() {
       var messageList;
+      var showingRoomName = false;
 
       if (this.props.showRoomName) {
         messageList = this.state.messageList;
+        showingRoomName = this.state.messageList.some(function(item) {
+          return item.contentType === CHAT_CONTENT_TYPES.ROOM_NAME;
+        });
       } else {
         messageList = this.state.messageList.filter(function(item) {
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
       var textChatViewClasses = React.addons.classSet({
+        "showing-room-name": showingRoomName,
         "text-chat-view": true,
         "text-chat-disabled": !this.state.textChatEnabled,
         "text-chat-entries-empty": !messageList.length
       });
 
       return (
         React.createElement("div", {className: textChatViewClasses}, 
           React.createElement(TextChatEntriesView, {
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -373,32 +373,37 @@ loop.shared.views.chat = (function(mozL1
     },
 
     getInitialState: function() {
       return this.getStoreState();
     },
 
     render: function() {
       var messageList;
+      var showingRoomName = false;
 
       if (this.props.showRoomName) {
         messageList = this.state.messageList;
+        showingRoomName = this.state.messageList.some(function(item) {
+          return item.contentType === CHAT_CONTENT_TYPES.ROOM_NAME;
+        });
       } else {
         messageList = this.state.messageList.filter(function(item) {
           return item.type !== CHAT_MESSAGE_TYPES.SPECIAL ||
             item.contentType !== CHAT_CONTENT_TYPES.ROOM_NAME;
         });
       }
 
       // Only show the placeholder if we've sent messages.
       var hasSentMessages = messageList.some(function(item) {
         return item.type === CHAT_MESSAGE_TYPES.SENT;
       });
 
       var textChatViewClasses = React.addons.classSet({
+        "showing-room-name": showingRoomName,
         "text-chat-view": true,
         "text-chat-disabled": !this.state.textChatEnabled,
         "text-chat-entries-empty": !messageList.length
       });
 
       return (
         <div className={textChatViewClasses}>
           <TextChatEntriesView
--- a/browser/components/loop/content/shared/js/views.js
+++ b/browser/components/loop/content/shared/js/views.js
@@ -852,27 +852,31 @@ loop.shared.views = (function(_, mozL10n
 
   var Checkbox = React.createClass({displayName: "Checkbox",
     propTypes: {
       additionalClass: React.PropTypes.string,
       checked: React.PropTypes.bool,
       disabled: React.PropTypes.bool,
       label: React.PropTypes.string,
       onChange: React.PropTypes.func.isRequired,
+      // If true, this will cause the label to be cut off at the end of the
+      // first line with an ellipsis, and a tooltip supplied.
+      useEllipsis: React.PropTypes.bool,
       // If `value` is not supplied, the consumer should rely on the boolean
       // `checked` state changes.
       value: React.PropTypes.string
     },
 
     getDefaultProps: function() {
       return {
         additionalClass: "",
         checked: false,
         disabled: false,
         label: null,
+        useEllipsis: false,
         value: ""
       };
     },
 
     componentWillReceiveProps: function(nextProps) {
       // Only change the state if the prop has changed, and if it is also
       // different from the state.
       if (this.props.checked !== nextProps.checked &&
@@ -905,27 +909,36 @@ loop.shared.views = (function(_, mozL10n
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
       };
+      var labelClasses = {
+        "checkbox-label": true,
+        "ellipsis": this.props.useEllipsis
+      };
+
       if (this.props.additionalClass) {
         wrapperClasses[this.props.additionalClass] = true;
       }
       return (
         React.createElement("div", {className: cx(wrapperClasses), 
              disabled: this.props.disabled, 
              onClick: this._handleClick}, 
           React.createElement("div", {className: cx(checkClasses)}), 
-          this.props.label ?
-            React.createElement("label", null, this.props.label) :
-            null
+          
+            this.props.label ?
+              React.createElement("div", {className: cx(labelClasses), 
+                   title: this.props.useEllipsis ? this.props.label : ""}, 
+                this.props.label
+              ) : null
+          
         )
       );
     }
   });
 
   /**
    * Renders an avatar element for display when video is muted.
    */
--- a/browser/components/loop/content/shared/js/views.jsx
+++ b/browser/components/loop/content/shared/js/views.jsx
@@ -852,27 +852,31 @@ loop.shared.views = (function(_, mozL10n
 
   var Checkbox = React.createClass({
     propTypes: {
       additionalClass: React.PropTypes.string,
       checked: React.PropTypes.bool,
       disabled: React.PropTypes.bool,
       label: React.PropTypes.string,
       onChange: React.PropTypes.func.isRequired,
+      // If true, this will cause the label to be cut off at the end of the
+      // first line with an ellipsis, and a tooltip supplied.
+      useEllipsis: React.PropTypes.bool,
       // If `value` is not supplied, the consumer should rely on the boolean
       // `checked` state changes.
       value: React.PropTypes.string
     },
 
     getDefaultProps: function() {
       return {
         additionalClass: "",
         checked: false,
         disabled: false,
         label: null,
+        useEllipsis: false,
         value: ""
       };
     },
 
     componentWillReceiveProps: function(nextProps) {
       // Only change the state if the prop has changed, and if it is also
       // different from the state.
       if (this.props.checked !== nextProps.checked &&
@@ -905,27 +909,36 @@ loop.shared.views = (function(_, mozL10n
         "checkbox-wrapper": true,
         disabled: this.props.disabled
       };
       var checkClasses = {
         checkbox: true,
         checked: this.state.checked,
         disabled: this.props.disabled
       };
+      var labelClasses = {
+        "checkbox-label": true,
+        "ellipsis": this.props.useEllipsis
+      };
+
       if (this.props.additionalClass) {
         wrapperClasses[this.props.additionalClass] = true;
       }
       return (
         <div className={cx(wrapperClasses)}
              disabled={this.props.disabled}
              onClick={this._handleClick}>
           <div className={cx(checkClasses)} />
-          {this.props.label ?
-            <label>{this.props.label}</label> :
-            null}
+          {
+            this.props.label ?
+              <div className={cx(labelClasses)}
+                   title={this.props.useEllipsis ? this.props.label : ""}>
+                {this.props.label}
+              </div> : null
+          }
         </div>
       );
     }
   });
 
   /**
    * Renders an avatar element for display when video is muted.
    */
--- a/browser/components/loop/standalone/content/js/standaloneMozLoop.js
+++ b/browser/components/loop/standalone/content/js/standaloneMozLoop.js
@@ -86,17 +86,16 @@ loop.StandaloneMozLoop = (function(mozL1
           }
         }.bind(this)
       });
 
       req.done(function(responseData) {
         try {
           // We currently only require things we need rather than everything possible.
           callback(null, validate(responseData, {
-            roomOwner: String,
             roomUrl: String
           }));
         } catch (err) {
           console.error("Error requesting call info", err.message);
           callback(err);
         }
       }.bind(this));
 
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -305,17 +305,16 @@ describe("loop.store.ActiveRoomStore", f
     var fakeToken, fakeRoomData;
 
     beforeEach(function() {
       fakeToken = "337-ff-54";
       fakeRoomData = {
         decryptedContext: {
           roomName: "Monkeys"
         },
-        roomOwner: "Alfred",
         roomUrl: "http://invalid"
       };
 
       store = new loop.store.ActiveRoomStore(dispatcher, {
         mozLoop: fakeMozLoop,
         sdkDriver: {}
       });
       fakeMozLoop.rooms.get.withArgs(fakeToken).callsArgOnWith(
@@ -348,17 +347,16 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledTwice(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.SetupRoomInfo({
             roomContextUrls: undefined,
             roomDescription: undefined,
             roomToken: fakeToken,
             roomName: fakeRoomData.decryptedContext.roomName,
-            roomOwner: fakeRoomData.roomOwner,
             roomUrl: fakeRoomData.roomUrl,
             socialShareProviders: []
           }));
       });
 
     it("should dispatch a JoinRoom action if the get is successful",
       function() {
         store.setupWindowData(new sharedActions.SetupWindowData({
@@ -421,38 +419,35 @@ describe("loop.store.ActiveRoomStore", f
     it("should call mozLoop.rooms.get to get the room data", function() {
       store.fetchServerData(fetchServerAction);
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.get);
     });
 
     it("should dispatch an UpdateRoomInfo message with 'no data' failure if neither roomName nor context are supplied", function() {
       fakeMozLoop.rooms.get.callsArgWith(1, null, {
-        roomOwner: "Dan",
         roomUrl: "http://invalid"
       });
 
       store.fetchServerData(fetchServerAction);
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.UpdateRoomInfo({
           roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA,
-          roomOwner: "Dan",
           roomState: ROOM_STATES.READY,
           roomUrl: "http://invalid"
         }));
     });
 
     describe("mozLoop.rooms.get returns roomName as a separate field (no context)", function() {
       it("should dispatch UpdateRoomInfo if mozLoop.rooms.get is successful", function() {
         var roomDetails = {
           roomName: "fakeName",
-          roomUrl: "http://invalid",
-          roomOwner: "gavin"
+          roomUrl: "http://invalid"
         };
 
         fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
 
         store.fetchServerData(fetchServerAction);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
@@ -465,22 +460,20 @@ describe("loop.store.ActiveRoomStore", f
     describe("mozLoop.rooms.get returns encryptedContext", function() {
       var roomDetails, expectedDetails;
 
       beforeEach(function() {
         roomDetails = {
           context: {
             value: "fakeContext"
           },
-          roomUrl: "http://invalid",
-          roomOwner: "Mark"
+          roomUrl: "http://invalid"
         };
         expectedDetails = {
-          roomUrl: "http://invalid",
-          roomOwner: "Mark"
+          roomUrl: "http://invalid"
         };
 
         fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails);
 
         sandbox.stub(loop.crypto, "isSupported").returns(true);
       });
 
       it("should dispatch UpdateRoomInfo message with 'unsupported' failure if WebCrypto is unsupported", function() {
@@ -592,17 +585,16 @@ describe("loop.store.ActiveRoomStore", f
   });
 
   describe("#setupRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
-        roomOwner: "Me",
         roomToken: "fakeToken",
         roomUrl: "http://invalid",
         socialShareProviders: []
       };
     });
 
     it("should set the state to READY", function() {
       store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
@@ -610,45 +602,42 @@ describe("loop.store.ActiveRoomStore", f
       expect(store._storeState.roomState).eql(ROOM_STATES.READY);
     });
 
     it("should save the room information", function() {
       store.setupRoomInfo(new sharedActions.SetupRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
-      expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
       expect(state.roomToken).eql(fakeRoomInfo.roomToken);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
       expect(state.socialShareProviders).eql([]);
     });
   });
 
   describe("#updateRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
-        roomOwner: "Me",
         roomUrl: "http://invalid",
         urls: [{
           description: "fake site",
           location: "http://invalid.com",
           thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
         }]
       };
     });
 
     it("should save the room information", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
-      expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
       expect(state.roomContextUrls).eql(fakeRoomInfo.urls);
     });
   });
 
   describe("#updateSocialShareInfo", function() {
     var fakeSocialShareInfo;
 
@@ -1498,17 +1487,16 @@ describe("loop.store.ActiveRoomStore", f
     });
   });
 
   describe("Events", function() {
     describe("update:{roomToken}", function() {
       beforeEach(function() {
         store.setupRoomInfo(new sharedActions.SetupRoomInfo({
           roomName: "Its a room",
-          roomOwner: "Me",
           roomToken: "fakeToken",
           roomUrl: "http://invalid",
           socialShareProviders: []
         }));
       });
 
       it("should dispatch an UpdateRoomInfo action", function() {
         sinon.assert.calledTwice(fakeMozLoop.rooms.on);
@@ -1516,60 +1504,56 @@ describe("loop.store.ActiveRoomStore", f
         var fakeRoomData = {
           decryptedContext: {
             description: "fakeDescription",
             roomName: "fakeName",
             urls: {
               fake: "url"
             }
           },
-          roomOwner: "you",
           roomUrl: "original"
         };
 
         fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo({
             description: "fakeDescription",
             roomName: fakeRoomData.decryptedContext.roomName,
-            roomOwner: fakeRoomData.roomOwner,
             roomUrl: fakeRoomData.roomUrl,
             urls: {
               fake: "url"
             }
           }));
       });
 
       it("should call close window", function() {
         var fakeRoomData = {
           decryptedContext: {
             description: "fakeDescription",
             roomName: "fakeName",
             urls: {
               fake: "url"
             }
           },
-          roomOwner: "you",
           roomUrl: "original"
         };
 
         fakeMozLoop.rooms.on.callArgWith(1, "update", fakeRoomData);
 
         sinon.assert.calledOnce(window.close);
       });
     });
 
     describe("delete:{roomToken}", function() {
       var fakeRoomData = {
         decryptedContext: {
           roomName: "Its a room"
         },
-        roomOwner: "Me",
         roomToken: "fakeToken",
         roomUrl: "http://invalid"
       };
 
       beforeEach(function() {
         store.setupRoomInfo(new sharedActions.SetupRoomInfo(
           _.extend(fakeRoomData, {
             socialShareProviders: []
--- a/browser/components/loop/test/shared/textChatStore_test.js
+++ b/browser/components/loop/test/shared/textChatStore_test.js
@@ -151,34 +151,32 @@ describe("loop.store.TextChatStore", fun
         new CustomEvent("LoopChatMessageAppended"));
     });
   });
 
   describe("#updateRoomInfo", function() {
     it("should add the room name to the list", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
-        roomOwner: "Mark",
         roomUrl: "fake"
       }));
 
       expect(store.getStoreState("messageList")).eql([{
         type: CHAT_MESSAGE_TYPES.SPECIAL,
         contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
         message: "Let's share!",
         extraData: undefined,
         sentTimestamp: undefined,
         receivedTimestamp: undefined
       }]);
     });
 
     it("should add the context to the list", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
-        roomOwner: "Mark",
         roomUrl: "fake",
         urls: [{
           description: "A wonderful event",
           location: "http://wonderful.invalid",
           thumbnail: "fake"
         }]
       }));
 
@@ -201,17 +199,16 @@ describe("loop.store.TextChatStore", fun
             thumbnail: "fake"
           }
         }
       ]);
     });
 
     it("should not add more than one context message", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
-        roomOwner: "Mark",
         roomUrl: "fake",
         urls: [{
           description: "A wonderful event",
           location: "http://wonderful.invalid",
           thumbnail: "fake"
         }]
       }));
 
@@ -223,17 +220,16 @@ describe("loop.store.TextChatStore", fun
         receivedTimestamp: undefined,
         extraData: {
           location: "http://wonderful.invalid",
           thumbnail: "fake"
         }
       }]);
 
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
-        roomOwner: "Mark",
         roomUrl: "fake",
         urls: [{
           description: "A wonderful event2",
           location: "http://wonderful.invalid2",
           thumbnail: "fake2"
         }]
       }));
 
@@ -248,16 +244,15 @@ describe("loop.store.TextChatStore", fun
           thumbnail: "fake2"
         }
       }]);
     });
 
     it("should not dispatch a LoopChatMessageAppended event", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "Let's share!",
-        roomOwner: "Mark",
         roomUrl: "fake"
       }));
 
       sinon.assert.notCalled(window.dispatchEvent);
     });
   });
 });
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -448,16 +448,50 @@ describe("loop.shared.views.TextChatView
         message: "Hello!",
         sentTimestamp: "1970-01-01T00:02:00.000Z",
         receivedTimestamp: "1970-01-01T00:02:00.000Z"
       });
 
       expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
     });
 
+    it("should add a showing room name class when the view shows room names and it has a room name", function() {
+      view = mountTestComponent({
+        showRoomName: true
+      });
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "Study",
+        roomUrl: "Fake"
+      }));
+
+      expect(view.getDOMNode().classList.contains("showing-room-name")).eql(true);
+    });
+
+    it("shouldn't add a showing room name class when the view doesn't show room names", function() {
+      view = mountTestComponent({
+        showRoomName: false
+      });
+
+      store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
+        roomName: "Study",
+        roomUrl: "Fake"
+      }));
+
+      expect(view.getDOMNode().classList.contains("showing-room-name")).eql(false);
+    });
+
+    it("shouldn't add a showing room name class when the view doesn't have a name", function() {
+      view = mountTestComponent({
+        showRoomName: true
+      });
+
+      expect(view.getDOMNode().classList.contains("showing-room-name")).eql(false);
+    });
+
     it("should show timestamps from msgs sent more than 1 min apart", function() {
       view = mountTestComponent();
 
       store.sendTextChatMessage({
         contentType: CHAT_CONTENT_TYPES.TEXT,
         message: "Hello!",
         sentTimestamp: "1970-01-01T00:02:00.000Z",
         receivedTimestamp: "1970-01-01T00:02:00.000Z"
@@ -535,17 +569,16 @@ describe("loop.shared.views.TextChatView
 
     it("should render a room name special entry", function() {
       view = mountTestComponent({
         showRoomName: true
       });
 
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "A wonderful surprise!",
-        roomOwner: "Chris",
         roomUrl: "Fake"
       }));
 
       var node = view.getDOMNode();
       expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
 
       var entries = node.querySelectorAll(".text-chat-entry");
       expect(entries.length).eql(1);
@@ -553,17 +586,16 @@ describe("loop.shared.views.TextChatView
       expect(entries[0].classList.contains("room-name")).eql(true);
     });
 
     it("should render a special entry for the context url", function() {
       view = mountTestComponent();
 
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "A Very Long Conversation Name",
-        roomOwner: "fake",
         roomUrl: "http://showcase",
         urls: [{
           description: "A wonderful page!",
           location: "http://wonderful.invalid"
           // use the fallback thumbnail
         }]
       }));
 
--- a/browser/components/loop/test/shared/views_test.js
+++ b/browser/components/loop/test/shared/views_test.js
@@ -936,17 +936,17 @@ describe("loop.shared.views", function()
         expect(node.hasAttribute("disabled")).to.eql(false);
         expect(node.childNodes.length).to.eql(1);
       });
 
       it("should render a label when it's supplied", function() {
         view = mountTestComponent({ label: "Some label" });
 
         var node = view.getDOMNode();
-        expect(node.lastChild.localName).to.eql("label");
+        expect(node.lastChild.localName).to.eql("div");
         expect(node.lastChild.textContent).to.eql("Some label");
       });
 
       it("should render the checkbox as disabled when told to", function() {
         view = mountTestComponent({
           disabled: true
         });
 
@@ -969,16 +969,36 @@ describe("loop.shared.views", function()
           checked: true
         });
 
         view.setProps({checked: false});
 
         var checkbox = view.getDOMNode().querySelector(".checkbox");
         expect(checkbox.classList.contains("checked")).eql(false);
       });
+
+      it("should add an ellipsis class when the prop is set", function() {
+        view = mountTestComponent({
+          label: "Some label",
+          useEllipsis: true
+        });
+
+        var label = view.getDOMNode().querySelector(".checkbox-label");
+        expect(label.classList.contains("ellipsis")).eql(true);
+      });
+
+      it("should not add an ellipsis class when the prop is not set", function() {
+        view = mountTestComponent({
+          label: "Some label",
+          useEllipsis: false
+        });
+
+        var label = view.getDOMNode().querySelector(".checkbox-label");
+        expect(label.classList.contains("ellipsis")).eql(false);
+      });
     });
 
     describe("#_handleClick", function() {
       var onChange;
 
       beforeEach(function() {
         onChange = sinon.stub();
       });
--- a/browser/components/loop/test/standalone/standaloneMozLoop_test.js
+++ b/browser/components/loop/test/standalone/standaloneMozLoop_test.js
@@ -78,18 +78,17 @@ describe("loop.StandaloneMozLoop", funct
       expect(requests[0].method).eql("GET");
     });
 
     it("should call the callback with success parameters", function() {
       mozLoop.rooms.get("fakeToken", callback);
 
       var roomDetails = {
         roomName: "fakeName",
-        roomUrl: "http://invalid",
-        roomOwner: "gavin"
+        roomUrl: "http://invalid"
       };
 
       requests[0].respond(200, {"Content-Type": "application/json"},
         JSON.stringify(roomDetails));
 
       sinon.assert.calledOnce(callback);
       sinon.assert.calledWithExactly(callback, null, roomDetails);
     });
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -10,43 +10,40 @@ var fakeRooms = [
       "roomName": "First Room Name",
       "urls": [{
         description: "The mozilla page",
         location: "https://www.mozilla.org",
         thumbnail: "https://www.mozilla.org/favicon.ico"
       }]
     },
     "roomUrl": "http://localhost:3000/rooms/_nxD4V4FflQ",
-    "roomOwner": "Alexis",
     "maxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517546,
     "expiresAt": 1405534180,
     "participants": []
   },
   {
     "roomToken": "QzBbvGmIZWU",
     "decryptedContext": {
       "roomName": "Second Room Name"
     },
     "roomUrl": "http://localhost:3000/rooms/QzBbvGmIZWU",
-    "roomOwner": "Alexis",
     "maxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517546,
     "expiresAt": 1405534180,
     "participants": []
   },
   {
     "roomToken": "3jKS_Els9IU",
     "decryptedContext": {
       "roomName": "UX Discussion"
     },
     "roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU",
-    "roomOwner": "Alexis",
     "maxSize": 2,
     "clientMaxSize": 2,
     "creationTime": 1405517546,
     "ctime": 1405517818,
     "expiresAt": 1405534180,
     "participants": [
        { "displayName": "Alexis", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
        { "displayName": "Adam", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -380,17 +380,16 @@
   var conversationStores = [];
   for (var index = 0; index < 5; index++) {
     conversationStores[index] = makeConversationStore();
   }
 
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
-    roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -380,17 +380,16 @@
   var conversationStores = [];
   for (var index = 0; index < 5; index++) {
     conversationStores[index] = makeConversationStore();
   }
 
   // Update the text chat store with the room info.
   textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
     roomName: "A Very Long Conversation Name",
-    roomOwner: "fake",
     roomUrl: "http://showcase",
     urls: [{
       description: "A wonderful page!",
       location: "http://wonderful.invalid"
       // use the fallback thumbnail
     }]
   }));
 
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -22,17 +22,16 @@ support-files =
 [browser_555547.js]
 [browser_bookmarklet_windowOpen.js]
 support-files =
   pageopeningwindow.html
 [browser_bookmarkProperties_addFolderDefaultButton.js]
 [browser_bookmarkProperties_addKeywordForThisSearch.js]
 [browser_bookmarkProperties_readOnlyRoot.js]
 [browser_bookmarksProperties.js]
-skip-if = (os == 'win' && os_version == "6.2") # Bug 1178709
 [browser_drag_bookmarks_on_toolbar.js]
 skip-if = e10s # Bug ?????? - test fails - "Number of dragged items should be the same. - Got 0, expected 1"
 [browser_forgetthissite_single.js]
 [browser_history_sidebar_search.js]
 skip-if = e10s && (os == 'linux' || os == 'mac') # Bug 1116457
 [browser_library_batch_delete.js]
 [browser_library_commands.js]
 [browser_library_downloads.js]
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -399,31 +399,30 @@ function open_properties_dialog() {
     ok(tree.selectedNode,
        "We have a places node selected: " + tree.selectedNode.title);
 
     // Wait for the Properties dialog.
     function windowObserver(aSubject, aTopic, aData) {
       if (aTopic != "domwindowopened")
         return;
       ww.unregisterNotification(windowObserver);
-      var win = aSubject.QueryInterface(Ci.nsIDOMWindow);
-      win.addEventListener("focus", function (event) {
-        win.removeEventListener("focus", arguments.callee, false);
+      let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+      waitForFocus(() => {
         // Windows has been loaded, execute our test now.
         executeSoon(function () {
           // Ensure overlay is loaded
           ok(win.gEditItemOverlay.initialized, "EditItemOverlay is initialized");
           gCurrentTest.window = win;
           try {
             gCurrentTest.run();
           } catch (ex) {
             ok(false, "An error occured during test run: " + ex.message);
           }
         });
-      }, false);
+      }, win);
     }
     ww.registerNotification(windowObserver);
 
     var command = null;
     switch (gCurrentTest.action) {
       case ACTION_EDIT:
         command = "placesCmd_show:info";
         break;
--- a/browser/components/preferences/in-content/sync.js
+++ b/browser/components/preferences/in-content/sync.js
@@ -255,17 +255,17 @@ let gSyncPane = {
     setEventListener("noFxaSignUp", "command", function () {
       gSyncPane.signUp();
       return false;
     });
     setEventListener("noFxaSignIn", "command", function () {
       gSyncPane.signIn();
       return false;
     });
-    setEventListener("verifiedManage", "command",
+    setEventListener("verifiedManage", "click",
       gSyncPane.manageFirefoxAccount);
     setEventListener("fxaUnlinkButton", "click", function () {
       gSyncPane.unlinkFirefoxAccount(true);
     });
     setEventListener("verifyFxaAccount", "command",
       gSyncPane.verifyFirefoxAccount);
     setEventListener("unverifiedUnlinkFxaAccount", "command", function () {
       /* no warning as account can't have previously synced */
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.css
@@ -31,19 +31,19 @@ body[globalTpEnabled] .showGlobalTpDisab
 
 #bar {
   align-self: stretch;
   background: url("chrome://browser/skin/privatebrowsing/mask.svg") no-repeat 22px 50%,
               #8d20ae;
   background-size: 47px 26px;
   padding-inline-start: 87px;
   color: white;
-  font-size: 24pt;
+  font-size: 1.5em;
   font-weight: 200;
-  line-height: 60pt;
+  line-height: 2.5em;
 }
 
 #main {
   padding: 0 2em;
   flex: 1;
   display: flex;
   flex-flow: row wrap;
   align-items: center;
@@ -57,17 +57,17 @@ body[globalTpEnabled] .showGlobalTpDisab
 /* PRIVATE BROWSING SECTION */
 
 #privateBrowsingSection {
   margin: 1em;
   padding: 0 1em;
 }
 
 ul {
-  margin-bottom: 0;
+  margin: 0;
   padding-inline-start: 8px;
 }
 
 li {
   list-style: none;
   padding-inline-start: 24px;
   background-size: 16px 16px;
   background-repeat: no-repeat;
@@ -95,20 +95,21 @@ li {
 #list-area {
   display: flex;
   flex-direction: row;
   justify-content: flex-start;
   align-items: flex-start;
 }
 
 #list-area > div {
-  margin-inline-end: 1em;
+  margin-inline-end: 3em;
 }
 
 .list-header {
+  margin-bottom: 0.4em;
   font-weight: bold;
 }
 
 /* TRACKING PROTECTION SECTION */
 
 #trackingProtectionSection {
   margin: 1em;
   padding: 1em;
@@ -151,18 +152,17 @@ li {
   }
 }
 
 #tpStartTour {
   margin-bottom: 0;
 }
 
 #startTour {
-  display: inline-block;
-  width: 16em;
+  display: block;
   border-radius: 2px;
   background-color: var(--in-content-primary-button-background);
   color: var(--in-content-selected-text);
   padding: 0.1em 0.3em;
   line-height: 2.25em;
   text-decoration: none;
 }
 
--- a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
+++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.xhtml
@@ -29,17 +29,17 @@
     <button xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             id="startPrivateBrowsing"
             class="showNormal"
             label="&privatebrowsingpage.openPrivateWindow.label;"
             accesskey="&privatebrowsingpage.openPrivateWindow.accesskey;"/>
     <div id="bar" class="showPrivate">&privateBrowsing.title;</div>
     <div id="main" class="showPrivate">
       <div id="privateBrowsingSection"
-           style="width: &aboutPrivateBrowsing.width;">
+           style="width: &aboutPrivateBrowsing.width1;">
         <div class="sectionHeader">&aboutPrivateBrowsing.title;</div>
         <p>&aboutPrivateBrowsing.subtitle;</p>
         <div id="list-area">
           <div>
             <div class="list-header">&aboutPrivateBrowsing.info.forgotten;</div>
             <ul id="forgotten">
               <li>&aboutPrivateBrowsing.info.history;</li>
               <li>&aboutPrivateBrowsing.info.searches;</li>
@@ -54,17 +54,17 @@
               <li>&aboutPrivateBrowsing.info.bookmarks;</li>
             </ul>
           </div>
         </div>
         <p>&aboutPrivateBrowsing.note1;</p>
         <a id="learnMore" target="_blank">&aboutPrivateBrowsing.learnMore;</a>
       </div>
       <div id="trackingProtectionSection"
-           style="width: &trackingProtection.width;">
+           style="width: &trackingProtection.width1;">
         <div class="sectionHeader">&trackingProtection.title;
           <span id="tpEnabled"
                 style="width: &trackingProtection.state.width;"
                 class="showTpEnabled">&trackingProtection.state.enabled;</span>
           <span id="tpDisabled"
                 style="width: &trackingProtection.state.width;"
                 class="showTpDisabled">&trackingProtection.state.disabled;</span>
         </div>
--- a/browser/devtools/inspector/breadcrumbs.js
+++ b/browser/devtools/inspector/breadcrumbs.js
@@ -232,17 +232,17 @@ HTMLBreadcrumbs.prototype = {
    * Open the sibling menu.
    * @param {DOMNode} button the button representing the node.
    * @param {NodeFront} node the node we want the siblings from.
    */
   openSiblingMenu: function(button, node) {
     // We make sure that the targeted node is selected
     // because we want to use the nodemenu that only works
     // for inspector.selection
-    this.selection.setNodeFront(node, "breadcrumbs");
+    this.navigateTo(node);
 
     // Build a list of extra menu items that will be appended at the end of the
     // inspector node context menu.
     let items = [this.chromeDoc.createElement("menuseparator")];
 
     this.walker.siblings(node, {
       whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
     }).then(siblings => {
@@ -257,20 +257,20 @@ HTMLBreadcrumbs.prototype = {
         if (nodes[i] === node) {
           item.setAttribute("disabled", "true");
           item.setAttribute("checked", "true");
         }
 
         item.setAttribute("type", "radio");
         item.setAttribute("label", this.prettyPrintNodeAsText(nodes[i]));
 
-        let selection = this.selection;
+        let self = this;
         item.onmouseup = (function(node) {
           return function() {
-            selection.setNodeFront(node, "breadcrumbs");
+            self.navigateTo(node);
           };
         })(nodes[i]);
 
         items.push(item);
       }
 
       // Append the items to the inspector node context menu and show the menu.
       this.inspector.showNodeMenu(button, "before_start", items);
@@ -348,49 +348,47 @@ HTMLBreadcrumbs.prototype = {
     this.inspector.toolbox.highlighterUtils.unhighlight();
   },
 
   /**
    * On key press, navigate the node hierarchy.
    * @param {DOMEvent} event.
    */
   handleKeyPress: function(event) {
-    let node = null;
-    this._keyPromise = this._keyPromise || promise.resolve(null);
+    let navigate = promise.resolve(null);
 
     this._keyPromise = (this._keyPromise || promise.resolve(null)).then(() => {
       switch (event.keyCode) {
         case this.chromeWin.KeyEvent.DOM_VK_LEFT:
           if (this.currentIndex != 0) {
-            node = promise.resolve(this.nodeHierarchy[this.currentIndex - 1].node);
+            navigate = promise.resolve(
+              this.nodeHierarchy[this.currentIndex - 1].node);
           }
           break;
         case this.chromeWin.KeyEvent.DOM_VK_RIGHT:
           if (this.currentIndex < this.nodeHierarchy.length - 1) {
-            node = promise.resolve(this.nodeHierarchy[this.currentIndex + 1].node);
+            navigate = promise.resolve(
+              this.nodeHierarchy[this.currentIndex + 1].node);
           }
           break;
         case this.chromeWin.KeyEvent.DOM_VK_UP:
-          node = this.walker.previousSibling(this.selection.nodeFront, {
+          navigate = this.walker.previousSibling(this.selection.nodeFront, {
             whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
           });
           break;
         case this.chromeWin.KeyEvent.DOM_VK_DOWN:
-          node = this.walker.nextSibling(this.selection.nodeFront, {
+          navigate = this.walker.nextSibling(this.selection.nodeFront, {
             whatToShow: Ci.nsIDOMNodeFilter.SHOW_ELEMENT
           });
           break;
       }
 
-      return node.then((node) => {
-        if (node) {
-          this.selection.setNodeFront(node, "breadcrumbs");
-        }
-      });
+      return navigate.then(node => this.navigateTo(node));
     });
+
     event.preventDefault();
     event.stopPropagation();
   },
 
   /**
    * Remove nodes and clean up.
    */
   destroy: function() {
@@ -465,16 +463,24 @@ HTMLBreadcrumbs.prototype = {
    */
   cutAfter: function(index) {
     while (this.nodeHierarchy.length > (index + 1)) {
       let toRemove = this.nodeHierarchy.pop();
       this.container.removeChild(toRemove.button);
     }
   },
 
+  navigateTo: function(node) {
+    if (node) {
+      this.selection.setNodeFront(node, "breadcrumbs");
+    } else {
+      this.inspector.emit("breadcrumbs-navigation-cancelled");
+    }
+  },
+
   /**
    * Build a button representing the node.
    * @param {NodeFront} node The node from the page.
    * @return {DOMNode} The <button> for this node.
    */
   buildButton: function(node) {
     let button = this.chromeDoc.createElement("button");
     button.appendChild(this.prettyPrintNodeAsXUL(node));
@@ -485,17 +491,17 @@ HTMLBreadcrumbs.prototype = {
     button.onkeypress = function onBreadcrumbsKeypress(e) {
       if (e.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
           e.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
         button.click();
       }
     };
 
     button.onBreadcrumbsClick = () => {
-      this.selection.setNodeFront(node, "breadcrumbs");
+      this.navigateTo(node);
     };
 
     button.onBreadcrumbsHover = () => {
       this.inspector.toolbox.highlighterUtils.highlightNodeFront(node);
     };
 
     button.onclick = (function _onBreadcrumbsRightClick(event) {
       button.focus();
--- a/browser/devtools/inspector/test/browser.ini
+++ b/browser/devtools/inspector/test/browser.ini
@@ -26,16 +26,17 @@ support-files =
   doc_inspector_search-reserved.html
   doc_inspector_search-suggestions.html
   doc_inspector_select-last-selected-01.html
   doc_inspector_select-last-selected-02.html
   head.js
 
 [browser_inspector_breadcrumbs.js]
 [browser_inspector_breadcrumbs_highlight_hover.js]
+[browser_inspector_breadcrumbs_keybinding.js]
 [browser_inspector_breadcrumbs_menu.js]
 [browser_inspector_breadcrumbs_mutations.js]
 [browser_inspector_delete-selected-node-01.js]
 [browser_inspector_delete-selected-node-02.js]
 [browser_inspector_delete-selected-node-03.js]
 [browser_inspector_destroy-after-navigation.js]
 [browser_inspector_destroy-before-ready.js]
 [browser_inspector_gcli-inspect-command.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/inspector/test/browser_inspector_breadcrumbs_keybinding.js
@@ -0,0 +1,109 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the breadcrumbs keybindings work.
+
+const TEST_URI = TEST_URL_ROOT + "doc_inspector_breadcrumbs.html";
+const TEST_DATA = [{
+  desc: "Pressing left should select the parent <body>",
+  key: "VK_LEFT",
+  newSelection: "body"
+}, {
+  desc: "Pressing left again should select the parent <html>",
+  key: "VK_LEFT",
+  newSelection: "html"
+}, {
+  desc: "Pressing left again should stay on root <html>",
+  key: "VK_LEFT",
+  newSelection: "html"
+}, {
+  desc: "Pressing right should go down to <body>",
+  key: "VK_RIGHT",
+  newSelection: "body"
+}, {
+  desc: "Pressing right again should go down to #i2",
+  key: "VK_RIGHT",
+  newSelection: "#i2"
+}, {
+  desc: "Continue down to #i21",
+  key: "VK_RIGHT",
+  newSelection: "#i21"
+}, {
+  desc: "Continue down to #i211",
+  key: "VK_RIGHT",
+  newSelection: "#i211"
+}, {
+  desc: "Continue down to #i2111",
+  key: "VK_RIGHT",
+  newSelection: "#i2111"
+}, {
+  desc: "Pressing right once more should stay at leaf node #i2111",
+  key: "VK_RIGHT",
+  newSelection: "#i2111"
+}, {
+  desc: "Go back to #i211",
+  key: "VK_LEFT",
+  newSelection: "#i211"
+}, {
+  desc: "Go back to #i21",
+  key: "VK_LEFT",
+  newSelection: "#i21"
+}, {
+  desc: "Pressing down should move to next sibling #i22",
+  key: "VK_DOWN",
+  newSelection: "#i22"
+}, {
+  desc: "Pressing up should move to previous sibling #i21",
+  key: "VK_UP",
+  newSelection: "#i21"
+}, {
+  desc: "Pressing up again should stay on #i21 as there's no previous sibling",
+  key: "VK_UP",
+  newSelection: "#i21"
+}, {
+  desc: "Going back down to #i22",
+  key: "VK_DOWN",
+  newSelection: "#i22"
+}, {
+  desc: "Pressing down again should stay on #i22 as there's no next sibling",
+  key: "VK_DOWN",
+  newSelection: "#i22"
+}];
+
+add_task(function*() {
+  let {inspector} = yield openInspectorForURL(TEST_URI);
+
+  info("Selecting the test node");
+  yield selectNode("#i2", inspector);
+
+  info("Clicking on the corresponding breadcrumbs node to focus it");
+  let container = inspector.panelDoc.getElementById("inspector-breadcrumbs");
+
+  let button = container.querySelector("button[checked]");
+  button.click();
+
+  let currentSelection = "#id2";
+  for (let {desc, key, newSelection} of TEST_DATA) {
+    info(desc);
+
+    let onUpdated;
+    if (newSelection !== currentSelection) {
+      info("Expecting a new node to be selected");
+      onUpdated = inspector.once("breadcrumbs-updated");
+    } else {
+      info("Expecting the same node to remain selected");
+      onUpdated = inspector.once("breadcrumbs-navigation-cancelled");
+    }
+
+    EventUtils.synthesizeKey(key, {});
+    yield onUpdated;
+
+    let newNodeFront = yield getNodeFront(newSelection, inspector);
+    is(newNodeFront, inspector.selection.nodeFront,
+       "The current selection is correct");
+
+    currentSelection = newSelection;
+  }
+});
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -1051,16 +1051,20 @@ let UI = {
 
     document.querySelector("#action-button-debug").setAttribute("active", "true");
 
     this.updateToolboxFullscreenState();
     return gDevTools.showToolbox(target, null, host, options);
   },
 
   updateToolboxFullscreenState: function() {
+    if (projectList.sidebarsEnabled) {
+      return;
+    }
+
     let panel = document.querySelector("#deck").selectedPanel;
     let nbox = document.querySelector("#notificationbox");
     if (panel && panel.id == "deck-panel-details" &&
         AppManager.selectedProject &&
         AppManager.selectedProject.type != "packaged" &&
         this.toolboxIframe) {
       nbox.setAttribute("toolboxfullscreen", "true");
     } else {
--- a/browser/devtools/webide/test/sidebars/test_fullscreenToolbox.html
+++ b/browser/devtools/webide/test/sidebars/test_fullscreenToolbox.html
@@ -46,17 +46,17 @@
           yield waitForUpdate(win, "project");
 
           ok(win.UI.toolboxPromise, "Toolbox promise exists");
           yield win.UI.toolboxPromise;
 
           ok(win.UI.toolboxIframe, "Toolbox iframe exists");
 
           let nbox = win.document.querySelector("#notificationbox");
-          ok(nbox.hasAttribute("toolboxfullscreen"), "Toolbox is fullsreen");
+          ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
 
           win.Cmds.showRuntimeDetails();
 
           ok(!nbox.hasAttribute("toolboxfullscreen"), "Toolbox is not fullscreen");
 
           yield win.Cmds.disconnectRuntime();
 
           yield closeWebIDE(win);
--- a/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutPrivateBrowsing.dtd
@@ -3,20 +3,23 @@
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <!ENTITY aboutPrivateBrowsing.notPrivate       "You are currently not in a private window.">
 <!ENTITY privatebrowsingpage.openPrivateWindow.label "Open a Private Window">
 <!ENTITY privatebrowsingpage.openPrivateWindow.accesskey "P">
 
 <!ENTITY privateBrowsing.title                 "Private Browsing">
 
-<!-- LOCALIZATION NOTE (aboutPrivateBrowsing.width):
-     Width of the Private Browsing section.
+<!-- LOCALIZATION NOTE (aboutPrivateBrowsing.width1):
+     Width of the Private Browsing section. This should depend primarily on the
+     length of the headers and text, but should be roughly 1.5 times the width
+     of the Tracking Protection section, and in general not much larger than
+     30em to prevent the sections from wrapping on smaller window sizes.
      -->
-<!ENTITY aboutPrivateBrowsing.width            "25em">
+<!ENTITY aboutPrivateBrowsing.width1           "30em">
 
 <!-- LOCALIZATION NOTE (aboutPrivateBrowsing.subtitle,
      aboutPrivateBrowsing.info.forgotten, aboutPrivateBrowsing.info.kept):
      These strings will be replaced by aboutPrivateBrowsing.forgotten and
      aboutPrivateBrowsing.kept when the new visual design lands (bug 1192625).
      -->
 <!ENTITY aboutPrivateBrowsing.title            "You're browsing privately">
 <!ENTITY aboutPrivateBrowsing.subtitle         "In this window, &brandShortName; will not remember any history.">
@@ -31,21 +34,25 @@
 <!ENTITY aboutPrivateBrowsing.kept             "&brandShortName; will keep:">
 <!ENTITY aboutPrivateBrowsing.info.kept        "Kept">
 <!ENTITY aboutPrivateBrowsing.info.downloads   "Downloads">
 <!ENTITY aboutPrivateBrowsing.info.bookmarks   "Bookmarks">
 
 <!ENTITY aboutPrivateBrowsing.note1            "Please note that your employer or Internet service provider can still track the pages you visit.">
 <!ENTITY aboutPrivateBrowsing.learnMore        "Learn More.">
 
-<!-- LOCALIZATION NOTE (trackingProtection.width):
-     Width of the Tracking Protection section. This should be enough to
-     accommodate the title as well as the enabled or disabled indicator.
+<!-- LOCALIZATION NOTE (trackingProtection.width1):
+     Width of the Tracking Protection section. It is fine for the enabled or
+     disabled indicator or the words in the title to wrap to the next line, but
+     you can expand or reduce this section to fit better, as long as the width
+     of the Private Browsing section is roughly 1.5 times the width of this one.
+     Note that the required space may vary between platforms because fonts are
+     different, so testing on Windows, Mac, and Linux is encouraged.
      -->
-<!ENTITY trackingProtection.width              "22em">
+<!ENTITY trackingProtection.width1             "22em">
 <!ENTITY trackingProtection.title              "Tracking Protection">
 
 <!-- LOCALIZATION NOTE (trackingProtection.state.width):
      Width of the element representing the enabled or disabled indicator.
      -->
 <!ENTITY trackingProtection.state.width        "6ch">
 <!ENTITY trackingProtection.state.enabled      "ON">
 <!ENTITY trackingProtection.state.disabled     "OFF">
--- a/browser/themes/shared/devtools/netmonitor.css
+++ b/browser/themes/shared/devtools/netmonitor.css
@@ -541,16 +541,20 @@ box.requests-menu-status[code^="5"] {
   transition: transform 0.2s ease-out;
 }
 
 /* Security tabpanel */
 .security-info-section {
   -moz-padding-start: 1em;
 }
 
+.theme-dark #security-error-message {
+  color: var(--theme-selection-color);
+}
+
 #security-tabpanel {
   overflow: auto;
 }
 
 .security-warning-icon {
   background-image: url(alerticon-warning.png);
   background-size: 13px 12px;
   -moz-margin-start: 5px;
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -23,16 +23,19 @@
                                 var(--identity-box-border-color) 15%,
                                 var(--identity-box-border-color) 85%,
                                 transparent 85%);
   border-image-slice: 1;
   font-size: .9em;
   padding: 3px 5px;
   margin-inline-end: 4px;
   overflow: hidden;
+  /* The latter two properties have a transition to handle the delayed hiding of
+     the forward button when hovered. */
+  transition: background-color 150ms ease, padding-left, padding-right;
 }
 
 #identity-box:hover,
 #identity-box[open=true] {
   background-color: var(--identity-box-selected-background-color);
   border-image-source: none;
 }
 
@@ -52,27 +55,25 @@
   padding-inline-start: 10px;
   border-radius: 0;
 }
 
 @conditionalForwardWithUrlbar@ > #urlbar > #identity-box {
   border-radius: 0;
 }
 
-@conditionalForwardWithUrlbar@:not([switchingtabs]) > #urlbar > #identity-box {
-  transition: padding-left, padding-right;
-}
-
 @conditionalForwardWithUrlbar@ > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
   padding-inline-start: calc(var(--backbutton-urlbar-overlap) + 4px);
 }
 
 @conditionalForwardWithUrlbar@:hover:not([switchingtabs]) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
-  /* forward button hiding is delayed when hovered */
-  transition-delay: 100s;
+  /* Forward button hiding is delayed when hovered, so we should use the same
+     delay for the identity box. We handle both horizontal paddings (for LTR and
+     RTL), the latter two delays here are for padding-left and padding-right. */
+  transition-delay: 0s, 100s, 100s;
 }
 
 @conditionalForwardWithUrlbar@:not(:hover) > #forward-button[disabled] + #urlbar > #notification-popup-box[hidden] + #identity-box {
   /* when not hovered anymore, trigger a new non-delayed transition to react to the forward button hiding */
   padding-inline-start: calc(var(--backbutton-urlbar-overlap) + 4.01px);
 }
 
 /* TRACKING PROTECTION ICON */
--- a/browser/themes/shared/newtab/newTab.inc.css
+++ b/browser/themes/shared/newtab/newTab.inc.css
@@ -110,17 +110,17 @@
   text-decoration: none;
   transition-property: top, left, opacity, box-shadow, background-color;
 }
 
 .newtab-cell:not([ignorehover]) .newtab-control:hover ~ .newtab-link,
 .newtab-cell:not([ignorehover]) .newtab-link:hover,
 .newtab-site[dragged] {
   border: 2px solid white;
-  box-shadow: 0 0 6px 2px #4cb1ff;
+  box-shadow: 0 0 6px 1px #add6ff;
   margin: -2px;
 }
 
 .newtab-site[dragged] {
   transition-property: box-shadow, background-color;
   background-color: rgb(242,242,242);
 }
 
@@ -163,18 +163,18 @@
 .newtab-suggested[active] {
   background-color: rgba(51, 51, 51, 0.95);
   border: 0;
   color: white;
 }
 
 .newtab-site:hover .newtab-title {
   color: white;
-  background-color: black;
-  border: 1px solid black;
+  background-color: #333;
+  border: 1px solid #333;
   border-top: 1px solid white;
 }
 
 .newtab-site[pinned] .newtab-title {
   -moz-padding-start: 24px;
 }
 
 .newtab-site[pinned] .newtab-title::before {
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -433,19 +433,18 @@
 
 /* Background tab separators.
    Also show separators beside the selected tab when dragging it. */
 #tabbrowser-tabs[movingtab] > .tabbrowser-tab[beforeselected]:not([last-visible-tab])::after,
 .tabbrowser-tab:not([visuallyselected]):not([afterselected-visible]):not([afterhovered]):not([first-visible-tab]):not(:hover)::before,
 #tabbrowser-tabs:not([overflow]) > .tabbrowser-tab[last-visible-tab]:not([visuallyselected]):not([beforehovered]):not(:hover)::after {
   width: 1px;
   -moz-margin-start: -1px;
-  padding-top: calc(var(--tab-separator-margin) + 1px);
-  padding-bottom: var(--tab-separator-margin);
-  background-clip: content-box;
+  margin-top: calc(var(--tab-separator-margin) + 1px);
+  margin-bottom: var(--tab-separator-margin);
   background-color: currentColor;
   opacity: var(--tab-separator-opacity);
   content: "";
   display: -moz-box;
 }
 
 /* New tab button */
 
--- a/mobile/android/base/db/TabsProvider.java
+++ b/mobile/android/base/db/TabsProvider.java
@@ -3,41 +3,67 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.db;
 
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.db.BrowserContract.Clients;
 import org.mozilla.gecko.db.BrowserContract.Tabs;
 
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 
 public class TabsProvider extends SharedBrowserDatabaseProvider {
+    private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
+    private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
+    private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
+
     static final String TABLE_TABS = "tabs";
     static final String TABLE_CLIENTS = "clients";
 
     static final int TABS = 600;
     static final int TABS_ID = 601;
     static final int CLIENTS = 602;
     static final int CLIENTS_ID = 603;
     static final int CLIENTS_RECENCY = 604;
 
+    // Exclude clients that are more than three weeks old and also any duplicates that are older than one week old.
+    static final String EXCLUDE_STALE_CLIENTS_SUBQUERY =
+    "(SELECT " + Clients.GUID +
+    ", " + Clients.NAME +
+    ", " + Clients.LAST_MODIFIED +
+    ", " + Clients.DEVICE_TYPE +
+    "  FROM " + TABLE_CLIENTS +
+    "  WHERE " + Clients.LAST_MODIFIED + " > %1$s " +
+    " GROUP BY " + Clients.NAME +
+    " UNION ALL " +
+    " SELECT c." +  Clients.GUID + " AS " + Clients.GUID +
+    ", c." + Clients.NAME + " AS " + Clients.NAME +
+    ", c." + Clients.LAST_MODIFIED + " AS " + Clients.LAST_MODIFIED +
+    ", c." + Clients.DEVICE_TYPE + " AS " + Clients.DEVICE_TYPE +
+    " FROM " + TABLE_CLIENTS + " AS c " +
+    " JOIN (" +
+        " SELECT " + Clients.GUID +
+        ", " + "MAX( " + Clients.LAST_MODIFIED + ") AS " + Clients.LAST_MODIFIED +
+        " FROM " + TABLE_CLIENTS +
+        " WHERE (" + Clients.LAST_MODIFIED + " < %1$s" + " AND " + Clients.LAST_MODIFIED + " > %2$s) AND " +
+        Clients.NAME + " NOT IN " + "( SELECT " + Clients.NAME + " FROM " + TABLE_CLIENTS + " WHERE " + Clients.LAST_MODIFIED + " > %1$s)" +
+        " GROUP BY " + Clients.NAME +
+    ") AS c2" +
+    " ON c." + Clients.GUID + " = c2." + Clients.GUID + ")";
+
     static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC";
     static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC";
     static final String DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC";
 
     static final String INDEX_TABS_GUID = "tabs_guid_index";
     static final String INDEX_TABS_POSITION = "tabs_position_index";
     static final String INDEX_CLIENTS_GUID = "clients_guid_index";
 
@@ -290,18 +316,25 @@ public class TabsProvider extends Shared
             case CLIENTS_RECENCY:
                 trace("Query is on CLIENTS_RECENCY: " + uri);
                 if (TextUtils.isEmpty(sortOrder)) {
                     sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER;
                 } else {
                     debug("Using sort order " + sortOrder + ".");
                 }
 
+                final long oneWeekAgo = System.currentTimeMillis() - ONE_WEEK_IN_MILLISECONDS;
+                final long threeWeeksAgo = System.currentTimeMillis() - THREE_WEEKS_IN_MILLISECONDS;
+
+                final String excludeStaleClientsTable = String.format(EXCLUDE_STALE_CLIENTS_SUBQUERY, oneWeekAgo, threeWeeksAgo);
+
                 qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP);
-                qb.setTables(TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
+
+                // Use a subquery to quietly exclude stale duplicate client records.
+                qb.setTables(excludeStaleClientsTable + " AS " + TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
                         " ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) +
                         " = " + projectColumn(TABLE_TABS,Tabs.CLIENT_GUID) + ")");
                 groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID);
                 break;
 
             default:
                 throw new UnsupportedOperationException("Unknown query URI " + uri);
         }
rename from mobile/android/base/resources/color-large-v11/tab_item_title.xml
rename to mobile/android/base/resources/color/tab_item_title.xml
index 9dbbcad93a865121cd2baab63a6881eb83ebca8c..7aadf4c45b511ead05554de58347283b7ea8de16
GIT binary patch
literal 178
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh0wlLOK8*rWm7Xq+Ar*|V=eLS72MRa`h9;~y
zbYZ6Q<*3UygIJektduc+@xtH2yz)@pxoJ<+m}HaAG|7r5h=1SlS*h;Q%dg#XI*;|*
zUYwJf&8eRh7!w|Rz(P^N+S0mm;nJl~=3JU8p4|UEF+tJmlGcXLPKN9Sg0|Tg+ZR9D
c5O2re$-er@j(H0%03E^L>FVdQ&MBb@0PCJZq5uE@
rename from mobile/android/base/resources/drawable-large-hdpi-v11/tablet_tab_close_active.png
rename to mobile/android/base/resources/drawable-hdpi/tab_close_active.png
deleted file mode 100644
index 7aadf4c45b511ead05554de58347283b7ea8de16..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 8e4908e0c58ff7d4235206e5cc22df3beacb18ce..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
index 88797492bb31a7d5c03ab5855bc9eb24be974d5d..8e4908e0c58ff7d4235206e5cc22df3beacb18ce
GIT binary patch
literal 190
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+0wn(&ce?|mT0LDHLn;_cFB}y->>$#d=o_;*
zdLqwt19jcDQw0+LJHM^Szu$D_w9)U7B$4=8rI9b3cHNe0<ayU?{z*~x=IodYo-5;C
z9P-dxF?GsCb>^9`gbaixPMiRQAL4%m2^meQ@A=<UuY6H8;iLY~)OAk}^D}<-jQlUK
nY3->)((~_l^<>ZAq}y$ru!M^%X=;Hv&_N8Iu6{1-oD!M<CNNEN
rename from mobile/android/base/resources/drawable-large-xhdpi-v11/tablet_tab_close_active.png
rename to mobile/android/base/resources/drawable-xhdpi/tab_close_active.png
rename from mobile/android/base/resources/drawable-large-xxhdpi-v11/tablet_tab_close.png
rename to mobile/android/base/resources/drawable-xxhdpi/tab_close.png
rename from mobile/android/base/resources/drawable-large-xxhdpi-v11/tablet_tab_close_active.png
rename to mobile/android/base/resources/drawable-xxhdpi/tab_close_active.png
rename from mobile/android/base/resources/drawable-large-v11/tab_item_close_button.xml
rename to mobile/android/base/resources/drawable/tab_item_close_button.xml
--- a/mobile/android/base/resources/drawable-large-v11/tab_item_close_button.xml
+++ b/mobile/android/base/resources/drawable/tab_item_close_button.xml
@@ -2,17 +2,17 @@
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
 
     <!-- pressed state -->
     <item android:state_pressed="true"
-          android:drawable="@drawable/tablet_tab_close_active"/>
+          android:drawable="@drawable/tab_close_active"/>
 
     <item android:state_checked="true"
-          android:drawable="@drawable/tablet_tab_close_active"/>
+          android:drawable="@drawable/tab_close_active"/>
 
     <!-- normal mode -->
-    <item android:drawable="@drawable/tablet_tab_close"/>
+    <item android:drawable="@drawable/tab_close"/>
 
 </selector>
--- a/mobile/android/base/resources/layout-large-v11/tab_strip_item_view.xml
+++ b/mobile/android/base/resources/layout-large-v11/tab_strip_item_view.xml
@@ -41,12 +41,12 @@
 
     <org.mozilla.gecko.widget.ThemedImageButton
         android:id="@+id/close"
         android:layout_width="40dip"
         android:layout_height="match_parent"
         android:background="@android:color/transparent"
         android:scaleType="center"
         android:contentDescription="@string/close_tab"
-        android:src="@drawable/tablet_tab_close"
+        android:src="@drawable/tab_close"
         android:duplicateParentState="true"/>
 
 </merge>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/tabs_item_cell.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
-                                           style="@style/TabsItem"
-                                           android:focusable="true"
-                                           android:id="@+id/info"
-                                           android:layout_width="wrap_content"
-                                           android:layout_height="wrap_content"
-                                           android:paddingTop="6dip"
-                                           android:paddingBottom="6dip"
-                                           android:paddingLeft="1dip"
-                                           android:paddingRight="1dip"
-                                           android:gravity="center">
-
-    <!-- We set state_private on this View dynamically in TabsListLayout. -->
-    <org.mozilla.gecko.widget.TabThumbnailWrapper
-            android:id="@+id/wrapper"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_margin="6dip"
-            android:padding="4dip"
-            android:background="@drawable/tab_thumbnail"
-            android:duplicateParentState="true">
-
-        <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
-                                                       android:layout_width="@dimen/tab_thumbnail_width"
-                                                       android:layout_height="@dimen/tab_thumbnail_height"/>
-
-        <LinearLayout android:layout_width="@dimen/tab_thumbnail_width"
-                      android:layout_height="wrap_content"
-                      android:orientation="horizontal"
-                      android:background="#EFFF"
-                      android:layout_below="@id/thumbnail"
-                      android:duplicateParentState="true">
-
-            <TextView android:id="@+id/title"
-                      android:layout_width="0dip"
-                      android:layout_height="wrap_content"
-                      android:layout_weight="1.0"
-                      android:padding="4dip"
-                      style="@style/TabLayoutItemTextAppearance"
-                      android:textSize="12sp"
-                      android:textColor="@color/placeholder_active_grey"
-                      android:singleLine="true"
-                      android:duplicateParentState="true"/>
-
-            <ImageButton android:id="@+id/audio_playing"
-                         android:visibility="gone"
-                         android:layout_width="20dip"
-                         android:layout_height="match_parent"
-                         android:background="@drawable/action_bar_button_inverse"
-                         android:scaleType="center"
-                         android:contentDescription="@string/tab_audio_playing"
-                         android:src="@drawable/tab_audio_playing"/>
-
-            <ImageButton android:id="@+id/close"
-                         style="@style/TabsItemClose"
-                         android:layout_width="32dip"
-                         android:layout_height="match_parent"
-                         android:background="@drawable/action_bar_button_inverse"
-                         android:scaleType="center"
-                         android:contentDescription="@string/close_tab"
-                         android:src="@drawable/tab_close"/>
-
-        </LinearLayout>
-
-    </org.mozilla.gecko.widget.TabThumbnailWrapper>
-
-</org.mozilla.gecko.tabs.TabsLayoutItemView>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/tabs_item_row.xml
+++ /dev/null
@@ -1,72 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
-                                           style="@style/TabsItem"
-                                           android:focusable="true"
-                                           android:id="@+id/info"
-                                           android:layout_width="match_parent"
-                                           android:layout_height="wrap_content"
-                                           android:paddingLeft="12dip"
-                                           android:paddingTop="6dip"
-                                           android:paddingBottom="6dip"
-                                           android:background="@drawable/tab_row">
-
-    <!-- We set state_private on this View dynamically in TabsListLayout. -->
-    <org.mozilla.gecko.widget.TabThumbnailWrapper
-                  android:id="@+id/wrapper"
-	          android:layout_width="wrap_content"
-                  android:layout_height="wrap_content"
-                  android:padding="4dip"
-                  android:background="@drawable/tab_thumbnail"
-                  android:duplicateParentState="true">
-
-        <org.mozilla.gecko.tabs.TabsPanelThumbnailView android:id="@+id/thumbnail"
-                                                       android:layout_width="@dimen/tab_thumbnail_width"
-                                                       android:layout_height="@dimen/tab_thumbnail_height"/>
-
-    </org.mozilla.gecko.widget.TabThumbnailWrapper>
-
-    <LinearLayout android:layout_width="0dip"
-                  android:layout_height="match_parent"
-                  android:orientation="vertical"
-                  android:layout_weight="1.0"
-                  android:paddingTop="4dip"
-                  android:paddingLeft="8dip"
-                  android:paddingRight="4dip">
-
-        <TextView android:id="@+id/title"
-                  android:layout_width="match_parent"
-                  android:layout_height="0dip"
-                  android:layout_weight="1.0"
-                  style="@style/TabLayoutItemTextAppearance"
-                  android:textColor="#FFFFFFFF"
-                  android:textSize="14sp"
-                  android:singleLine="false"
-                  android:maxLines="4"
-                  android:duplicateParentState="true"/>
-
-        <ImageButton android:id="@+id/audio_playing"
-                     android:visibility="gone"
-                     android:layout_width="20dip"
-                     android:layout_height="20dip"
-                     android:gravity="bottom"
-                     android:background="@drawable/action_bar_button_inverse"
-                     android:scaleType="center"
-                     android:contentDescription="@string/tab_audio_playing"
-                     android:src="@drawable/tab_audio_playing"/>
-
-    </LinearLayout>
-
-    <ImageButton android:id="@+id/close"
-                 style="@style/TabsItemClose"
-                 android:layout_width="34dip"
-                 android:layout_height="match_parent"
-                 android:background="@drawable/action_bar_button_inverse"
-                 android:scaleType="center"
-                 android:contentDescription="@string/close_tab"
-                 android:src="@drawable/tab_close"/>
-
-</org.mozilla.gecko.tabs.TabsLayoutItemView>
rename from mobile/android/base/resources/layout-v11/tablet_tabs_item_cell.xml
rename to mobile/android/base/resources/layout/tabs_layout_item_view.xml
deleted file mode 100644
--- a/mobile/android/base/resources/values-land/layout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<resources>
-    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
-</resources>
deleted file mode 100644
--- a/mobile/android/base/resources/values-large-v11/layout.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<resources>
-    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
-</resources>
--- a/mobile/android/base/resources/values/layout.xml
+++ b/mobile/android/base/resources/values/layout.xml
@@ -1,16 +1,13 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
-    <item type="layout" name="tabs_layout_item_view">@layout/tabs_item_row</item>
-
     <!-- These items are v11+ resources but are referenced in code shipped with
          API 9 builds. Since v11+ resources don't ship on API 9 builds, in order
          for the resource ID to be found (and thus compilation to succeed), we
          provide dummy values below. -->
     <item type="layout" name="tab_strip">@null</item>
-    <item type="layout" name="tablet_tabs_item_cell">@null</item>
     <item type="layout" name="tabs_panel_back_button">@null</item>
 </resources>
--- a/mobile/android/base/restrictions/RestrictionProvider.java
+++ b/mobile/android/base/restrictions/RestrictionProvider.java
@@ -3,16 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.restrictions;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
 import org.mozilla.gecko.restrictions.Restriction;
+import org.mozilla.gecko.sync.setup.Constants;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.RestrictionEntry;
 import android.os.Build;
@@ -48,16 +49,20 @@ public class RestrictionProvider extends
             }
         }.start();
     }
 
     private ArrayList<RestrictionEntry> initRestrictions(Context context, Bundle oldRestrictions) {
         ArrayList<RestrictionEntry> entries = new ArrayList<RestrictionEntry>();
 
         for (Restriction restriction : RestrictedProfileConfiguration.DEFAULT_RESTRICTIONS) {
+            if (restriction == Restriction.DISALLOW_LOCATION_SERVICE && !AppConstants.MOZ_STUMBLER_BUILD_TIME_ENABLED) {
+                continue;
+            }
+
             RestrictionEntry entry = createRestrictionEntryWithDefaultValue(context, restriction,
                     oldRestrictions.getBoolean(restriction.name, true));
             entries.add(entry);
         }
 
         return entries;
     }
 
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -416,17 +416,17 @@ class TabsGridLayout extends GridView
         animator.start();
     }
 
     private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
 
         final private Button.OnClickListener mCloseClickListener;
 
         public TabsGridLayoutAdapter(Context context) {
-            super(context, R.layout.tablet_tabs_item_cell);
+            super(context, R.layout.tabs_layout_item_view);
 
             mCloseClickListener = new Button.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     closeTab(v);
                 }
             };
         }
--- a/mobile/android/base/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/tabs/TabsLayoutItemView.java
@@ -97,19 +97,17 @@ public class TabsLayoutItemView extends 
     protected void onFinishInflate() {
         super.onFinishInflate();
         mTitle = (TextView) findViewById(R.id.title);
         mThumbnail = (TabsPanelThumbnailView) findViewById(R.id.thumbnail);
         mCloseButton = (ImageView) findViewById(R.id.close);
         mAudioPlayingButton = (ImageView) findViewById(R.id.audio_playing);
         mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
 
-        if (HardwareUtils.isTablet()) {
-            growCloseButtonHitArea();
-        }
+        growCloseButtonHitArea();
 
         mAudioPlayingButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 if (mTabId < 0) {
                     throw new IllegalStateException("Invalid tab id:" + mTabId);
                 }
 
--- a/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java
+++ b/mobile/android/tests/browser/junit3/src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java
@@ -1,31 +1,35 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.tests.browser.junit3;
 
 import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentValues;
+import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.RemoteException;
 import android.test.InstrumentationTestCase;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.background.db.CursorDumper;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.LocalTabsAccessor;
 import org.mozilla.gecko.db.RemoteClient;
-import org.mozilla.gecko.db.TabsAccessor;
 import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
 
 import java.util.List;
 
 public class TestRemoteTabs extends InstrumentationTestCase {
+    private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60 * 24;
+    private static final long ONE_WEEK_IN_MILLISECONDS = 7 * ONE_DAY_IN_MILLISECONDS;
+    private static final long THREE_WEEKS_IN_MILLISECONDS = 3 * ONE_WEEK_IN_MILLISECONDS;
+
     public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception {
         final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
         final ContentResolver cr = getInstrumentation().getTargetContext().getContentResolver();
         final ContentProviderClient cpc = cr.acquireContentProviderClient(uri);
         final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
 
         try {
             // Delete all tabs to begin with.
@@ -119,9 +123,94 @@ public class TestRemoteTabs extends Inst
                 assertEquals("guid2", clients.get(1).guid);
             } finally {
                 allClients.close();
             }
         } finally {
             cpc.release();
         }
     }
+
+    public void testGetRecentRemoteClientsUpToOneWeekOld() throws Exception {
+        final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
+        final Context context = getInstrumentation().getTargetContext();
+        final String profileName = GeckoProfile.get(context).getName();
+        final ContentResolver cr = context.getContentResolver();
+        final ContentProviderClient cpc = cr.acquireContentProviderClient(uri);
+        final LocalTabsAccessor accessor = new LocalTabsAccessor(profileName);
+
+        try {
+            // Start Clean
+            cpc.delete(uri, null, null);
+            final Cursor allClients = cpc.query(uri, null, null, null, null);
+            try {
+                assertEquals(0, allClients.getCount());
+            } finally {
+                allClients.close();
+            }
+
+            // Insert a local and remote1 client record, neither with tabs.
+            final long now = System.currentTimeMillis();
+            // Local client has GUID = null.
+            final ContentValues local = new ContentValues();
+            local.put(BrowserContract.Clients.NAME, "local");
+            local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
+            // Remote clients have GUID != null.
+            final ContentValues remote1 = new ContentValues();
+            remote1.put(BrowserContract.Clients.GUID, "guid1");
+            remote1.put(BrowserContract.Clients.NAME, "remote1");
+            remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
+
+            // Insert a Remote Client that is 6 days old.
+            final ContentValues remote2 = new ContentValues();
+            remote2.put(BrowserContract.Clients.GUID, "guid2");
+            remote2.put(BrowserContract.Clients.NAME, "remote2");
+            remote2.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+            // Insert a Remote Client with the same name as previous but with more than 3 weeks old
+            final ContentValues remote3 = new ContentValues();
+            remote3.put(BrowserContract.Clients.GUID, "guid21");
+            remote3.put(BrowserContract.Clients.NAME, "remote2");
+            remote3.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS - ONE_DAY_IN_MILLISECONDS);
+
+            // Insert another remote client with the same name as previous but with 3 weeks - 1 day old.
+            final ContentValues remote4 = new ContentValues();
+            remote4.put(BrowserContract.Clients.GUID, "guid22");
+            remote4.put(BrowserContract.Clients.NAME, "remote2");
+            remote4.put(BrowserContract.Clients.LAST_MODIFIED, now - THREE_WEEKS_IN_MILLISECONDS + ONE_DAY_IN_MILLISECONDS);
+
+            // Insert a Remote Client that is exactly one week old.
+            final ContentValues remote5 = new ContentValues();
+            remote5.put(BrowserContract.Clients.GUID, "guid3");
+            remote5.put(BrowserContract.Clients.NAME, "remote3");
+            remote5.put(BrowserContract.Clients.LAST_MODIFIED, now - ONE_WEEK_IN_MILLISECONDS);
+
+            ContentValues[] values = new ContentValues[]{local, remote1, remote2, remote3, remote4, remote5};
+            int inserted = cpc.bulkInsert(uri, values);
+            assertEquals(values.length, inserted);
+
+            final Cursor remoteClients =
+                    accessor.getRemoteClientsByRecencyCursor(context);
+
+            try {
+                CursorDumper.dumpCursor(remoteClients);
+                // Local client is not included.
+                // (remote1, guid1), (remote2, guid2), (remote3, guid3) are expected.
+                assertEquals(3, remoteClients.getCount());
+
+                // Check the inner data, according to recency.
+                List<RemoteClient> recentRemoteClientsList =
+                        accessor.getClientsWithoutTabsByRecencyFromCursor(remoteClients);
+                assertEquals(3, recentRemoteClientsList.size());
+                assertEquals("remote1", recentRemoteClientsList.get(0).name);
+                assertEquals("guid1", recentRemoteClientsList.get(0).guid);
+                assertEquals("remote2", recentRemoteClientsList.get(1).name);
+                assertEquals("guid2", recentRemoteClientsList.get(1).guid);
+                assertEquals("remote3", recentRemoteClientsList.get(2).name);
+                assertEquals("guid3", recentRemoteClientsList.get(2).guid);
+            } finally {
+                remoteClients.close();
+            }
+        } finally {
+            cpc.release();
+        }
+    }
 }
--- a/toolkit/components/perfmonitoring/PerformanceStats-content.js
+++ b/toolkit/components/perfmonitoring/PerformanceStats-content.js
@@ -74,18 +74,19 @@ Services.cpmm.addMessageListener("perfor
  *
  * @param {{payload: Array<string>}} msg.data The message received. `payload`
  * must be an array of probe names.
  */
 Services.cpmm.addMessageListener("performance-stats-service-release", function(msg) {
   if (!isContent) {
     return;
   }
+
   // Keep only the probes that do not appear in the payload
-  let probes = gMonitor.getProbeNames
+  let probes = gMonitor.probeNames
     .filter(x => msg.data.payload.indexOf(x) == -1);
   gMonitor = PerformanceStats.getMonitor(probes);
 });
 
 /**
  * Ensure that this process has all the probes it needs.
  *
  * @param {Array<string>} probeNames The name of all probes needed by the
--- a/toolkit/components/telemetry/docs/environment.rst
+++ b/toolkit/components/telemetry/docs/environment.rst
@@ -36,16 +36,17 @@ Structure::
         blocklistEnabled: <bool>, // true on failure
         isDefaultBrowser: <bool>, // null on failure, not available on Android
         defaultSearchEngine: <string>, // e.g. "yahoo"
         defaultSearchEngineData: {, // data about the current default engine
           name: <string>, // engine name, e.g. "Yahoo"; or "NONE" if no default
           loadPath: <string>, // where the engine line is located; missing if no default
           submissionURL: <string> // missing if no default or for user-installed engines
         },
+        searchCohort: <string>, // optional, contains an identifier for any active search A/B experiments
         e10sEnabled: <bool>, // whether e10s is on, i.e. browser tabs open by default in a different process
         telemetryEnabled: <bool>, // false on failure
         isInOptoutSample: <bool>, // whether this client is part of the opt-out sample
         locale: <string>, // e.g. "it", null on failure
         update: {
           channel: <string>, // e.g. "release", null on failure
           enabled: <bool>, // true on failure
           autoDownload: <bool>, // true on failure