Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Tue, 30 Aug 2016 17:51:54 -0700
changeset 311942 506facea63169a29e04eb140663da1730052db64
parent 311919 5931a8286060ca165423aba08b165e0c8bff71d9 (current diff)
parent 311941 0f4d2ef453c82d2499564402c0c3666ac834b5d8 (diff)
child 311972 60063982b91c3adf6570312546c489b749ea48be
child 312000 0fab4436ad09df818b87d4dba3e11b15b5a804d9
push id30626
push userkwierso@gmail.com
push dateWed, 31 Aug 2016 00:51:58 +0000
treeherdermozilla-central@506facea6316 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone51.0a1
first release with
nightly linux32
506facea6316 / 51.0a1 / 20160831030224 / files
nightly linux64
506facea6316 / 51.0a1 / 20160831030224 / files
nightly mac
506facea6316 / 51.0a1 / 20160831030224 / files
nightly win32
506facea6316 / 51.0a1 / 20160831030224 / files
nightly win64
506facea6316 / 51.0a1 / 20160831030224 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
mobile/android/base/java/org/mozilla/gecko/home/activitystream/TopSitesRecyclerAdapter.java
mobile/android/base/resources/layout/activity_stream_card_top_sites_item.xml
toolkit/components/telemetry/Histograms.json
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -19,21 +19,33 @@ richlistitem[type="download"]:not([selec
 }
 
 richlistitem[type="download"].download-state[state="1"]:not([exists]) .downloadShow {
   display: none;
 }
 
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryProgress,
 #downloadsSummary:not([inprogress]) > vbox > #downloadsSummaryDetails,
-#downloadsFooter[showingsummary] > #downloadsFooterButtons,
-#downloadsFooter:not([showingsummary]) > #downloadsSummary {
+#downloadsFooter:not([showingsummary]) #downloadsSummary {
   display: none;
 }
 
+#downloadsFooter[showingdropdown] > stack > #downloadsSummary,
+#downloadsFooter[showingsummary] > stack:hover > #downloadsSummary,
+#downloadsFooter[showingsummary]:not([showingdropdown]) > stack:not(:hover) > #downloadsFooterButtons {
+  /* If we used "visibility: hidden;" then the mouseenter event of
+     #downloadsHistory wouldn't be triggered immediately, and the hover styling
+     of the button would not apply until the mouse is moved again.
+
+     "-moz-user-focus: ignore;" prevents the elements with "opacity: 0;" from
+     being focused with the keyboard. */
+  opacity: 0;
+  -moz-user-focus: ignore;
+}
+
 /*** Downloads View ***/
 
 /**
  * The downloads richlistbox may list thousands of items, and it turns out
  * XBL binding attachment, and even more so detachment, is a performance hog.
  * This hack makes sure we don't apply any binding to inactive items (inactive
  * items are history downloads that haven't been in the visible area).
  * We can do this because the richlistbox implementation does not interact
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -377,24 +377,25 @@ const DownloadsPanel = {
 
   onFooterPopupShowing(aEvent) {
     let itemClearList = document.getElementById("downloadsDropdownItemClearList");
     if (DownloadsCommon.getData(window).canRemoveFinished) {
       itemClearList.removeAttribute("hidden");
     } else {
       itemClearList.setAttribute("hidden", "true");
     }
+    DownloadsViewController.updateCommands();
 
-    document.getElementById("downloadsFooterButtonsSplitter").classList
-      .add("downloadsDropmarkerSplitterExtend");
+    document.getElementById("downloadsFooter")
+      .setAttribute("showingdropdown", true);
   },
 
   onFooterPopupHidden(aEvent) {
-    document.getElementById("downloadsFooterButtonsSplitter").classList
-      .remove("downloadsDropmarkerSplitterExtend");
+    document.getElementById("downloadsFooter")
+      .removeAttribute("showingdropdown");
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Related operations
 
   /**
    * Shows or focuses the user interface dedicated to downloads history.
    */
--- a/browser/components/downloads/content/downloadsOverlay.xul
+++ b/browser/components/downloads/content/downloadsOverlay.xul
@@ -124,61 +124,63 @@
                        ondragstart="DownloadsView.onDownloadDragStart(event);"/>
           <description id="emptyDownloads"
                        mousethrough="always">
              &downloadsPanelEmpty.label;
           </description>
           <spacer flex="1"/>
           <vbox id="downloadsFooter"
                 class="downloadsPanelFooter">
-            <hbox id="downloadsSummary"
-                  align="center"
-                  orient="horizontal"
-                  onkeydown="DownloadsSummary.onKeyDown(event);"
-                  onclick="DownloadsSummary.onClick(event);">
-              <image class="downloadTypeIcon" />
-              <vbox pack="center"
-                    class="downloadContainer"
-                    style="width: &downloadDetails.width;">
-                <description id="downloadsSummaryDescription"
-                             style="min-width: &downloadsSummary.minWidth2;"/>
-                <progressmeter id="downloadsSummaryProgress"
-                               class="downloadProgress"
-                               min="0"
-                               max="100"
-                               mode="normal" />
-                <description id="downloadsSummaryDetails"
-                             crop="end"/>
-              </vbox>
-            </hbox>
-            <hbox id="downloadsFooterButtons">
-              <button id="downloadsHistory"
-                      class="plain downloadsPanelFooterButton"
-                      label="&downloadsHistory.label;"
-                      accesskey="&downloadsHistory.accesskey;"
-                      flex="1"
-                      oncommand="DownloadsPanel.showDownloadsHistory();"/>
-              <toolbarseparator id="downloadsFooterButtonsSplitter"
-                      class="downloadsDropmarkerSplitter"/>
-              <button id="downloadsFooterDropmarker"
-                      class="plain downloadsPanelFooterButton downloadsDropmarker"
-                      type="menu">
-                <menupopup id="downloadSubPanel"
-                           onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);"
-                           onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);"
-                           position="after_end">
-                  <menuitem id="downloadsDropdownItemClearList"
-                            command="downloadsCmd_clearList"
-                            label="&cmd.clearList2.label;"/>
-                  <menuitem id="downloadsDropdownItemOpenDownloadsFolder"
-                            oncommand="DownloadsPanel.openDownloadsFolder();"
-                            label="&openDownloadsFolder.label;"/>
-                </menupopup>
-              </button>
-            </hbox>
+            <stack>
+              <hbox id="downloadsSummary"
+                    align="center"
+                    orient="horizontal"
+                    onkeydown="DownloadsSummary.onKeyDown(event);"
+                    onclick="DownloadsSummary.onClick(event);">
+                <image class="downloadTypeIcon" />
+                <vbox pack="center"
+                      class="downloadContainer"
+                      style="width: &downloadDetails.width;">
+                  <description id="downloadsSummaryDescription"
+                               style="min-width: &downloadsSummary.minWidth2;"/>
+                  <progressmeter id="downloadsSummaryProgress"
+                                 class="downloadProgress"
+                                 min="0"
+                                 max="100"
+                                 mode="normal" />
+                  <description id="downloadsSummaryDetails"
+                               crop="end"/>
+                </vbox>
+              </hbox>
+              <hbox id="downloadsFooterButtons">
+                <button id="downloadsHistory"
+                        class="plain downloadsPanelFooterButton"
+                        label="&downloadsHistory.label;"
+                        accesskey="&downloadsHistory.accesskey;"
+                        flex="1"
+                        oncommand="DownloadsPanel.showDownloadsHistory();"/>
+                <toolbarseparator id="downloadsFooterButtonsSplitter"
+                        class="downloadsDropmarkerSplitter"/>
+                <button id="downloadsFooterDropmarker"
+                        class="plain downloadsPanelFooterButton downloadsDropmarker"
+                        type="menu">
+                  <menupopup id="downloadSubPanel"
+                             onpopupshowing="DownloadsPanel.onFooterPopupShowing(event);"
+                             onpopuphidden="DownloadsPanel.onFooterPopupHidden(event);"
+                             position="after_end">
+                    <menuitem id="downloadsDropdownItemClearList"
+                              command="downloadsCmd_clearList"
+                              label="&cmd.clearList2.label;"/>
+                    <menuitem id="downloadsDropdownItemOpenDownloadsFolder"
+                              oncommand="DownloadsPanel.openDownloadsFolder();"
+                              label="&openDownloadsFolder.label;"/>
+                  </menupopup>
+                </button>
+              </hbox>
+            </stack>
           </vbox>
         </panelview>
 
         <panelview id="downloadsPanel-blockedSubview"
                    orient="vertical"
                    flex="1">
           <description id="downloadsPanel-blockedSubview-title"/>
           <description id="downloadsPanel-blockedSubview-details1"/>
--- a/browser/themes/shared/downloads/downloads.inc.css
+++ b/browser/themes/shared/downloads/downloads.inc.css
@@ -89,17 +89,17 @@
   padding-left: 58px !important;
 }
 
 toolbarseparator.downloadsDropmarkerSplitter {
   margin: 7px 0;
 }
 
 #downloadsFooter:hover toolbarseparator.downloadsDropmarkerSplitter,
-#downloadsFooter toolbarseparator.downloadsDropmarkerSplitterExtend {
+#downloadsFooter[showingdropdown] toolbarseparator {
   margin: 0;
 }
 
 .downloadsDropmarker {
   padding: 0 19px !important;
 }
 
 .downloadsDropmarker > .button-box > hbox {
--- a/devtools/client/inspector/inspector.xul
+++ b/devtools/client/inspector/inspector.xul
@@ -24,16 +24,17 @@
   <!ENTITY % layoutviewDTD SYSTEM "chrome://devtools/locale/layoutview.dtd"> %layoutviewDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:html="http://www.w3.org/1999/xhtml">
 
   <script type="application/javascript;version=1.8"
           src="chrome://devtools/content/shared/theme-switching.js"/>
+
   <box flex="1" class="devtools-responsive-container theme-body">
     <vbox flex="1" class="devtools-main-content">
       <html:div id="inspector-toolbar"
         class="devtools-toolbar"
         nowindowdrag="true">
         <html:button id="inspector-element-add-button"
           title="&inspectorAddNode.label;"
           class="devtools-button" />
--- a/devtools/client/inspector/toolsidebar.js
+++ b/devtools/client/inspector/toolsidebar.js
@@ -76,16 +76,18 @@ ToolSidebar.prototype = {
 
   // Rendering
 
   render: function () {
     let Tabbar = this.React.createFactory(this.browserRequire(
       "devtools/client/shared/components/tabs/tabbar"));
 
     let sidebar = Tabbar({
+      toolbox: this._toolPanel._toolbox,
+      showAllTabsMenu: true,
       onSelect: this.handleSelectionChange.bind(this),
     });
 
     this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
   },
 
   addExistingTab: function (id, title, selected) {
     this._tabbar.addTab(id, title, selected, this.InspectorTabPanel);
--- a/devtools/client/shared/components/reps/date-time.js
+++ b/devtools/client/shared/components/reps/date-time.js
@@ -25,17 +25,17 @@ define(function (require, exports, modul
     propTypes: {
       object: React.PropTypes.object.isRequired
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return this.props.objectLink({
           object: grip
-        }, grip.class);
+        }, grip.class + " ");
       }
       return "";
     },
 
     render: function () {
       let grip = this.props.object;
       return (
         span({className: "objectBox"},
--- a/devtools/client/shared/components/reps/document.js
+++ b/devtools/client/shared/components/reps/document.js
@@ -31,17 +31,17 @@ define(function (require, exports, modul
       return location ? getURLDisplayString(location) : "";
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return span({className: "objectBox"},
           this.props.objectLink({
             object: grip
-          }, grip.class)
+          }, grip.class + " ")
         );
       }
       return "";
     },
 
     getTooltip: function (doc) {
       return doc.location.href;
     },
--- a/devtools/client/shared/components/reps/function.js
+++ b/devtools/client/shared/components/reps/function.js
@@ -25,17 +25,17 @@ define(function (require, exports, modul
     propTypes: {
       object: React.PropTypes.object.isRequired
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return this.props.objectLink({
           object: grip
-        }, "function");
+        }, "function ");
       }
       return "";
     },
 
     summarizeFunction: function (grip) {
       let name = grip.userDisplayName || grip.displayName || grip.name || "function";
       return cropString(name + "()", 100);
     },
--- a/devtools/client/shared/components/reps/grip-array.js
+++ b/devtools/client/shared/components/reps/grip-array.js
@@ -33,17 +33,17 @@ define(function (require, exports, modul
       return grip.preview ? grip.preview.length : 0;
     },
 
     getTitle: function (object, context) {
       let objectLink = this.props.objectLink || span;
       if (this.props.mode != "tiny") {
         return objectLink({
           object: object
-        }, object.class);
+        }, object.class + " ");
       }
       return "";
     },
 
     arrayIterator: function (grip, max) {
       let items = [];
 
       if (!grip.preview || !grip.preview.length) {
--- a/devtools/client/shared/components/reps/grip.js
+++ b/devtools/client/shared/components/reps/grip.js
@@ -29,17 +29,17 @@ define(function (require, exports, modul
       mode: React.PropTypes.string,
       isInterestingProp: React.PropTypes.func
     },
 
     getTitle: function (object) {
       if (this.props.objectLink) {
         return this.props.objectLink({
           object: object
-        }, object.class);
+        }, object.class + " ");
       }
       return object.class || "Object";
     },
 
     safePropIterator: function (object, max) {
       max = (typeof max === "undefined") ? 3 : max;
       try {
         return this.propIterator(object, max);
--- a/devtools/client/shared/components/reps/object-with-text.js
+++ b/devtools/client/shared/components/reps/object-with-text.js
@@ -26,17 +26,17 @@ define(function (require, exports, modul
       object: React.PropTypes.object.isRequired,
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return span({className: "objectBox"},
           this.props.objectLink({
             object: grip
-          }, this.getType(grip))
+          }, this.getType(grip) + " ")
         );
       }
       return "";
     },
 
     getType: function (grip) {
       return grip.class;
     },
--- a/devtools/client/shared/components/reps/object-with-url.js
+++ b/devtools/client/shared/components/reps/object-with-url.js
@@ -26,17 +26,17 @@ define(function (require, exports, modul
       object: React.PropTypes.object.isRequired,
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return span({className: "objectBox"},
           this.props.objectLink({
             object: grip
-          }, this.getType(grip))
+          }, this.getType(grip) + " ")
         );
       }
       return "";
     },
 
     getType: function (grip) {
       return grip.class;
     },
--- a/devtools/client/shared/components/reps/object.js
+++ b/devtools/client/shared/components/reps/object.js
@@ -25,17 +25,17 @@ define(function (require, exports, modul
       object: React.PropTypes.object,
       mode: React.PropTypes.string,
     },
 
     getTitle: function (object) {
       if (this.props.objectLink) {
         return this.props.objectLink({
           object: object
-        }, object.class);
+        }, object.class + " ");
       }
       return "Object";
     },
 
     safePropIterator: function (object, max) {
       max = (typeof max === "undefined") ? 3 : max;
       try {
         return this.propIterator(object, max);
--- a/devtools/client/shared/components/reps/rep.js
+++ b/devtools/client/shared/components/reps/rep.js
@@ -67,16 +67,17 @@ define(function (require, exports, modul
    * property.
    */
   const Rep = React.createClass({
     displayName: "Rep",
 
     propTypes: {
       object: React.PropTypes.any,
       defaultRep: React.PropTypes.object,
+      mode: React.PropTypes.string
     },
 
     render: function () {
       let rep = getRep(this.props.object, this.props.defaultRep);
       return rep(this.props);
     },
   });
 
--- a/devtools/client/shared/components/reps/stylesheet.js
+++ b/devtools/client/shared/components/reps/stylesheet.js
@@ -27,17 +27,17 @@ define(function (require, exports, modul
     },
 
     getTitle: function (grip) {
       let title = "StyleSheet ";
       if (this.props.objectLink) {
         return DOM.span({className: "objectBox"},
           this.props.objectLink({
             object: grip
-          }, title)
+          }, title + " ")
         );
       }
       return title;
     },
 
     getLocation: function (grip) {
       // Embedded stylesheets don't have URL and so, no preview.
       let url = grip.preview ? grip.preview.url : "";
--- a/devtools/client/shared/components/reps/window.js
+++ b/devtools/client/shared/components/reps/window.js
@@ -26,17 +26,17 @@ define(function (require, exports, modul
       object: React.PropTypes.object.isRequired,
     },
 
     getTitle: function (grip) {
       if (this.props.objectLink) {
         return DOM.span({className: "objectBox"},
           this.props.objectLink({
             object: grip
-          }, grip.class)
+          }, grip.class + " ")
         );
       }
       return "";
     },
 
     getLocation: function (grip) {
       return getURLDisplayString(grip.preview.url);
     },
--- a/devtools/client/shared/components/tabs/tabbar.css
+++ b/devtools/client/shared/components/tabs/tabbar.css
@@ -29,18 +29,21 @@
   height: 24px;
 }
 
 .tabs .tabs-menu-item a {
   cursor: default;
 }
 
 /* The tab takes entire horizontal space and individual tabs
-  should stretch accordingly. Use flexbox for the behavior. */
+  should stretch accordingly. Use flexbox for the behavior.
+  Use also `overflow: hidden` so, 'overflow' and 'underflow'
+  events are fired (it's utilized by the all-tabs-menu). */
 .tabs .tabs-navigation .tabs-menu {
+  overflow: hidden;
   display: flex;
 }
 
 .tabs .tabs-navigation .tabs-menu-item {
   flex-grow: 1;
 }
 
 .tabs .tabs-navigation .tabs-menu-item a {
--- a/devtools/client/shared/components/tabs/tabbar.js
+++ b/devtools/client/shared/components/tabs/tabbar.js
@@ -4,27 +4,38 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { DOM, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
 const Tabs = createFactory(require("devtools/client/shared/components/tabs/tabs").Tabs);
 
+const Menu = require("devtools/client/framework/menu");
+const MenuItem = require("devtools/client/framework/menu-item");
+
 // Shortcuts
 const { div } = DOM;
 
 /**
  * Renders Tabbar component.
  */
 let Tabbar = createClass({
   displayName: "Tabbar",
 
   propTypes: {
     onSelect: PropTypes.func,
+    showAllTabsMenu: PropTypes.bool,
+    toolbox: PropTypes.object,
+  },
+
+  getDefaultProps: function () {
+    return {
+      showAllTabsMenu: false,
+    };
   },
 
   getInitialState: function () {
     return {
       tabs: [],
       activeTab: 0
     };
   },
@@ -120,16 +131,43 @@ let Tabbar = createClass({
       activeTab: index
     });
 
     if (this.props.onSelect) {
       this.props.onSelect(this.state.tabs[index].id);
     }
   },
 
+  onAllTabsMenuClick: function (event) {
+    let menu = new Menu();
+    let target = event.target;
+
+    // Generate list of menu items from the list of tabs.
+    this.state.tabs.forEach(tab => {
+      menu.append(new MenuItem({
+        label: tab.title,
+        type: "checkbox",
+        checked: this.getCurrentTabId() == tab.id,
+        click: () => this.select(tab.id),
+      }));
+    });
+
+    // Show a drop down menu with frames.
+    // XXX Missing menu API for specifying target (anchor)
+    // and relative position to it. See also:
+    // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XUL/Method/openPopup
+    // https://bugzilla.mozilla.org/show_bug.cgi?id=1274551
+    let rect = target.getBoundingClientRect();
+    let screenX = target.ownerDocument.defaultView.mozInnerScreenX;
+    let screenY = target.ownerDocument.defaultView.mozInnerScreenY;
+    menu.popup(rect.left + screenX, rect.bottom + screenY, this.props.toolbox);
+
+    return menu;
+  },
+
   // Rendering
 
   renderTab: function (tab) {
     if (typeof tab.panel === "function") {
       return tab.panel({
         key: tab.id,
         title: tab.title,
         id: tab.id,
@@ -143,16 +181,18 @@ let Tabbar = createClass({
   render: function () {
     let tabs = this.state.tabs.map(tab => {
       return this.renderTab(tab);
     });
 
     return (
       div({className: "devtools-sidebar-tabs"},
         Tabs({
+          onAllTabsMenuClick: this.onAllTabsMenuClick,
+          showAllTabsMenu: this.props.showAllTabsMenu,
           tabActive: this.state.activeTab,
           onAfterChange: this.onTabChanged},
           tabs
         )
       )
     );
   },
 });
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -34,25 +34,47 @@
 .tabs .panels {
   height: calc(100% - 24px);
 }
 
 .tabs .tab-panel {
   height: 100%;
 }
 
+.tabs .all-tabs-menu  {
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 15px;
+  height: 100%;
+  border-style: solid;
+  border-width: 0;
+  border-inline-start-width: 1px;
+  border-color: var(--theme-splitter-color);
+  background: url("chrome://devtools/skin/images/dropmarker.svg");
+  background-repeat: no-repeat;
+  background-position: center;
+  background-color: var(--theme-tab-toolbar-background);
+}
+
+.tabs .all-tabs-menu:-moz-locale-dir(rtl) {
+  right: unset;
+  left: 0;
+}
+
 /* Light Theme */
 
 .theme-dark .tabs,
 .theme-light .tabs {
   background: var(--theme-body-background);
 }
 
 .theme-dark .tabs .tabs-navigation,
 .theme-light .tabs .tabs-navigation {
+  position: relative;
   border-bottom: 1px solid var(--theme-splitter-color);
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-menu-item,
 .theme-light .tabs .tabs-menu-item {
   margin: 0;
   padding: 0;
--- a/devtools/client/shared/components/tabs/tabs.js
+++ b/devtools/client/shared/components/tabs/tabs.js
@@ -42,70 +42,108 @@ define(function (require, exports, modul
       ]),
       tabActive: React.PropTypes.number,
       onMount: React.PropTypes.func,
       onBeforeChange: React.PropTypes.func,
       onAfterChange: React.PropTypes.func,
       children: React.PropTypes.oneOfType([
         React.PropTypes.array,
         React.PropTypes.element
-      ]).isRequired
+      ]).isRequired,
+      showAllTabsMenu: React.PropTypes.bool,
+      onAllTabsMenuClick: React.PropTypes.func,
     },
 
     getDefaultProps: function () {
       return {
-        tabActive: 0
+        tabActive: 0,
+        showAllTabsMenu: false,
       };
     },
 
     getInitialState: function () {
       return {
         tabActive: this.props.tabActive,
 
         // This array is used to store an information whether a tab
         // at specific index has already been created (e.g. selected
         // at least once).
         // If yes, it's rendered even if not currently selected.
         // This is because in some cases we don't want to re-create
         // tab content when it's being unselected/selected.
         // E.g. in case of an iframe being used as a tab-content
         // we want the iframe to stay in the DOM.
         created: [],
+
+        // True if tabs can't fit into available horizontal space.
+        overflow: false,
       };
     },
 
     componentDidMount: function () {
       let node = findDOMNode(this);
       node.addEventListener("keydown", this.onKeyDown, false);
 
+      // Register overflow listeners to manage visibility
+      // of all-tabs-menu. This menu is displayed when there
+      // is not enough h-space to render all tabs.
+      // It allows the user to select a tab even if it's hidden.
+      if (this.props.showAllTabsMenu) {
+        node.addEventListener("overflow", this.onOverflow, false);
+        node.addEventListener("underflow", this.onUnderflow, false);
+      }
+
       let index = this.state.tabActive;
       if (this.props.onMount) {
         this.props.onMount(index);
       }
     },
 
     componentWillReceiveProps: function (newProps) {
-      if (newProps.tabActive) {
+      // Check type of 'tabActive' props to see if it's valid
+      // (it's 0-based index).
+      if (typeof newProps.tabActive == "number") {
         let created = [...this.state.created];
         created[newProps.tabActive] = true;
 
         this.setState(Object.assign({}, this.state, {
           tabActive: newProps.tabActive,
           created: created,
         }));
       }
     },
 
     componentWillUnmount: function () {
       let node = findDOMNode(this);
       node.removeEventListener("keydown", this.onKeyDown, false);
+
+      if (this.props.showAllTabsMenu) {
+        node.removeEventListener("overflow", this.onOverflow, false);
+        node.removeEventListener("underflow", this.onUnderflow, false);
+      }
     },
 
     // DOM Events
 
+    onOverflow: function (event) {
+      if (event.target.classList.contains("tabs-menu")) {
+        this.setState({
+          overflow: true
+        });
+      }
+    },
+
+    onUnderflow: function (event) {
+      if (event.target.classList.contains("tabs-menu")) {
+        this.setState({
+          overflow: false
+        });
+      }
+    },
+
     onKeyDown: function (event) {
       // Bail out if the focus isn't on a tab.
       if (!event.target.closest(".tabs-menu-item")) {
         return;
       }
 
       let tabActive = this.state.tabActive;
       let tabCount = this.props.children.length;
@@ -124,16 +162,22 @@ define(function (require, exports, modul
       }
     },
 
     onClickTab: function (index, event) {
       this.setActive(index);
       event.preventDefault();
     },
 
+    onAllTabsMenuClick: function (event) {
+      if (this.props.onAllTabsMenuClick) {
+        this.props.onAllTabsMenuClick(event);
+      }
+    },
+
     // API
 
     setActive: function (index) {
       let onAfterChange = this.props.onAfterChange;
       let onBeforeChange = this.props.onBeforeChange;
 
       if (onBeforeChange) {
         let cancel = onBeforeChange(index);
@@ -213,21 +257,31 @@ define(function (require, exports, modul
                 onClick: this.onClickTab.bind(this, index),
               },
                 title
               )
             )
           );
         });
 
+      // Display the menu only if there is not enough horizontal
+      // space for all tabs (and overflow happened).
+      let allTabsMenu = this.state.overflow ? (
+        DOM.div({
+          className: "all-tabs-menu",
+          onClick: this.props.onAllTabsMenuClick
+        })
+      ) : null;
+
       return (
         DOM.nav({className: "tabs-navigation"},
           DOM.ul({className: "tabs-menu", role: "tablist"},
             tabs
-          )
+          ),
+          allTabsMenu
         )
       );
     },
 
     renderPanels: function () {
       if (!this.props.children) {
         throw new Error("There must be at least one Tab");
       }
--- a/devtools/client/shared/components/test/mochitest/chrome.ini
+++ b/devtools/client/shared/components/test/mochitest/chrome.ini
@@ -24,16 +24,17 @@ support-files =
 [test_reps_string.html]
 [test_reps_stylesheet.html]
 [test_reps_text-node.html]
 [test_reps_undefined.html]
 [test_reps_window.html]
 [test_sidebar_toggle.html]
 [test_stack-trace.html]
 [test_tabs_accessibility.html]
+[test_tabs_menu.html]
 [test_tree_01.html]
 [test_tree_02.html]
 [test_tree_03.html]
 [test_tree_04.html]
 [test_tree_05.html]
 [test_tree_06.html]
 [test_tree_07.html]
 [test_tree_08.html]
--- a/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
+++ b/devtools/client/shared/components/test/mochitest/test_reps_grip-array.html
@@ -46,17 +46,17 @@ window.onload = Task.async(function* () 
     const testName = "testBasic";
 
     // Test that correct rep is chosen
     const gripStub = getGripStub("testBasic");
     const renderedRep = shallowRenderComponent(Rep, { object: gripStub });
     is(renderedRep.type, GripArray.rep, `Rep correctly selects ${GripArray.rep.displayName}`);
 
     // Test rendering
-    const defaultOutput = `Array[]`;
+    const defaultOutput = `Array []`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -74,17 +74,17 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMaxProps() {
     // Test array: `[1, "foo", {}]`;
     const testName = "testMaxProps";
 
-    const defaultOutput = `Array[ 1, "foo", Object ]`;
+    const defaultOutput = `Array [ 1, "foo", Object ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -102,46 +102,46 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMoreThanShortMaxProps() {
     // Test array = `["test string"…] //4 items`
     const testName = "testMoreThanShortMaxProps";
 
-    const defaultOutput = `Array[ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, 1 more… ]`;
+    const defaultOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, 1 more… ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
         expectedOutput: `[${maxLength.short + 1}]`,
       },
       {
         mode: "short",
         expectedOutput: defaultOutput,
       },
       {
         mode: "long",
-        expectedOutput: `Array[ ${Array(maxLength.short + 1).fill("\"test string\"").join(", ")} ]`,
+        expectedOutput: `Array [ ${Array(maxLength.short + 1).fill("\"test string\"").join(", ")} ]`,
       }
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testMoreThanLongMaxProps() {
     // Test array = `["test string"…] //301 items`
     const testName = "testMoreThanLongMaxProps";
 
-    const defaultShortOutput = `Array[ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
-    const defaultLongOutput = `Array[ ${Array(maxLength.long).fill("\"test string\"").join(", ")}, 1 more… ]`;
+    const defaultShortOutput = `Array [ ${Array(maxLength.short).fill("\"test string\"").join(", ")}, ${maxLength.long + 1 - maxLength.short} more… ]`;
+    const defaultLongOutput = `Array [ ${Array(maxLength.long).fill("\"test string\"").join(", ")}, 1 more… ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultShortOutput,
       },
       {
         mode: "tiny",
@@ -159,17 +159,17 @@ window.onload = Task.async(function* () 
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testRecursiveArray() {
     // Test array = `let a = []; a = [a]`
     const testName = "testRecursiveArray";
 
-    const defaultOutput = `Array[ [1] ]`;
+    const defaultOutput = `Array [ [1] ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
@@ -186,18 +186,18 @@ window.onload = Task.async(function* () 
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testPreviewLimit() {
     const testName = "testPreviewLimit";
 
-    const shortOutput = `Array[ 0, 1, 2, 8 more… ]`;
-    const defaultOutput = `Array[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 more… ]`;
+    const shortOutput = `Array [ 0, 1, 2, 8 more… ]`;
+    const defaultOutput = `Array [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 1 more… ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: shortOutput,
       },
       {
         mode: "tiny",
@@ -214,17 +214,17 @@ window.onload = Task.async(function* () 
     ];
 
     testRepRenderModes(modeTests, testName, componentUnderTest, getGripStub(testName));
   }
 
   function testNamedNodeMap() {
     const testName = "testNamedNodeMap";
 
-    const defaultOutput = `NamedNodeMap[ class="myclass", cellpadding="7", border="3" ]`;
+    const defaultOutput = `NamedNodeMap [ class="myclass", cellpadding="7", border="3" ]`;
 
     const modeTests = [
       {
         mode: undefined,
         expectedOutput: defaultOutput,
       },
       {
         mode: "tiny",
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/components/test/mochitest/test_tabs_menu.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html class="theme-light">
+<!--
+Test all-tabs menu.
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Tabs component All-tabs menu test</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/variables.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/common.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/themes/light-theme.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabs.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/shared/components/tabs/tabbar.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/side-panel.css">
+  <link rel="stylesheet" type="text/css" href="resource://devtools/client/inspector/components/inspector-tab-panel.css">
+</head>
+<body>
+<pre id="test">
+<script src="head.js" type="application/javascript;version=1.8"></script>
+<script type="application/javascript;version=1.8">
+window.onload = Task.async(function* () {
+  try {
+    const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
+    const React = browserRequire("devtools/client/shared/vendor/react");
+    const Tabbar = React.createFactory(browserRequire("devtools/client/shared/components/tabs/tabbar"));
+
+    // Create container for the TabBar. Set smaller width
+    // to ensure that tabs won't fit and the all-tabs menu
+    // needs to appear.
+    const tabBarBox = document.createElement("div");
+    tabBarBox.style.width = "200px";
+    tabBarBox.style.height = "200px";
+    tabBarBox.style.border = "1px solid lightgray";
+    document.body.appendChild(tabBarBox);
+
+    // Render the tab-bar.
+    const tabbar = Tabbar({
+      showAllTabsMenu: true,
+    });
+
+    const tabbarReact = ReactDOM.render(tabbar, tabBarBox);
+
+    // Test panel.
+    let TabPanel = React.createFactory(React.createClass({
+      render: function () {
+        return React.DOM.div({}, "content");
+      }
+    }));
+
+    // Create a few panels.
+    yield addTabWithPanel(1);
+    yield addTabWithPanel(2);
+    yield addTabWithPanel(3);
+    yield addTabWithPanel(4);
+    yield addTabWithPanel(5);
+
+    // Make sure the all-tabs menu is there.
+    const allTabsMenu = tabBarBox.querySelector(".all-tabs-menu");
+    ok(allTabsMenu, "All-tabs menu must be rendered");
+
+    function addTabWithPanel(tabId) {
+      return setState(tabbarReact, Object.assign({}, tabbarReact.state, {
+        tabs: tabbarReact.state.tabs.concat({id: `${tabId}`,
+          title: `tab-${tabId}`, panel: TabPanel}),
+      }));
+    }
+  } catch(e) {
+    ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
+  } finally {
+    SimpleTest.finish();
+  }
+});
+</script>
+</pre>
+</body>
+</html>
--- a/devtools/client/webconsole/test/browser.ini
+++ b/devtools/client/webconsole/test/browser.ini
@@ -362,16 +362,17 @@ tags = trackingprotection
 [browser_webconsole_output_03.js]
 [browser_webconsole_output_04.js]
 [browser_webconsole_output_05.js]
 [browser_webconsole_output_06.js]
 [browser_webconsole_output_dom_elements_01.js]
 [browser_webconsole_output_dom_elements_02.js]
 skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
 [browser_webconsole_output_dom_elements_03.js]
+skip-if = e10s # Bug 1241019
 [browser_webconsole_output_dom_elements_04.js]
 skip-if = e10s # Bug 1042253 - webconsole e10s tests (Linux debug timeout)
 [browser_webconsole_output_dom_elements_05.js]
 [browser_webconsole_output_events.js]
 [browser_webconsole_output_regexp.js]
 [browser_webconsole_output_table.js]
 [browser_console_variables_view_highlighter.js]
 [browser_webconsole_start_netmon_first.js]
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -52,17 +52,16 @@ import org.mozilla.gecko.home.HomeConfig
 import org.mozilla.gecko.home.HomeConfigPrefsBackend;
 import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.home.HomeScreen;
 import org.mozilla.gecko.home.SearchEngine;
 import org.mozilla.gecko.javaaddons.JavaAddonManager;
-import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.SafeIntent;
 import org.mozilla.gecko.notifications.NotificationClient;
 import org.mozilla.gecko.notifications.ServiceNotificationClient;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
@@ -699,18 +698,19 @@ public class BrowserApp extends GeckoApp
             "Settings:Show",
             "Telemetry:Gather",
             "Updater:Launch");
 
         final GeckoProfile profile = getProfile();
 
         // We want to upload the telemetry core ping as soon after startup as possible. It relies on the
         // Distribution being initialized. If you move this initialization, ensure it plays well with telemetry.
-        final Distribution distribution = Distribution.init(this);
-        distribution.addOnDistributionReadyCallback(new DistributionStoreCallback(this, profile.getName()));
+        final Distribution distribution = Distribution.init(getApplicationContext());
+        distribution.addOnDistributionReadyCallback(
+                new DistributionStoreCallback(getApplicationContext(), profile.getName()));
 
         mSearchEngineManager = new SearchEngineManager(this, distribution);
 
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
         final BrowserDB db = profile.getDB();
         db.setSuggestedSites(suggestedSites);
 
@@ -763,18 +763,16 @@ public class BrowserApp extends GeckoApp
                            @Override
                            public void run() {
                                showUpdaterPermissionSnackbar();
                            }
                        })
                       .run();
         }
 
-        AudioFocusAgent.getInstance().attachToContext(this);
-
         for (final BrowserAppDelegate delegate : delegates) {
             delegate.onCreate(this, savedInstanceState);
         }
 
         // We want to get an understanding of how our user base is spread (bug 1221646).
         final String installerPackageName = getPackageManager().getInstallerPackageName(getPackageName());
         Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.SYSTEM, "installer_" + installerPackageName);
     }
@@ -1402,17 +1400,18 @@ public class BrowserApp extends GeckoApp
         mSearchEngineManager.unregisterListeners();
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Gecko:DelayedStartup",
             "Menu:Open",
             "Menu:Update",
             "LightweightTheme:Update",
             "Search:Keyword",
-            "Prompt:ShowTop");
+            "Prompt:ShowTop",
+            "Video:Play");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "CharEncoding:Data",
             "CharEncoding:State",
             "Download:AndroidDownloadManager",
             "Experiments:GetActive",
             "Experiments:SetOverride",
             "Experiments:ClearOverride",
@@ -2702,17 +2701,18 @@ public class BrowserApp extends GeckoApp
 
         // Show the toolbar before hiding about:home so the
         // onMetricsChanged callback still works.
         if (mDynamicToolbar.isEnabled()) {
             mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
         }
 
         if (mHomeScreen == null) {
-            if (ActivityStream.isEnabled(this)) {
+            if (ActivityStream.isEnabled(this) &&
+                !ActivityStream.isHomePanel()) {
                 final ViewStub asStub = (ViewStub) findViewById(R.id.activity_stream_stub);
                 mHomeScreen = (HomeScreen) asStub.inflate();
             } else {
                 final ViewStub homePagerStub = (ViewStub) findViewById(R.id.home_pager_stub);
                 mHomeScreen = (HomeScreen) homePagerStub.inflate();
 
                 // For now these listeners are HomePager specific. In future we might want
                 // to have a more abstracted data storage, with one Bundle containing all
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1651,17 +1651,17 @@ public abstract class GeckoApp
                 }
             }
         }, 50);
 
         final int updateServiceDelay = 30 * 1000;
         ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
             @Override
             public void run() {
-                UpdateServiceHelper.registerForUpdates(GeckoApp.this);
+                UpdateServiceHelper.registerForUpdates(GeckoAppShell.getApplicationContext());
             }
         }, updateServiceDelay);
 
         if (mIsRestoringActivity) {
             Tab selectedTab = Tabs.getInstance().getSelectedTab();
             if (selectedTab != null) {
                 Tabs.getInstance().notifyListeners(selectedTab, Tabs.TabEvents.SELECTED);
             }
@@ -2210,16 +2210,17 @@ public abstract class GeckoApp
             "Gecko:Ready",
             "Gecko:Exited",
             "Accessibility:Event");
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener)this,
             "Accessibility:Ready",
             "Bookmark:Insert",
             "Contact:Add",
+            "DevToolsAuth:Scan",
             "DOMFullScreen:Start",
             "DOMFullScreen:Stop",
             "Image:SetAs",
             "Locale:Set",
             "Permissions:Data",
             "PrivateBrowsing:Data",
             "RuntimePermissions:Prompt",
             "Session:StatePurged",
@@ -2415,27 +2416,28 @@ public abstract class GeckoApp
                 public void run() {
                     Handler handler = new Handler();
                     handler.postDelayed(new DeferredCleanupTask(), CLEANUP_DEFERRAL_SECONDS * 1000);
                 }
             });
         }
     }
 
-    private class DeferredCleanupTask implements Runnable {
+    private static class DeferredCleanupTask implements Runnable {
         // The cleanup-version setting is recorded to avoid repeating the same
         // tasks on subsequent startups; CURRENT_CLEANUP_VERSION may be updated
         // if we need to do additional cleanup for future Gecko versions.
 
         private static final String CLEANUP_VERSION = "cleanup-version";
         private static final int CURRENT_CLEANUP_VERSION = 1;
 
         @Override
         public void run() {
-            long cleanupVersion = getSharedPreferences().getInt(CLEANUP_VERSION, 0);
+            final Context context = GeckoAppShell.getApplicationContext();
+            long cleanupVersion = GeckoSharedPrefs.forApp(context).getInt(CLEANUP_VERSION, 0);
 
             if (cleanupVersion < 1) {
                 // Reduce device storage footprint by removing .ttf files from
                 // the res/fonts directory: we no longer need to copy our
                 // bundled fonts out of the APK in order to use them.
                 // See https://bugzilla.mozilla.org/show_bug.cgi?id=878674.
                 File dir = new File("res/fonts");
                 if (dir.exists() && dir.isDirectory()) {
@@ -2448,17 +2450,17 @@ public abstract class GeckoApp
                         Log.w(LOGTAG, "unable to delete res/fonts directory (not empty?)");
                     }
                 }
             }
 
             // Additional cleanup needed for future versions would go here
 
             if (cleanupVersion != CURRENT_CLEANUP_VERSION) {
-                SharedPreferences.Editor editor = GeckoApp.this.getSharedPreferences().edit();
+                SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(context).edit();
                 editor.putInt(CLEANUP_VERSION, CURRENT_CLEANUP_VERSION);
                 editor.apply();
             }
         }
     }
 
     @Override
     public void onBackPressed() {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApplication.java
@@ -16,16 +16,17 @@ import com.squareup.leakcanary.RefWatche
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.home.HomePanelsManager;
 import org.mozilla.gecko.lwt.LightweightTheme;
 import org.mozilla.gecko.mdns.MulticastDNSManager;
+import org.mozilla.gecko.media.AudioFocusAgent;
 import org.mozilla.gecko.notifications.NotificationHelper;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.File;
 import java.lang.reflect.Method;
 
@@ -204,16 +205,20 @@ public class GeckoApplication extends Ap
                     }
                 }
             });
         }
 
         if (AppConstants.MOZ_ANDROID_DOWNLOAD_CONTENT_SERVICE) {
             DownloadContentService.startStudy(this);
         }
+
+        GeckoAccessibility.setAccessibilityManagerListeners(this);
+
+        AudioFocusAgent.getInstance().attachToContext(this);
     }
 
     public boolean isApplicationInBackground() {
         return mInBackground;
     }
 
     public LightweightTheme getLightweightTheme() {
         return mLightweightTheme;
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStream.java
@@ -15,9 +15,17 @@ public class ActivityStream {
     public static boolean isEnabled(Context context) {
         if (!AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
             return false;
         }
 
         return GeckoSharedPrefs.forApp(context)
                 .getBoolean(GeckoPreferences.PREFS_ACTIVITY_STREAM, false);
     }
+
+    /**
+     * Query whether we want to display Activity Stream as a Home Panel (within the HomePager),
+     * or as a HomePager replacement.
+     */
+    public static boolean isHomePanel() {
+        return true;
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -40,16 +40,17 @@ public class BrowserContract {
 
     public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins";
     public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY);
 
     public static final String PARAM_PROFILE = "profile";
     public static final String PARAM_PROFILE_PATH = "profilePath";
     public static final String PARAM_LIMIT = "limit";
     public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
+    public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned";
     public static final String PARAM_IS_SYNC = "sync";
     public static final String PARAM_SHOW_DELETED = "show_deleted";
     public static final String PARAM_IS_TEST = "test";
     public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
     public static final String PARAM_INCREMENT_VISITS = "increment_visits";
     public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
     public static final String PARAM_EXPIRE_PRIORITY = "priority";
     public static final String PARAM_DATASET_ID = "dataset_id";
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -16,16 +16,17 @@ import org.mozilla.gecko.distribution.Di
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.graphics.drawable.BitmapDrawable;
+import android.support.v4.content.CursorLoader;
 
 /**
  * Interface for interactions with all databases. If you want an instance
  * that implements this, you should go through GeckoProfile. E.g.,
  * <code>GeckoProfile.get(context).getDB()</code>.
  *
  * GeckoProfile itself will construct an appropriate subclass using
  * a factory that the containing application can set with
@@ -74,16 +75,18 @@ public interface BrowserDB {
     /**
      * @return a cursor over top sites (high-ranking bookmarks and history).
      * Can return <code>null</code>.
      * Returns no more than <code>limit</code> results.
      * Suggested sites will be limited to being within the first <code>suggestedRangeLimit</code> results.
      */
     public abstract Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit);
 
+    public abstract CursorLoader getActivityStreamTopSites(Context context, int limit);
+
     public abstract void updateVisitedHistory(ContentResolver cr, String uri);
 
     public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title);
 
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getAllVisitedHistory(ContentResolver cr);
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -38,16 +38,17 @@ import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.OperationApplicationException;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.MatrixCursor;
+import android.database.MergeCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteCursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
@@ -789,16 +790,79 @@ public class BrowserProvider extends Sha
                 }
             }
         }
 
         debug("Updated " + updated + " rows for URI: " + uri);
         return updated;
     }
 
+    /**
+     * Get topsites by themselves, without the inclusion of pinned sites. Suggested sites
+     * will be appended (if necessary) to the end of the list in order to provide up to PARAM_LIMIT items.
+     */
+    private Cursor getPlainTopSites(final Uri uri) {
+        final SQLiteDatabase db = getReadableDatabase(uri);
+
+        final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+        final int limit;
+        if (limitParam != null) {
+            limit = Integer.parseInt(limitParam);
+        } else {
+            limit = 12;
+        }
+
+        // Filter out: unvisited pages (history_id == -1) pinned (and other special) sites, deleted sites,
+        // and about: pages.
+        final String ignoreForTopSitesWhereClause =
+                "(" + Combined.HISTORY_ID + " IS NOT -1)" +
+                " AND " +
+                Combined.URL + " NOT IN (SELECT " +
+                Bookmarks.URL + " FROM " + TABLE_BOOKMARKS + " WHERE " +
+                DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.PARENT) + " < " + Bookmarks.FIXED_ROOT_ID + " AND " +
+                DBUtils.qualifyColumn(TABLE_BOOKMARKS, Bookmarks.IS_DELETED) + " == 0)" +
+                " AND " +
+                "(" + Combined.URL + " NOT LIKE ?)";
+
+        final String[] ignoreForTopSitesArgs = new String[] {
+                AboutPages.URL_FILTER
+        };
+
+        final Cursor c = db.rawQuery("SELECT " +
+                   Bookmarks._ID + ", " +
+                   Combined.BOOKMARK_ID + ", " +
+                   Combined.HISTORY_ID + ", " +
+                   Bookmarks.URL + ", " +
+                   Bookmarks.TITLE + ", " +
+                   Combined.HISTORY_ID + ", " +
+                   TopSites.TYPE_TOP + " AS " + TopSites.TYPE +
+                   " FROM " + Combined.VIEW_NAME +
+                   " WHERE " + ignoreForTopSitesWhereClause +
+                   " ORDER BY " + BrowserContract.getCombinedFrecencySortOrder(true, false) +
+                   " LIMIT " + limit,
+                ignoreForTopSitesArgs);
+
+        c.setNotificationUri(getContext().getContentResolver(),
+                BrowserContract.AUTHORITY_URI);
+
+        if (c.getCount() == limit) {
+            return c;
+        }
+
+        // If we don't have enough data: get suggested sites too
+        final SuggestedSites suggestedSites = GeckoProfile.get(getContext(), uri.getQueryParameter(BrowserContract.PARAM_PROFILE)).getDB().getSuggestedSites();
+
+        final Cursor suggestedSitesCursor = suggestedSites.get(limit - c.getCount());
+
+        return new MergeCursor(new Cursor[]{
+                c,
+                suggestedSitesCursor
+        });
+    }
+
     private Cursor getTopSites(final Uri uri) {
         // In order to correctly merge the top and pinned sites we:
         //
         // 1. Generate a list of free ids for topsites - this is the positions that are NOT used by pinned sites.
         //    We do this using a subquery with a self-join in order to generate rowids, that allow us to join with
         //    the list of topsites.
         // 2. Generate the list of topsites in order of frecency.
         // 3. Join these, so that each topsite is given its resulting position
@@ -941,20 +1005,20 @@ public class BrowserProvider extends Sha
         final String suggestedLimitClause = " LIMIT MAX(0, (" + suggestedGridLimit + " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ") - (SELECT COUNT(*) " + pinnedSitesFromClause + "))) ";
 
         // Pinned site positions are zero indexed, but we need to get the maximum 1-indexed position.
         // Hence to correctly calculate the largest pinned position (which should be 0 if there are
         // no sites, or 1-6 if we have at least one pinned site), we coalesce the DB position (0-5)
         // with -1 to represent no-sites, which allows us to directly add 1 to obtain the expected value
         // regardless of whether a position was actually retrieved.
         final String blanksLimitClause = " LIMIT MAX(0, " +
-                                         "COALESCE((SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + "), -1) + 1" +
-                                         " - (SELECT COUNT(*) " + pinnedSitesFromClause + ")" +
-                                         " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ")" +
-                                         ")";
+                            "COALESCE((SELECT " + Bookmarks.POSITION + " " + pinnedSitesFromClause + "), -1) + 1" +
+                            " - (SELECT COUNT(*) " + pinnedSitesFromClause + ")" +
+                            " - (SELECT COUNT(*) FROM " + TABLE_TOPSITES + ")" +
+                            ")";
 
         db.beginTransaction();
         try {
             db.execSQL("DROP TABLE IF EXISTS " + TABLE_TOPSITES);
 
             db.execSQL("CREATE TEMP TABLE " + TABLE_TOPSITES + " AS" +
                        " SELECT " +
                        Bookmarks._ID + ", " +
@@ -1072,17 +1136,21 @@ public class BrowserProvider extends Sha
     }
 
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
         final int match = URI_MATCHER.match(uri);
 
         if (match == TOPSITES) {
-            return getTopSites(uri);
+            if (uri.getBooleanQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, false)) {
+                return getPlainTopSites(uri);
+            } else {
+                return getTopSites(uri);
+            }
         }
 
         SQLiteDatabase db = getReadableDatabase(uri);
 
         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
         String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
         String groupBy = null;
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -19,16 +19,17 @@ import java.util.List;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
@@ -50,19 +51,21 @@ import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.MatrixCursor;
 import android.database.MergeCursor;
 import android.graphics.Bitmap;
 import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
+import android.os.SystemClock;
 import android.support.annotation.CheckResult;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
+import android.support.v4.content.CursorLoader;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.util.IOUtils;
 
 import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
 import static org.mozilla.gecko.favicons.LoadFaviconTask.DEFAULT_FAVICON_BUFFER_SIZE;
 
 public class LocalBrowserDB implements BrowserDB {
@@ -93,16 +96,18 @@ public class LocalBrowserDB implements B
 
     private volatile SuggestedSites mSuggestedSites;
 
     // Constants used when importing history data from legacy browser.
     public static String HISTORY_VISITS_DATE = "date";
     public static String HISTORY_VISITS_COUNT = "visits";
     public static String HISTORY_VISITS_URL = "url";
 
+    private static final String TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES = "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS";
+
     private final Uri mBookmarksUriWithProfile;
     private final Uri mParentsUriWithProfile;
     private final Uri mHistoryUriWithProfile;
     private final Uri mHistoryExpireUriWithProfile;
     private final Uri mCombinedUriWithProfile;
     private final Uri mUpdateHistoryUriWithProfile;
     private final Uri mFaviconsUriWithProfile;
     private final Uri mThumbnailsUriWithProfile;
@@ -1820,16 +1825,65 @@ public class LocalBrowserDB implements B
             if (StringUtils.isUserEnteredUrl(url)) {
                 url = StringUtils.decodeUserEnteredUrl(url);
             }
 
             urls.add(url);
         } while (c.moveToNext());
     }
 
+
+    /**
+     * Internal CursorLoader that extends the framework CursorLoader in order to measure
+     * performance for telemetry purposes.
+     */
+    private static final class TelemetrisedCursorLoader extends CursorLoader {
+        final String mHistogramName;
+
+        public TelemetrisedCursorLoader(Context context, Uri uri, String[] projection, String selection,
+                                        String[] selectionArgs, String sortOrder,
+                                        final String histogramName) {
+            super(context, uri, projection, selection, selectionArgs, sortOrder);
+            mHistogramName = histogramName;
+        }
+
+        @Override
+        public Cursor loadInBackground() {
+            final long start = SystemClock.uptimeMillis();
+
+            final Cursor cursor = super.loadInBackground();
+
+            final long end = SystemClock.uptimeMillis();
+            final long took = end - start;
+
+            Telemetry.addToHistogram(mHistogramName, (int) Math.min(took, Integer.MAX_VALUE));
+            return cursor;
+        }
+    }
+
+    public CursorLoader getActivityStreamTopSites(Context context, int limit) {
+        final Uri uri = mTopSitesUriWithProfile.buildUpon()
+                .appendQueryParameter(BrowserContract.PARAM_LIMIT,
+                        String.valueOf(limit))
+                .appendQueryParameter(BrowserContract.PARAM_TOPSITES_DISABLE_PINNED, Boolean.TRUE.toString())
+                .build();
+
+        return new TelemetrisedCursorLoader(context,
+                uri,
+                new String[]{ Combined._ID,
+                        Combined.URL,
+                        Combined.TITLE,
+                        Combined.BOOKMARK_ID,
+                        Combined.HISTORY_ID },
+                null,
+                null,
+                null,
+                TELEMETRY_HISTOGRAM_ACITIVITY_STREAM_TOPSITES);
+    }
+
     @Override
     public Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit) {
         final Uri uri = mTopSitesUriWithProfile.buildUpon()
                 .appendQueryParameter(BrowserContract.PARAM_LIMIT,
                         String.valueOf(limit))
                 .appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT,
                         String.valueOf(suggestedRangeLimit))
                 .build();
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -21,16 +21,17 @@ import org.mozilla.gecko.feeds.subscript
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.graphics.drawable.BitmapDrawable;
+import android.support.v4.content.CursorLoader;
 
 class StubSearches implements Searches {
     public StubSearches() {
     }
 
     public void insert(ContentResolver cr, String query) {
     }
 }
@@ -370,16 +371,20 @@ public class StubBrowserDB implements Br
     public int getSuggestedBackgroundColorForUrl(String url) {
         return 0;
     }
 
     public Cursor getTopSites(ContentResolver cr, int suggestedRangeLimit, int limit) {
         return null;
     }
 
+    public CursorLoader getActivityStreamTopSites(Context context, int limit) {
+        return null;
+    }
+
     public static Factory getFactory() {
         return new Factory() {
             @Override
             public BrowserDB get(String profileName, File profileDir) {
                 return new StubBrowserDB(profileName);
             }
         };
     }
--- a/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/SuggestedSites.java
@@ -75,16 +75,17 @@ public class SuggestedSites {
 
     // File in profile dir with the list of suggested sites.
     private static final String FILENAME = "suggestedsites.json";
 
     private static final String[] COLUMNS = new String[] {
         BrowserContract.SuggestedSites._ID,
         BrowserContract.SuggestedSites.URL,
         BrowserContract.SuggestedSites.TITLE,
+        BrowserContract.Combined.HISTORY_ID
     };
 
     private static final String JSON_KEY_URL = "url";
     private static final String JSON_KEY_TITLE = "title";
     private static final String JSON_KEY_IMAGE_URL = "imageurl";
     private static final String JSON_KEY_BG_COLOR = "bgcolor";
     private static final String JSON_KEY_RESTRICTED = "restricted";
 
@@ -473,32 +474,41 @@ public class SuggestedSites {
         }
 
         excludeUrls = includeBlacklist(excludeUrls);
 
         final int sitesCount = cachedSites.size();
         Log.d(LOGTAG, "Number of suggested sites: " + sitesCount);
 
         final int maxCount = Math.min(limit, sitesCount);
+        // History IDS: real history is positive, -1 is no history id in the combined table
+        // hence we can start at -2 for suggested sites
+        int id = -1;
         for (Site site : cachedSites.values()) {
+            // Decrement ID here: this ensure we have a consistent ID to URL mapping, even if items
+            // are removed. If we instead decremented at the point of insertion we'd end up with
+            // ID conflicts when a suggested site is removed. (note that cachedSites does not change
+            // while we're already showing topsites)
+            --id;
             if (cursor.getCount() == maxCount) {
                 break;
             }
 
             if (excludeUrls != null && excludeUrls.contains(site.url)) {
                 continue;
             }
 
             final boolean restrictedProfile =  Restrictions.isRestrictedProfile(context);
 
             if (restrictedProfile == site.restricted) {
                 final RowBuilder row = cursor.newRow();
-                row.add(-1);
+                row.add(id);
                 row.add(site.url);
                 row.add(site.title);
+                row.add(id);
             }
         }
 
         cursor.setNotificationUri(context.getContentResolver(),
                                   BrowserContract.SuggestedSites.CONTENT_URI);
 
         return cursor;
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeAdapter.java
@@ -1,17 +1,19 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
+import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.home.HomeConfig.PanelConfig;
 import org.mozilla.gecko.home.HomeConfig.PanelType;
+import org.mozilla.gecko.home.activitystream.ActivityStreamHomeFragment;
 
 import android.content.Context;
 import android.os.Bundle;
 import android.support.v4.app.Fragment;
 import android.support.v4.app.FragmentManager;
 import android.support.v4.app.FragmentStatePagerAdapter;
 import android.view.ViewGroup;
 
@@ -59,17 +61,17 @@ public class HomeAdapter extends Fragmen
     @Override
     public int getCount() {
         return mPanelInfos.size();
     }
 
     @Override
     public Fragment getItem(int position) {
         PanelInfo info = mPanelInfos.get(position);
-        return Fragment.instantiate(mContext, info.getClassName(), info.getArgs());
+        return Fragment.instantiate(mContext, info.getClassName(mContext), info.getArgs());
     }
 
     @Override
     public CharSequence getPageTitle(int position) {
         if (mPanelInfos.size() > 0) {
             PanelInfo info = mPanelInfos.get(position);
             return info.getTitle().toUpperCase();
         }
@@ -188,18 +190,26 @@ public class HomeAdapter extends Fragmen
         public String getId() {
             return mPanelConfig.getId();
         }
 
         public String getTitle() {
             return mPanelConfig.getTitle();
         }
 
-        public String getClassName() {
+        public String getClassName(Context context) {
             final PanelType type = mPanelConfig.getType();
+
+            // Override top_sites with ActivityStream panel when enabled
+            // PanelType.toString() returns the panel id
+            if (type.toString() == "top_sites" &&
+                ActivityStream.isEnabled(context) &&
+                ActivityStream.isHomePanel()) {
+                return ActivityStreamHomeFragment.class.getName();
+            }
             return type.getPanelClass().getName();
         }
 
         public Bundle getArgs() {
             final Bundle args = new Bundle();
 
             args.putBoolean(HomePager.CAN_LOAD_ARG, mCanLoadHint);
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ImageLoader.java
@@ -51,17 +51,17 @@ public class ImageLoader {
     private static Picasso instance;
     private static LruCache lrucache;
 
     public static synchronized Picasso with(Context context) {
         if (instance == null) {
             lrucache = new LruCache(context);
             Picasso.Builder builder = new Picasso.Builder(context).memoryCache(lrucache);
 
-            final Distribution distribution = Distribution.getInstance(context);
+            final Distribution distribution = Distribution.getInstance(context.getApplicationContext());
             builder.downloader(new ImageDownloader(context, distribution));
             instance = builder.build();
         }
 
         return instance;
     }
 
     public static synchronized void clearLruCache() {
--- a/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/TwoLinePageRow.java
@@ -1,17 +1,15 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.home;
 
-import java.lang.ref.WeakReference;
-
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.distribution.PartnerBookmarksProviderProxy;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.Tab;
@@ -19,20 +17,18 @@ import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.widget.FaviconView;
 
 import android.content.Context;
 import android.database.Cursor;
-import android.graphics.Bitmap;
 import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.Log;
 import android.view.Gravity;
 import android.view.LayoutInflater;
 import android.widget.ImageView;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class TwoLinePageRow extends LinearLayout
@@ -46,46 +42,16 @@ public class TwoLinePageRow extends Line
 
     private int mSwitchToTabIconId;
 
     private final FaviconView mFavicon;
 
     private boolean mShowIcons;
     private int mLoadFaviconJobId = Favicons.NOT_LOADING;
 
-    // Only holds a reference to the FaviconView itself, so if the row gets
-    // discarded while a task is outstanding, we'll leak less memory.
-    private static class UpdateViewFaviconLoadedListener implements OnFaviconLoadedListener {
-        private final WeakReference<FaviconView> view;
-        public UpdateViewFaviconLoadedListener(FaviconView view) {
-            this.view = new WeakReference<FaviconView>(view);
-        }
-
-        /**
-         * Update this row's favicon.
-         * <p>
-         * This method is always invoked on the UI thread.
-         */
-        @Override
-        public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
-            FaviconView v = view.get();
-            if (v == null) {
-                // Guess we stuck around after the TwoLinePageRow went away.
-                return;
-            }
-
-            if (favicon == null) {
-                v.showDefaultFavicon(url);
-                return;
-            }
-
-            v.updateImage(favicon, faviconURL);
-        }
-    }
-
     // Listener for handling Favicon loads.
     private final OnFaviconLoadedListener mFaviconListener;
 
     // The URL for the page corresponding to this view.
     private String mPageUrl;
 
     private boolean mHasReaderCacheItem;
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/UpdateViewFaviconLoadedListener.java
@@ -0,0 +1,42 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home;
+
+import android.graphics.Bitmap;
+
+import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
+import org.mozilla.gecko.widget.FaviconView;
+
+import java.lang.ref.WeakReference;
+
+// Only holds a reference to the FaviconView itself, so if the row gets
+// discarded while a task is outstanding, we'll leak less memory.
+public class UpdateViewFaviconLoadedListener implements OnFaviconLoadedListener {
+    private final WeakReference<FaviconView> view;
+    public UpdateViewFaviconLoadedListener(FaviconView view) {
+        this.view = new WeakReference<FaviconView>(view);
+    }
+
+    /**
+     * Update this row's favicon.
+     * <p>
+     * This method is always invoked on the UI thread.
+     */
+    @Override
+    public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
+        FaviconView v = view.get();
+        if (v == null) {
+            // Guess we stuck around after the TwoLinePageRow went away.
+            return;
+        }
+
+        if (favicon == null) {
+            v.showDefaultFavicon(url);
+            return;
+        }
+
+        v.updateImage(favicon, faviconURL);
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
@@ -17,75 +17,49 @@ import android.widget.FrameLayout;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.home.HomeBanner;
 import org.mozilla.gecko.home.HomeFragment;
 import org.mozilla.gecko.home.HomeScreen;
 import org.mozilla.gecko.home.SimpleCursorLoader;
+import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
 
-public class ActivityStream extends FrameLayout implements HomeScreen {
+public class ActivityStream extends FrameLayout {
     private StreamRecyclerAdapter adapter;
 
+    private static final int LOADER_ID_HIGHLIGHTS = 0;
+    private static final int LOADER_ID_TOPSITES = 1;
+
     public ActivityStream(Context context, AttributeSet attrs) {
         super(context, attrs);
 
         inflate(context, R.layout.as_content, this);
     }
 
-    @Override
-    public boolean isVisible() {
-        // This is dependent on the loading state - currently we're a dumb panel so we're always
-        // "visible"
-        return true;
-    }
-
-    @Override
-    public void onToolbarFocusChange(boolean hasFocus) {
-        // We don't care: this is HomePager specific
-    }
-
-    @Override
-    public void showPanel(String panelId, Bundle restoreData) {
-        // We could use this to restore Panel data. In practice this isn't likely to be relevant for
-        // AS and can be ignore for now.
-    }
-
-    @Override
-    public void setOnPanelChangeListener(OnPanelChangeListener listener) {
-        // As with showPanel: not relevant yet, could be used for persistence (scroll position?)
-    }
-
-    @Override
-    public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
-        // See setOnPanelChangeListener
-    }
-
-    @Override
-    public void setBanner(HomeBanner banner) {
-        // TODO: we should probably implement this to show snippets.
-    }
-
-    @Override
-    public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData,
-                     PropertyAnimator animator) {
+    public void load(LoaderManager lm) {
         // Signal to load data from storage as needed, compare with HomePager
         RecyclerView rv = (RecyclerView) findViewById(R.id.activity_stream_main_recyclerview);
 
-        adapter = new StreamRecyclerAdapter();
+        // TODO: we need to retrieve BrowserApp and pass it in as onUrlOpenListener. That will
+        // be simpler once we're a HomeFragment, but isn't so simple while we're still a View.
+        adapter = new StreamRecyclerAdapter(lm, null);
         rv.setAdapter(adapter);
         rv.setLayoutManager(new LinearLayoutManager(getContext()));
         rv.setHasFixedSize(true);
 
-        lm.initLoader(0, null, new CursorLoaderCallbacks());
+        CursorLoaderCallbacks callbacks = new CursorLoaderCallbacks();
+        lm.initLoader(LOADER_ID_HIGHLIGHTS, null, callbacks);
+        lm.initLoader(LOADER_ID_TOPSITES, null, callbacks);
     }
 
-    @Override
     public void unload() {
+        adapter.swapHighlightsCursor(null);
+        adapter.swapTopSitesCursor(null);
         // Signal to clear data that has been loaded, compare with HomePager
     }
 
     /**
      * This is a temporary cursor loader. We'll probably need a completely new query for AS,
      * at that time we can switch to the new CursorLoader, as opposed to using our outdated
      * SimpleCursorLoader.
      */
@@ -100,22 +74,37 @@ public class ActivityStream extends Fram
             return GeckoProfile.get(context).getDB()
                     .getRecentHistory(context.getContentResolver(), 10);
         }
     }
 
     private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            return new HistoryLoader(getContext());
+            if (id == LOADER_ID_HIGHLIGHTS) {
+                return new HistoryLoader(getContext());
+            } else if (id == LOADER_ID_TOPSITES) {
+                return GeckoProfile.get(getContext()).getDB().getActivityStreamTopSites(getContext(),
+                        TopSitesPagerAdapter.TOTAL_ITEMS);
+            } else {
+                throw new IllegalArgumentException("Can't handle loader id " + id);
+            }
         }
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-            adapter.swapCursor(data);
+            if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
+                adapter.swapHighlightsCursor(data);
+            } else if (loader.getId() == LOADER_ID_TOPSITES) {
+                adapter.swapTopSitesCursor(data);
+            }
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
-            adapter.swapCursor(null);
+            if (loader.getId() == LOADER_ID_HIGHLIGHTS) {
+                adapter.swapHighlightsCursor(null);
+            } else if (loader.getId() == LOADER_ID_TOPSITES) {
+                adapter.swapTopSitesCursor(null);
+            }
         }
     }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeFragment.java
@@ -0,0 +1,36 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.home.HomeFragment;
+
+/**
+ * Simple wrapper around the ActivityStream view that allows embedding as a HomePager panel.
+ */
+public class ActivityStreamHomeFragment
+        extends HomeFragment {
+    private ActivityStream activityStream;
+
+    @Override
+    protected void load() {
+        activityStream.load(getLoaderManager());
+    }
+
+    @Nullable
+    @Override
+    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+                             @Nullable Bundle savedInstanceState) {
+        activityStream = (ActivityStream) inflater.inflate(R.layout.activity_stream, container, false);
+
+        return activityStream;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStreamHomeScreen.java
@@ -0,0 +1,73 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.LoaderManager;
+import android.util.AttributeSet;
+
+import org.mozilla.gecko.animation.PropertyAnimator;
+import org.mozilla.gecko.home.HomeBanner;
+import org.mozilla.gecko.home.HomeFragment;
+import org.mozilla.gecko.home.HomeScreen;
+
+/**
+ * HomeScreen implementation that displays ActivityStream.
+ */
+public class ActivityStreamHomeScreen
+        extends ActivityStream
+        implements HomeScreen {
+
+    private boolean visible = false;
+
+    public ActivityStreamHomeScreen(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public boolean isVisible() {
+        return visible;
+    }
+
+    @Override
+    public void onToolbarFocusChange(boolean hasFocus) {
+
+    }
+
+    @Override
+    public void showPanel(String panelId, Bundle restoreData) {
+
+    }
+
+    @Override
+    public void setOnPanelChangeListener(OnPanelChangeListener listener) {
+
+    }
+
+    @Override
+    public void setPanelStateChangeListener(HomeFragment.PanelStateChangeListener listener) {
+
+    }
+
+    @Override
+    public void setBanner(HomeBanner banner) {
+
+    }
+
+    @Override
+    public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData,
+                     PropertyAnimator animator) {
+        super.load(lm);
+        visible = true;
+    }
+
+    @Override
+    public void unload() {
+        super.unload();
+        visible = false;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
@@ -1,38 +1,54 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.gecko.home.activitystream;
 
 import android.database.Cursor;
+import android.support.v4.view.ViewPager;
 import android.support.v7.widget.RecyclerView;
 import android.text.format.DateUtils;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator;
+import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
 
 public abstract class StreamItem extends RecyclerView.ViewHolder {
     public StreamItem(View itemView) {
         super(itemView);
     }
 
     public void bind(Cursor cursor) {
         throw new IllegalStateException("Cannot bind " + this.getClass().getSimpleName());
     }
 
     public static class TopPanel extends StreamItem {
         public static final int LAYOUT_ID = R.layout.activity_stream_main_toppanel;
+        private final ViewPager topSitesPager;
 
-        public TopPanel(View itemView) {
+        public TopPanel(View itemView, HomePager.OnUrlOpenListener onUrlOpenListener) {
             super(itemView);
+
+            topSitesPager = (ViewPager) itemView.findViewById(R.id.topsites_pager);
+            topSitesPager.setAdapter(new TopSitesPagerAdapter(itemView.getContext(), onUrlOpenListener));
+
+            CirclePageIndicator indicator = (CirclePageIndicator) itemView.findViewById(R.id.topsites_indicator);
+            indicator.setViewPager(topSitesPager);
+        }
+
+        @Override
+        public void bind(Cursor cursor) {
+            ((TopSitesPagerAdapter) topSitesPager.getAdapter()).swapCursor(cursor);
         }
     }
 
     public static class BottomPanel extends StreamItem {
         public static final int LAYOUT_ID = R.layout.activity_stream_main_bottompanel;
 
         public BottomPanel(View itemView) {
             super(itemView);
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
@@ -1,26 +1,39 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 package org.mozilla.gecko.home.activitystream;
 
 import android.database.Cursor;
+import android.support.v4.app.LoaderManager;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 
+import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.activitystream.StreamItem.BottomPanel;
 import org.mozilla.gecko.home.activitystream.StreamItem.CompactItem;
 import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem;
 import org.mozilla.gecko.home.activitystream.StreamItem.TopPanel;
 
+import java.lang.ref.WeakReference;
+
 public class StreamRecyclerAdapter extends RecyclerView.Adapter<StreamItem> {
     private Cursor highlightsCursor;
+    private Cursor topSitesCursor;
+
+    private final WeakReference<LoaderManager> loaderManagerWeakReference;
+    private final HomePager.OnUrlOpenListener onUrlOpenListener;
+
+    StreamRecyclerAdapter(LoaderManager lm, HomePager.OnUrlOpenListener onUrlOpenListener) {
+        loaderManagerWeakReference = new WeakReference<>(lm);
+        this.onUrlOpenListener = onUrlOpenListener;
+    }
 
     @Override
     public int getItemViewType(int position) {
         if (position == 0) {
             return TopPanel.LAYOUT_ID;
         } else if (position == getItemCount() - 1) {
             return BottomPanel.LAYOUT_ID;
         } else {
@@ -33,17 +46,17 @@ public class StreamRecyclerAdapter exten
         }
     }
 
     @Override
     public StreamItem onCreateViewHolder(ViewGroup parent, final int type) {
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
 
         if (type == TopPanel.LAYOUT_ID) {
-            return new TopPanel(inflater.inflate(type, parent, false));
+            return new TopPanel(inflater.inflate(type, parent, false), onUrlOpenListener);
         } else if (type == BottomPanel.LAYOUT_ID) {
                 return new BottomPanel(inflater.inflate(type, parent, false));
         } else if (type == CompactItem.LAYOUT_ID) {
             return new CompactItem(inflater.inflate(type, parent, false));
         } else if (type == HighlightItem.LAYOUT_ID) {
             return new HighlightItem(inflater.inflate(type, parent, false));
         } else {
             throw new IllegalStateException("Missing inflation for ViewType " + type);
@@ -66,29 +79,37 @@ public class StreamRecyclerAdapter exten
 
         if (type == CompactItem.LAYOUT_ID ||
             type == HighlightItem.LAYOUT_ID) {
 
             final int cursorPosition = translatePositionToCursor(position);
 
             highlightsCursor.moveToPosition(cursorPosition);
             holder.bind(highlightsCursor);
+        } else if (type == TopPanel.LAYOUT_ID) {
+            holder.bind(topSitesCursor);
         }
     }
 
     @Override
     public int getItemCount() {
         final int highlightsCount;
         if (highlightsCursor != null) {
             highlightsCount = highlightsCursor.getCount();
         } else {
             highlightsCount = 0;
         }
 
         return 2 + highlightsCount;
     }
 
-    public void swapCursor(Cursor cursor) {
+    public void swapHighlightsCursor(Cursor cursor) {
         highlightsCursor = cursor;
 
         notifyDataSetChanged();
     }
+
+    public void swapTopSitesCursor(Cursor cursor) {
+        this.topSitesCursor = cursor;
+
+        notifyItemChanged(0);
+    }
 }
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/TopSitesRecyclerAdapter.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.mozilla.gecko.home.activitystream;
-
-import android.content.Context;
-import android.support.v7.widget.RecyclerView;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.mozilla.gecko.R;
-
-class TopSitesRecyclerAdapter extends RecyclerView.Adapter<TopSitesRecyclerAdapter.ViewHolder> {
-
-    private final Context context;
-    private final String[] items = {
-            "FastMail",
-            "Firefox",
-            "Mozilla",
-            "Hacker News",
-            "Github",
-            "YouTube",
-            "Google Maps"
-    };
-
-    TopSitesRecyclerAdapter(Context context) {
-        this.context = context;
-    }
-
-    @Override
-    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        View v = LayoutInflater
-                .from(context)
-                .inflate(R.layout.activity_stream_card_top_sites_item, parent, false);
-        return new ViewHolder(v);
-    }
-
-    @Override
-    public void onBindViewHolder(ViewHolder holder, int position) {
-        holder.vLabel.setText(items[position]);
-    }
-
-    @Override
-    public int getItemCount() {
-        return items.length;
-    }
-
-    static class ViewHolder extends RecyclerView.ViewHolder {
-        TextView vLabel;
-        ViewHolder(View itemView) {
-            super(itemView);
-            vLabel = (TextView) itemView.findViewById(R.id.card_row_label);
-        }
-    }
-}
-
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/CirclePageIndicator.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2011 Patrik Akerfeldt
+ * Copyright (C) 2011 Jake Wharton
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mozilla.gecko.home.activitystream.topsites;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.ViewConfigurationCompat;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+
+import org.mozilla.gecko.R;
+
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
+import static android.widget.LinearLayout.HORIZONTAL;
+import static android.widget.LinearLayout.VERTICAL;
+
+/**
+ * Draws circles (one for each view). The current view position is filled and
+ * others are only stroked.
+ *
+ * This file was imported from Jake Wharton's ViewPagerIndicator library:
+ * https://github.com/JakeWharton/ViewPagerIndicator
+ * It was modified to not extend the PageIndicator interface (as we only use one single Indicator)
+ * implementation, and has had some minor appearance related modifications added alter.
+ */
+public class CirclePageIndicator
+        extends View
+        implements ViewPager.OnPageChangeListener {
+
+    /**
+     * Separation between circles, as a factor of the circle radius. By default CirclePageIndicator
+     * shipped with a separation factor of 3, however we want to be able to tweak this for
+     * ActivityStream.
+     *
+     * If/when we reuse this indicator elsewhere, this should probably become a configurable property.
+     */
+    private static final int SEPARATION_FACTOR = 7;
+
+    private static final int INVALID_POINTER = -1;
+
+    private float mRadius;
+    private final Paint mPaintPageFill = new Paint(ANTI_ALIAS_FLAG);
+    private final Paint mPaintStroke = new Paint(ANTI_ALIAS_FLAG);
+    private final Paint mPaintFill = new Paint(ANTI_ALIAS_FLAG);
+    private ViewPager mViewPager;
+    private ViewPager.OnPageChangeListener mListener;
+    private int mCurrentPage;
+    private int mSnapPage;
+    private float mPageOffset;
+    private int mScrollState;
+    private int mOrientation;
+    private boolean mCentered;
+    private boolean mSnap;
+
+    private int mTouchSlop;
+    private float mLastMotionX = -1;
+    private int mActivePointerId = INVALID_POINTER;
+    private boolean mIsDragging;
+
+
+    public CirclePageIndicator(Context context) {
+        this(context, null);
+    }
+
+    public CirclePageIndicator(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.vpiCirclePageIndicatorStyle);
+    }
+
+    public CirclePageIndicator(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        if (isInEditMode()) return;
+
+        //Load defaults from resources
+        final Resources res = getResources();
+        final int defaultPageColor = res.getColor(R.color.default_circle_indicator_page_color);
+        final int defaultFillColor = res.getColor(R.color.default_circle_indicator_fill_color);
+        final int defaultOrientation = res.getInteger(R.integer.default_circle_indicator_orientation);
+        final int defaultStrokeColor = res.getColor(R.color.default_circle_indicator_stroke_color);
+        final float defaultStrokeWidth = res.getDimension(R.dimen.default_circle_indicator_stroke_width);
+        final float defaultRadius = res.getDimension(R.dimen.default_circle_indicator_radius);
+        final boolean defaultCentered = res.getBoolean(R.bool.default_circle_indicator_centered);
+        final boolean defaultSnap = res.getBoolean(R.bool.default_circle_indicator_snap);
+
+        //Retrieve styles attributes
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CirclePageIndicator, defStyle, 0);
+
+        mCentered = a.getBoolean(R.styleable.CirclePageIndicator_centered, defaultCentered);
+        mOrientation = a.getInt(R.styleable.CirclePageIndicator_android_orientation, defaultOrientation);
+        mPaintPageFill.setStyle(Style.FILL);
+        mPaintPageFill.setColor(a.getColor(R.styleable.CirclePageIndicator_pageColor, defaultPageColor));
+        mPaintStroke.setStyle(Style.STROKE);
+        mPaintStroke.setColor(a.getColor(R.styleable.CirclePageIndicator_strokeColor, defaultStrokeColor));
+        mPaintStroke.setStrokeWidth(a.getDimension(R.styleable.CirclePageIndicator_strokeWidth, defaultStrokeWidth));
+        mPaintFill.setStyle(Style.FILL);
+        mPaintFill.setColor(a.getColor(R.styleable.CirclePageIndicator_fillColor, defaultFillColor));
+        mRadius = a.getDimension(R.styleable.CirclePageIndicator_radius, defaultRadius);
+        mSnap = a.getBoolean(R.styleable.CirclePageIndicator_snap, defaultSnap);
+
+        Drawable background = a.getDrawable(R.styleable.CirclePageIndicator_android_background);
+        if (background != null) {
+          setBackgroundDrawable(background);
+        }
+
+        a.recycle();
+
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
+    }
+
+
+    public void setCentered(boolean centered) {
+        mCentered = centered;
+        invalidate();
+    }
+
+    public boolean isCentered() {
+        return mCentered;
+    }
+
+    public void setPageColor(int pageColor) {
+        mPaintPageFill.setColor(pageColor);
+        invalidate();
+    }
+
+    public int getPageColor() {
+        return mPaintPageFill.getColor();
+    }
+
+    public void setFillColor(int fillColor) {
+        mPaintFill.setColor(fillColor);
+        invalidate();
+    }
+
+    public int getFillColor() {
+        return mPaintFill.getColor();
+    }
+
+    public void setOrientation(int orientation) {
+        switch (orientation) {
+            case HORIZONTAL:
+            case VERTICAL:
+                mOrientation = orientation;
+                requestLayout();
+                break;
+
+            default:
+                throw new IllegalArgumentException("Orientation must be either HORIZONTAL or VERTICAL.");
+        }
+    }
+
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    public void setStrokeColor(int strokeColor) {
+        mPaintStroke.setColor(strokeColor);
+        invalidate();
+    }
+
+    public int getStrokeColor() {
+        return mPaintStroke.getColor();
+    }
+
+    public void setStrokeWidth(float strokeWidth) {
+        mPaintStroke.setStrokeWidth(strokeWidth);
+        invalidate();
+    }
+
+    public float getStrokeWidth() {
+        return mPaintStroke.getStrokeWidth();
+    }
+
+    public void setRadius(float radius) {
+        mRadius = radius;
+        invalidate();
+    }
+
+    public float getRadius() {
+        return mRadius;
+    }
+
+    public void setSnap(boolean snap) {
+        mSnap = snap;
+        invalidate();
+    }
+
+    public boolean isSnap() {
+        return mSnap;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mViewPager == null) {
+            return;
+        }
+        final int count = mViewPager.getAdapter().getCount();
+        if (count == 0) {
+            return;
+        }
+
+        if (mCurrentPage >= count) {
+            setCurrentItem(count - 1);
+            return;
+        }
+
+        int longSize;
+        int longPaddingBefore;
+        int longPaddingAfter;
+        int shortPaddingBefore;
+        if (mOrientation == HORIZONTAL) {
+            longSize = getWidth();
+            longPaddingBefore = getPaddingLeft();
+            longPaddingAfter = getPaddingRight();
+            shortPaddingBefore = getPaddingTop();
+        } else {
+            longSize = getHeight();
+            longPaddingBefore = getPaddingTop();
+            longPaddingAfter = getPaddingBottom();
+            shortPaddingBefore = getPaddingLeft();
+        }
+
+        final float threeRadius = mRadius * SEPARATION_FACTOR;
+        final float shortOffset = shortPaddingBefore + mRadius;
+        float longOffset = longPaddingBefore + mRadius;
+        if (mCentered) {
+            longOffset += ((longSize - longPaddingBefore - longPaddingAfter) / 2.0f) - ((count * threeRadius) / 2.0f);
+        }
+
+        float dX;
+        float dY;
+
+        float pageFillRadius = mRadius;
+        if (mPaintStroke.getStrokeWidth() > 0) {
+            pageFillRadius -= mPaintStroke.getStrokeWidth() / 2.0f;
+        }
+
+        //Draw stroked circles
+        for (int iLoop = 0; iLoop < count; iLoop++) {
+            float drawLong = longOffset + (iLoop * threeRadius);
+            if (mOrientation == HORIZONTAL) {
+                dX = drawLong;
+                dY = shortOffset;
+            } else {
+                dX = shortOffset;
+                dY = drawLong;
+            }
+            // Only paint fill if not completely transparent
+            if (mPaintPageFill.getAlpha() > 0) {
+                canvas.drawCircle(dX, dY, pageFillRadius, mPaintPageFill);
+            }
+
+            // Only paint stroke if a stroke width was non-zero
+            if (pageFillRadius != mRadius) {
+                canvas.drawCircle(dX, dY, mRadius, mPaintStroke);
+            }
+        }
+
+        //Draw the filled circle according to the current scroll
+        float cx = (mSnap ? mSnapPage : mCurrentPage) * threeRadius;
+        if (!mSnap) {
+            cx += mPageOffset * threeRadius;
+        }
+        if (mOrientation == HORIZONTAL) {
+            dX = longOffset + cx;
+            dY = shortOffset;
+        } else {
+            dX = shortOffset;
+            dY = longOffset + cx;
+        }
+        canvas.drawCircle(dX, dY, mRadius, mPaintFill);
+    }
+
+    public boolean onTouchEvent(android.view.MotionEvent ev) {
+        if (super.onTouchEvent(ev)) {
+            return true;
+        }
+        if ((mViewPager == null) || (mViewPager.getAdapter().getCount() == 0)) {
+            return false;
+        }
+
+        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+                mLastMotionX = ev.getX();
+                break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+                final float x = MotionEventCompat.getX(ev, activePointerIndex);
+                final float deltaX = x - mLastMotionX;
+
+                if (!mIsDragging) {
+                    if (Math.abs(deltaX) > mTouchSlop) {
+                        mIsDragging = true;
+                    }
+                }
+
+                if (mIsDragging) {
+                    mLastMotionX = x;
+                    if (mViewPager.isFakeDragging() || mViewPager.beginFakeDrag()) {
+                        mViewPager.fakeDragBy(deltaX);
+                    }
+                }
+
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                if (!mIsDragging) {
+                    final int count = mViewPager.getAdapter().getCount();
+                    final int width = getWidth();
+                    final float halfWidth = width / 2f;
+                    final float sixthWidth = width / 6f;
+
+                    if ((mCurrentPage > 0) && (ev.getX() < halfWidth - sixthWidth)) {
+                        if (action != MotionEvent.ACTION_CANCEL) {
+                            mViewPager.setCurrentItem(mCurrentPage - 1);
+                        }
+                        return true;
+                    } else if ((mCurrentPage < count - 1) && (ev.getX() > halfWidth + sixthWidth)) {
+                        if (action != MotionEvent.ACTION_CANCEL) {
+                            mViewPager.setCurrentItem(mCurrentPage + 1);
+                        }
+                        return true;
+                    }
+                }
+
+                mIsDragging = false;
+                mActivePointerId = INVALID_POINTER;
+                if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag();
+                break;
+
+            case MotionEventCompat.ACTION_POINTER_DOWN: {
+                final int index = MotionEventCompat.getActionIndex(ev);
+                mLastMotionX = MotionEventCompat.getX(ev, index);
+                mActivePointerId = MotionEventCompat.getPointerId(ev, index);
+                break;
+            }
+
+            case MotionEventCompat.ACTION_POINTER_UP:
+                final int pointerIndex = MotionEventCompat.getActionIndex(ev);
+                final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
+                if (pointerId == mActivePointerId) {
+                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+                    mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
+                }
+                mLastMotionX = MotionEventCompat.getX(ev, MotionEventCompat.findPointerIndex(ev, mActivePointerId));
+                break;
+        }
+
+        return true;
+    }
+
+    public void setViewPager(ViewPager view) {
+        if (mViewPager == view) {
+            return;
+        }
+        if (mViewPager != null) {
+            mViewPager.setOnPageChangeListener(null);
+        }
+        if (view.getAdapter() == null) {
+            throw new IllegalStateException("ViewPager does not have adapter instance.");
+        }
+        mViewPager = view;
+        mViewPager.setOnPageChangeListener(this);
+        invalidate();
+    }
+
+    public void setViewPager(ViewPager view, int initialPosition) {
+        setViewPager(view);
+        setCurrentItem(initialPosition);
+    }
+
+    public void setCurrentItem(int item) {
+        if (mViewPager == null) {
+            throw new IllegalStateException("ViewPager has not been bound.");
+        }
+        mViewPager.setCurrentItem(item);
+        mCurrentPage = item;
+        invalidate();
+    }
+
+    public void notifyDataSetChanged() {
+        invalidate();
+    }
+
+    @Override
+    public void onPageScrollStateChanged(int state) {
+        mScrollState = state;
+
+        if (mListener != null) {
+            mListener.onPageScrollStateChanged(state);
+        }
+    }
+
+    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        mCurrentPage = position;
+        mPageOffset = positionOffset;
+        invalidate();
+
+        if (mListener != null) {
+            mListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
+        }
+    }
+
+    @Override
+    public void onPageSelected(int position) {
+        if (mSnap || mScrollState == ViewPager.SCROLL_STATE_IDLE) {
+            mCurrentPage = position;
+            mSnapPage = position;
+            invalidate();
+        }
+
+        if (mListener != null) {
+            mListener.onPageSelected(position);
+        }
+    }
+
+    public void setOnPageChangeListener(ViewPager.OnPageChangeListener listener) {
+        mListener = listener;
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see android.view.View#onMeasure(int, int)
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mOrientation == HORIZONTAL) {
+            setMeasuredDimension(measureLong(widthMeasureSpec), measureShort(heightMeasureSpec));
+        } else {
+            setMeasuredDimension(measureShort(widthMeasureSpec), measureLong(heightMeasureSpec));
+        }
+    }
+
+    /**
+     * Determines the width of this view
+     *
+     * @param measureSpec
+     *            A measureSpec packed into an int
+     * @return The width of the view, honoring constraints from measureSpec
+     */
+    private int measureLong(int measureSpec) {
+        int result;
+        int specMode = MeasureSpec.getMode(measureSpec);
+        int specSize = MeasureSpec.getSize(measureSpec);
+
+        if ((specMode == MeasureSpec.EXACTLY) || (mViewPager == null)) {
+            //We were told how big to be
+            result = specSize;
+        } else {
+            //Calculate the width according the views count
+            final int count = mViewPager.getAdapter().getCount();
+            result = (int)(getPaddingLeft() + getPaddingRight()
+                    + (count * 2 * mRadius) + (count - 1) * mRadius + 1);
+            //Respect AT_MOST value if that was what is called for by measureSpec
+            if (specMode == MeasureSpec.AT_MOST) {
+                result = Math.min(result, specSize);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Determines the height of this view
+     *
+     * @param measureSpec
+     *            A measureSpec packed into an int
+     * @return The height of the view, honoring constraints from measureSpec
+     */
+    private int measureShort(int measureSpec) {
+        int result;
+        int specMode = MeasureSpec.getMode(measureSpec);
+        int specSize = MeasureSpec.getSize(measureSpec);
+
+        if (specMode == MeasureSpec.EXACTLY) {
+            //We were told how big to be
+            result = specSize;
+        } else {
+            //Measure the height
+            result = (int)(2 * mRadius + getPaddingTop() + getPaddingBottom() + 1);
+            //Respect AT_MOST value if that was what is called for by measureSpec
+            if (specMode == MeasureSpec.AT_MOST) {
+                result = Math.min(result, specSize);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState savedState = (SavedState)state;
+        super.onRestoreInstanceState(savedState.getSuperState());
+        mCurrentPage = savedState.currentPage;
+        mSnapPage = savedState.currentPage;
+        requestLayout();
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState savedState = new SavedState(superState);
+        savedState.currentPage = mCurrentPage;
+        return savedState;
+    }
+
+    static class SavedState extends BaseSavedState {
+        int currentPage;
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            currentPage = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(currentPage);
+        }
+
+        @SuppressWarnings("UnusedDeclaration")
+        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
@@ -0,0 +1,53 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream.topsites;
+
+import android.database.Cursor;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.home.UpdateViewFaviconLoadedListener;
+import org.mozilla.gecko.widget.FaviconView;
+
+import java.util.EnumSet;
+
+class TopSitesCard extends RecyclerView.ViewHolder {
+    private final FaviconView faviconView;
+
+    private final TextView title;
+    private final View menuButton;
+
+    private final UpdateViewFaviconLoadedListener mFaviconListener;
+
+    private String url;
+
+    private int mLoadFaviconJobId = Favicons.NOT_LOADING;
+
+    public TopSitesCard(CardView card) {
+        super(card);
+
+        faviconView = (FaviconView) card.findViewById(R.id.favicon);
+
+        title = (TextView) card.findViewById(R.id.title);
+        menuButton = card.findViewById(R.id.menu);
+
+        mFaviconListener = new UpdateViewFaviconLoadedListener(faviconView);
+    }
+
+    void bind(Cursor cursor) {
+        this.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
+
+        title.setText(cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE)));
+
+        Favicons.cancelFaviconLoad(mLoadFaviconJobId);
+
+        mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(faviconView.getContext(), url, mFaviconListener);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java
@@ -0,0 +1,50 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream.topsites;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
+
+import java.util.EnumSet;
+
+public class TopSitesPage
+        extends RecyclerView
+        implements RecyclerViewClickSupport.OnItemClickListener {
+    public TopSitesPage(Context context,
+                        @Nullable AttributeSet attrs) {
+        super(context, attrs);
+
+        setLayoutManager(new GridLayoutManager(context, TopSitesPagerAdapter.GRID_WIDTH));
+
+        RecyclerViewClickSupport.addTo(this)
+                .setOnItemClickListener(this);
+    }
+
+    private HomePager.OnUrlOpenListener onUrlOpenListener = null;
+
+    public TopSitesPageAdapter getAdapter() {
+        return (TopSitesPageAdapter) super.getAdapter();
+    }
+
+    public void setOnUrlOpenListener(HomePager.OnUrlOpenListener onUrlOpenListener) {
+        this.onUrlOpenListener = onUrlOpenListener;
+    }
+
+    @Override
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
+        if (onUrlOpenListener != null) {
+            final String url = getAdapter().getURLForPosition(position);
+
+            onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
@@ -0,0 +1,118 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream.topsites;
+
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.support.annotation.UiThread;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserContract;
+
+public class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> {
+
+    /**
+     * Cursor wrapper that handles the offsets and limits that we expect.
+     * This allows most of our code to completely ignore the fact that we're only touching part
+     * of the cursor.
+     */
+    private static final class SubsetCursor extends CursorWrapper {
+        private final int start;
+        private final int count;
+
+        public SubsetCursor(Cursor cursor, int start, int maxCount) {
+            super(cursor);
+
+            this.start = start;
+
+            if (start + maxCount < cursor.getCount()) {
+                count = maxCount;
+            } else {
+                count = cursor.getCount() - start;
+            }
+        }
+
+        @Override
+        public boolean moveToPosition(int position) {
+            return super.moveToPosition(position + start);
+        }
+
+        @Override
+        public int getCount() {
+            return count;
+        }
+    }
+
+    private Cursor cursor;
+
+    /**
+     *
+     * @param cursor
+     * @param startIndex The first item that this topsites group should show. This item, and the following
+     * 3 items will be displayed by this adapter.
+     */
+    public void swapCursor(Cursor cursor, int startIndex) {
+        if (cursor != null) {
+            if (startIndex >= cursor.getCount()) {
+                throw new IllegalArgumentException("startIndex must be within Cursor range");
+            }
+
+            this.cursor = new SubsetCursor(cursor, startIndex, TopSitesPagerAdapter.ITEMS_PER_PAGE);
+        } else {
+            this.cursor = null;
+        }
+
+        notifyDataSetChanged();
+    }
+
+    @Override
+    public void onBindViewHolder(TopSitesCard holder, int position) {
+        cursor.moveToPosition(position);
+        holder.bind(cursor);
+    }
+
+    public TopSitesPageAdapter() {
+        setHasStableIds(true);
+    }
+
+    @Override
+    public TopSitesCard onCreateViewHolder(ViewGroup parent, int viewType) {
+        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+
+        final CardView card = (CardView) inflater.inflate(R.layout.activity_stream_topsites_card, parent, false);
+
+        return new TopSitesCard(card);
+    }
+
+    @UiThread
+    public String getURLForPosition(int position) {
+        cursor.moveToPosition(position);
+
+        return cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
+    }
+
+    @Override
+    public int getItemCount() {
+        if (cursor != null) {
+            return cursor.getCount();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    @UiThread
+    public long getItemId(int position) {
+        cursor.moveToPosition(position);
+
+        // The Combined View only contains pages that have been visited at least once, i.e. any
+        // page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows.
+        return cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
@@ -0,0 +1,111 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.home.activitystream.topsites;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.v4.view.PagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.home.HomePager;
+
+import java.util.LinkedList;
+
+/**
+ * The primary / top-level TopSites adapter: it handles the ViewPager, and also handles
+ * all lower-level Adapters that populate the individual topsite items.
+ */
+public class TopSitesPagerAdapter extends PagerAdapter {
+    // Note: because of RecyclerView limitations we need to also adjust the layout height when
+    // GRID_HEIGHT is changed.
+    public static final int GRID_HEIGHT = 1;
+    public static final int GRID_WIDTH = 4;
+    public static final int PAGES = 4;
+
+    public static final int ITEMS_PER_PAGE = GRID_HEIGHT * GRID_WIDTH;
+    public static final int TOTAL_ITEMS = ITEMS_PER_PAGE * PAGES;
+
+    private LinkedList<TopSitesPage> pages = new LinkedList<>();
+
+    private final Context context;
+    private final HomePager.OnUrlOpenListener onUrlOpenListener;
+
+    private int count = 0;
+
+    public TopSitesPagerAdapter(Context context, HomePager.OnUrlOpenListener onUrlOpenListener) {
+        this.context = context;
+        this.onUrlOpenListener = onUrlOpenListener;
+    }
+
+    @Override
+    public int getCount() {
+        return count;
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        return view == object;
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+        TopSitesPage page = pages.get(position);
+
+        container.addView(page);
+
+        return page;
+    }
+
+    @Override
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        container.removeView((View) object);
+    }
+
+    public void swapCursor(Cursor cursor) {
+        final int oldPages = getCount();
+
+        // Divide while rounding up: 0 items = 0 pages, 1-ITEMS_PER_PAGE items = 1 page, etc.
+        if (cursor != null) {
+            count = (cursor.getCount() - 1) / ITEMS_PER_PAGE + 1;
+        } else {
+            count = 0;
+        }
+
+        final int pageDelta = count - oldPages;
+
+        if (pageDelta > 0) {
+            final LayoutInflater inflater = LayoutInflater.from(context);
+            for (int i = 0; i < pageDelta; i++) {
+                final TopSitesPage page = (TopSitesPage) inflater.inflate(R.layout.activity_stream_topsites_page, null, false);
+
+                page.setOnUrlOpenListener(onUrlOpenListener);
+                page.setAdapter(new TopSitesPageAdapter());
+                pages.add(page);
+            }
+        } else if (pageDelta < 0) {
+            for (int i = 0; i > pageDelta; i--) {
+                final TopSitesPage page = pages.getLast();
+
+                // Ensure the page doesn't use the old/invalid cursor anymore
+                page.getAdapter().swapCursor(null, 0);
+
+                pages.removeLast();
+            }
+        } else {
+            // do nothing: we will be updating all the pages below
+        }
+
+        int startIndex = 0;
+        for (TopSitesPage page : pages) {
+            page.getAdapter().swapCursor(cursor, startIndex);
+            startIndex += ITEMS_PER_PAGE;
+        }
+
+        notifyDataSetChanged();
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
+++ b/mobile/android/base/java/org/mozilla/gecko/media/CodecProxy.java
@@ -11,34 +11,26 @@ import org.mozilla.gecko.mozglue.JNIObje
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
 import android.media.MediaCodec;
 import android.media.MediaCodec.BufferInfo;
 import android.media.MediaFormat;
 import android.os.DeadObjectException;
-import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.Surface;
 
 import java.nio.ByteBuffer;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.LinkedList;
-import java.util.List;
-
 // Proxy class of ICodec binder.
 public final class CodecProxy {
     private static final String LOGTAG = "GeckoRemoteCodecProxy";
     private static final boolean DEBUG = false;
 
-    private static final RemoteManager sRemoteManager = new RemoteManager();
-
     private ICodec mRemote;
     private FormatParam mFormat;
     private Surface mOutputSurface;
     private CallbacksForwarder mCallbacks;
 
     public interface Callbacks {
         void onInputExhausted();
         void onOutputFormatChanged(MediaFormat format);
@@ -92,200 +84,46 @@ public final class CodecProxy {
             reportError(fatal);
         }
 
         public void reportError(boolean fatal) {
             mCallbacks.onError(fatal);
         }
     }
 
-    private static final class RemoteManager implements IBinder.DeathRecipient {
-        private List<CodecProxy> mProxies = new LinkedList<CodecProxy>();
-        private ICodecManager mRemote;
-        private volatile CountDownLatch mConnectionLatch;
-        private final ServiceConnection mConnection = new ServiceConnection() {
-            @Override
-            public void onServiceConnected(ComponentName name, IBinder service) {
-                if (DEBUG) Log.d(LOGTAG, "service connected");
-                try {
-                    service.linkToDeath(RemoteManager.this, 0);
-                } catch (RemoteException e) {
-                    e.printStackTrace();
-                }
-                mRemote = ICodecManager.Stub.asInterface(service);
-                if (mConnectionLatch != null) {
-                    mConnectionLatch.countDown();
-                }
-            }
-
-            /**
-             * Called when a connection to the Service has been lost.  This typically
-             * happens when the process hosting the service has crashed or been killed.
-             * This does <em>not</em> remove the ServiceConnection itself -- this
-             * binding to the service will remain active, and you will receive a call
-             * to {@link #onServiceConnected} when the Service is next running.
-             *
-             * @param name The concrete component name of the service whose
-             *             connection has been lost.
-             */
-            @Override
-            public void onServiceDisconnected(ComponentName name) {
-                if (DEBUG) Log.d(LOGTAG, "service disconnected");
-                mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0);
-                mRemote = null;
-                if (mConnectionLatch != null) {
-                    mConnectionLatch.countDown();
-                }
-            }
-        };
-
-        public synchronized boolean init() {
-            if (mRemote != null) {
-                return true;
-            }
-
-            if (DEBUG) Log.d(LOGTAG, "init remote manager " + this);
-            Context appCtxt = GeckoAppShell.getApplicationContext();
-            if (DEBUG) Log.d(LOGTAG, "ctxt=" + appCtxt);
-            appCtxt.bindService(new Intent(appCtxt, CodecManager.class),
-                    mConnection, Context.BIND_AUTO_CREATE);
-            if (!waitConnection()) {
-                appCtxt.unbindService(mConnection);
-                return false;
-            }
-            return true;
-        }
-
-        private boolean waitConnection() {
-            boolean ok = false;
-
-            mConnectionLatch = new CountDownLatch(1);
-            try {
-                int retryCount = 0;
-                while (retryCount < 5) {
-                    if (DEBUG) Log.d(LOGTAG, "waiting for connection latch:" + mConnectionLatch);
-                    mConnectionLatch.await(1, TimeUnit.SECONDS);
-                    if (mConnectionLatch.getCount() == 0) {
-                        break;
-                    }
-                    Log.w(LOGTAG, "Creator not connected in 1s. Try again.");
-                    retryCount++;
-                }
-                ok = true;
-            } catch (InterruptedException e) {
-                Log.e(LOGTAG, "service not connected in 5 seconds. Stop waiting.");
-                e.printStackTrace();
-            }
-            mConnectionLatch = null;
-
-            return ok;
-        }
-
-        public synchronized CodecProxy createCodec(MediaFormat format, Surface surface, Callbacks callbacks) {
-            try {
-                ICodec remote = mRemote.createCodec();
-                CodecProxy proxy = new CodecProxy(format, surface, callbacks);
-                if (proxy.init(remote)) {
-                    mProxies.add(proxy);
-                    return proxy;
-                } else {
-                    return null;
-                }
-            } catch (RemoteException e) {
-                e.printStackTrace();
-                return null;
-            }
-        }
-
-        @Override
-        public void binderDied() {
-            Log.e(LOGTAG, "remote codec is dead");
-            handleRemoteDeath();
-        }
-
-        private synchronized void handleRemoteDeath() {
-            // Wait for onServiceDisconnected()
-            if (!waitConnection()) {
-                notifyError(true);
-                return;
-            }
-            // Restart
-            if (init() && recoverRemoteCodec()) {
-                notifyError(false);
-            } else {
-                notifyError(true);
-            }
-        }
-
-        private synchronized void notifyError(boolean fatal) {
-            for (CodecProxy proxy : mProxies) {
-                proxy.mCallbacks.reportError(fatal);
-            }
-        }
-
-        private synchronized boolean recoverRemoteCodec() {
-            if (DEBUG) Log.d(LOGTAG, "recover codec");
-            boolean ok = true;
-            try {
-                for (CodecProxy proxy : mProxies) {
-                    ok &= proxy.init(mRemote.createCodec());
-                }
-                return ok;
-            } catch (RemoteException e) {
-                return false;
-            }
-        }
-
-        private void releaseCodec(CodecProxy proxy) throws DeadObjectException, RemoteException {
-            proxy.deinit();
-            synchronized (this) {
-                if (mProxies.remove(proxy) && mProxies.isEmpty()) {
-                    release();
-                }
-            }
-        }
-
-        private void release() {
-            if (DEBUG) Log.d(LOGTAG, "release remote manager " + this);
-            Context appCtxt = GeckoAppShell.getApplicationContext();
-            mRemote.asBinder().unlinkToDeath(this, 0);
-            mRemote = null;
-            appCtxt.unbindService(mConnection);
-        }
+    @WrapForJNI
+    public static CodecProxy create(MediaFormat format, Surface surface, Callbacks callbacks) {
+        return RemoteManager.getInstance().createCodec(format, surface, callbacks);
     }
 
-    @WrapForJNI
-    public static CodecProxy create(MediaFormat format, Surface surface, Callbacks callbacks) {
-        if (!sRemoteManager.init()) {
-            return null;
-        }
-        return sRemoteManager.createCodec(format, surface, callbacks);
+    public static CodecProxy createCodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
+        return new CodecProxy(format, surface, callbacks);
     }
 
     private CodecProxy(MediaFormat format, Surface surface, Callbacks callbacks) {
         mFormat = new FormatParam(format);
         mOutputSurface = surface;
         mCallbacks = new CallbacksForwarder(callbacks);
     }
 
-    private boolean init(ICodec remote) {
+    boolean init(ICodec remote) {
         try {
             remote.setCallbacks(mCallbacks);
             remote.configure(mFormat, mOutputSurface, 0);
             remote.start();
         } catch (RemoteException e) {
             e.printStackTrace();
             return false;
         }
 
         mRemote = remote;
         return true;
     }
 
-    private boolean deinit() {
+    boolean deinit() {
         try {
             mRemote.stop();
             mRemote.release();
             mRemote = null;
             return true;
         } catch (RemoteException e) {
             e.printStackTrace();
             return false;
@@ -333,18 +171,22 @@ public final class CodecProxy {
     @WrapForJNI
     public synchronized boolean release() {
         if (mRemote == null) {
             Log.w(LOGTAG, "codec already ended");
             return true;
         }
         if (DEBUG) Log.d(LOGTAG, "release " + this);
         try {
-            sRemoteManager.releaseCodec(this);
+            RemoteManager.getInstance().releaseCodec(this);
         } catch (DeadObjectException e) {
             return false;
         } catch (RemoteException e) {
             e.printStackTrace();
             return false;
         }
         return true;
     }
+
+    public synchronized void reportError(boolean fatal) {
+        mCallbacks.reportError(fatal);
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/media/RemoteManager.java
@@ -0,0 +1,201 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.media;
+
+import org.mozilla.gecko.GeckoAppShell;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.MediaFormat;
+import android.os.DeadObjectException;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.view.Surface;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.LinkedList;
+import java.util.List;
+
+public final class RemoteManager implements IBinder.DeathRecipient {
+    private static final String LOGTAG = "GeckoRemoteManager";
+    private static final boolean DEBUG = false;
+    private static RemoteManager sRemoteManager = null;
+
+    public synchronized static RemoteManager getInstance() {
+        if (sRemoteManager == null){
+            sRemoteManager = new RemoteManager();
+        }
+
+        sRemoteManager.init();
+        return sRemoteManager;
+    }
+
+    private List<CodecProxy> mProxies = new LinkedList<CodecProxy>();
+    private volatile ICodecManager mRemote;
+    private volatile CountDownLatch mConnectionLatch;
+    private final ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            if (DEBUG) Log.d(LOGTAG, "service connected");
+            try {
+                service.linkToDeath(RemoteManager.this, 0);
+            } catch (RemoteException e) {
+                e.printStackTrace();
+            }
+            mRemote = ICodecManager.Stub.asInterface(service);
+            if (mConnectionLatch != null) {
+                mConnectionLatch.countDown();
+            }
+        }
+
+        /**
+         * Called when a connection to the Service has been lost.  This typically
+         * happens when the process hosting the service has crashed or been killed.
+         * This does <em>not</em> remove the ServiceConnection itself -- this
+         * binding to the service will remain active, and you will receive a call
+         * to {@link #onServiceConnected} when the Service is next running.
+         *
+         * @param name The concrete component name of the service whose
+         *             connection has been lost.
+         */
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            if (DEBUG) Log.d(LOGTAG, "service disconnected");
+            mRemote.asBinder().unlinkToDeath(RemoteManager.this, 0);
+            mRemote = null;
+            if (mConnectionLatch != null) {
+                mConnectionLatch.countDown();
+            }
+        }
+    };
+
+    private synchronized boolean init() {
+        if (mRemote != null) {
+            return true;
+        }
+
+        if (DEBUG) Log.d(LOGTAG, "init remote manager " + this);
+        Context appCtxt = GeckoAppShell.getApplicationContext();
+        if (DEBUG) Log.d(LOGTAG, "ctxt=" + appCtxt);
+        appCtxt.bindService(new Intent(appCtxt, CodecManager.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+        if (!waitConnection()) {
+            appCtxt.unbindService(mConnection);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean waitConnection() {
+        boolean ok = false;
+
+        mConnectionLatch = new CountDownLatch(1);
+        try {
+            int retryCount = 0;
+            while (retryCount < 5) {
+                if (DEBUG) Log.d(LOGTAG, "waiting for connection latch:" + mConnectionLatch);
+                mConnectionLatch.await(1, TimeUnit.SECONDS);
+                if (mConnectionLatch.getCount() == 0) {
+                    break;
+                }
+                Log.w(LOGTAG, "Creator not connected in 1s. Try again.");
+                retryCount++;
+            }
+            ok = true;
+        } catch (InterruptedException e) {
+            Log.e(LOGTAG, "service not connected in 5 seconds. Stop waiting.");
+            e.printStackTrace();
+        }
+        mConnectionLatch = null;
+
+        return ok;
+    }
+
+    public synchronized CodecProxy createCodec(MediaFormat format,
+                                               Surface surface,
+                                               CodecProxy.Callbacks callbacks) {
+        if (mRemote == null) {
+            if (DEBUG) Log.d(LOGTAG, "createCodec failed due to not initialize");
+            return null;
+        }
+        try {
+            ICodec remote = mRemote.createCodec();
+            CodecProxy proxy = CodecProxy.createCodecProxy(format, surface, callbacks);
+            if (proxy.init(remote)) {
+                mProxies.add(proxy);
+                return proxy;
+            } else {
+                return null;
+            }
+        } catch (RemoteException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    @Override
+    public void binderDied() {
+        Log.e(LOGTAG, "remote codec is dead");
+        handleRemoteDeath();
+    }
+
+    private synchronized void handleRemoteDeath() {
+        // Wait for onServiceDisconnected()
+        if (!waitConnection()) {
+            notifyError(true);
+            return;
+        }
+        // Restart
+        if (init() && recoverRemoteCodec()) {
+            notifyError(false);
+        } else {
+            notifyError(true);
+        }
+    }
+
+    private synchronized void notifyError(boolean fatal) {
+        for (CodecProxy proxy : mProxies) {
+            proxy.reportError(fatal);
+        }
+    }
+
+    private synchronized boolean recoverRemoteCodec() {
+        if (DEBUG) Log.d(LOGTAG, "recover codec");
+        boolean ok = true;
+        try {
+            for (CodecProxy proxy : mProxies) {
+                ok &= proxy.init(mRemote.createCodec());
+            }
+            return ok;
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    public void releaseCodec(CodecProxy proxy) throws DeadObjectException, RemoteException {
+        if (mRemote == null) {
+            if (DEBUG) Log.d(LOGTAG, "releaseCodec called but not initialized yet");
+            return;
+        }
+        proxy.deinit();
+        synchronized (this) {
+            if (mProxies.remove(proxy) && mProxies.isEmpty()) {
+                release();
+            }
+        }
+    }
+
+    private void release() {
+        if (DEBUG) Log.d(LOGTAG, "release remote manager " + this);
+        Context appCtxt = GeckoAppShell.getApplicationContext();
+        mRemote.asBinder().unlinkToDeath(this, 0);
+        mRemote = null;
+        appCtxt.unbindService(mConnection);
+    }
+} // RemoteManager
\ No newline at end of file
--- a/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
+++ b/mobile/android/base/java/org/mozilla/gecko/menu/MenuPopup.java
@@ -37,28 +37,16 @@ public class MenuPopup extends PopupWind
         setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
         setWindowLayoutMode(ViewGroup.LayoutParams.WRAP_CONTENT,
                             ViewGroup.LayoutParams.WRAP_CONTENT);
 
         LayoutInflater inflater = LayoutInflater.from(context);
         mPanel = (CardView) inflater.inflate(R.layout.menu_popup, null);
         setContentView(mPanel);
 
-        // Disable corners on < lollipop:
-        // CardView only supports clipping content on API >= 21 (for performance reasons). Without
-        // content clipping the "action bar" will look ugly because it has its own background:
-        // by default there's a 2px white edge along the top and sides (i.e. an inset corresponding
-        // to the corner radius), if we disable the inset then the corners overlap.
-        // It's possible to implement custom clipping, however given that the support library
-        // chose not to support this for performance reasons, we too have chosen to just disable
-        // corners on < 21, see Bug 1271428.
-        if (AppConstants.Versions.preLollipop) {
-            mPanel.setRadius(0);
-        }
-
         setAnimationStyle(R.style.PopupAnimation);
     }
 
     /**
      * Adds the panel with the menu to its content.
      *
      * @param view The panel view with the menu to be shown.
      */
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/PageActionLayout.java
@@ -50,40 +50,48 @@ public class PageActionLayout extends Li
     public PageActionLayout(Context context, AttributeSet attrs) {
         super(context, attrs);
         mContext = context;
         mLayout = this;
 
         mPageActionList = new ArrayList<PageAction>();
         setNumberShown(DEFAULT_PAGE_ACTIONS_SHOWN);
         refreshPageActionIcons();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
 
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "PageActions:Add",
             "PageActions:Remove");
     }
 
+    @Override
+    protected void onDetachedFromWindow() {
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
+            "PageActions:Add",
+            "PageActions:Remove");
+
+        super.onDetachedFromWindow();
+    }
+
     private void setNumberShown(int count) {
         ThreadUtils.assertOnUiThread();
 
         mMaxVisiblePageActions = count;
 
         for (int index = 0; index < count; index++) {
             if ((getChildCount() - 1) < index) {
                 mLayout.addView(createImageButton());
             }
         }
     }
 
-    public void onDestroy() {
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
-            "PageActions:Add",
-            "PageActions:Remove");
-    }
-
     @Override
     public void handleMessage(final String event, final NativeJSObject message, final EventCallback callback) {
         // NativeJSObject cannot be used off of the Gecko thread, so convert it to a Bundle.
         final Bundle bundle = message.toBundle();
 
         ThreadUtils.postToUiThread(new Runnable() {
             @Override
             public void run() {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/FilledCardView.java
@@ -0,0 +1,39 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+package org.mozilla.gecko.widget;
+
+import android.content.Context;
+import android.support.v7.widget.CardView;
+import android.util.AttributeSet;
+
+import org.mozilla.gecko.AppConstants;
+
+/**
+ * CardView that ensures its content can fill the entire card. Use this instead of CardView
+ * if you want to fill the card with e.g. images, backgrounds, etc.
+ *
+ * On API < 21, CardView content isn't clipped for performance reasons. We work around this by disabling
+ * rounded corners on those devices.
+ */
+public class FilledCardView extends CardView {
+
+    public FilledCardView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // Disable corners on < lollipop:
+        // CardView only supports clipping content on API >= 21 (for performance reasons). Without
+        // content clipping, any cards that provide their own content that fills the card will look
+        // ugly: by default there is a 2px white edge along the top and sides (i.e. an inset corresponding
+        // to the corner radius), if we disable the inset then the corners overlap.
+        // It's possible to implement custom clipping, however given that the support library
+        // chose not to support this for performance reasons, we too have chosen to just disable
+        // corners on < 21, see Bug 1271428.
+        if (AppConstants.Versions.preLollipop) {
+            setRadius(0);
+        }
+
+        setUseCompatPadding(true);
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -426,19 +426,25 @@ gbjar.sources += ['java/org/mozilla/geck
     'GeckoProfilesProvider.java',
     'GeckoUpdateReceiver.java',
     'GlobalHistory.java',
     'GuestSession.java',
     'health/HealthRecorder.java',
     'health/SessionInformation.java',
     'health/StubbedHealthRecorder.java',
     'home/activitystream/ActivityStream.java',
+    'home/activitystream/ActivityStreamHomeFragment.java',
+    'home/activitystream/ActivityStreamHomeScreen.java',
     'home/activitystream/StreamItem.java',
     'home/activitystream/StreamRecyclerAdapter.java',
-    'home/activitystream/TopSitesRecyclerAdapter.java',
+    'home/activitystream/topsites/CirclePageIndicator.java',
+    'home/activitystream/topsites/TopSitesCard.java',
+    'home/activitystream/topsites/TopSitesPage.java',
+    'home/activitystream/topsites/TopSitesPageAdapter.java',
+    'home/activitystream/topsites/TopSitesPagerAdapter.java',
     'home/BookmarkFolderView.java',
     'home/BookmarkScreenshotRow.java',
     'home/BookmarksListAdapter.java',
     'home/BookmarksListView.java',
     'home/BookmarksPanel.java',
     'home/BrowserSearch.java',
     'home/ClientsAdapter.java',
     'home/CombinedHistoryAdapter.java',
@@ -487,32 +493,34 @@ gbjar.sources += ['java/org/mozilla/geck
     'home/SpacingDecoration.java',
     'home/TabMenuStrip.java',
     'home/TabMenuStripLayout.java',
     'home/TopSitesGridItemView.java',
     'home/TopSitesGridView.java',
     'home/TopSitesPanel.java',
     'home/TopSitesThumbnailView.java',
     'home/TwoLinePageRow.java',
+    'home/UpdateViewFaviconLoadedListener.java',
     'IntentHelper.java',
     'javaaddons/JavaAddonManager.java',
     'javaaddons/JavaAddonManagerV1.java',
     'LauncherActivity.java',
     'lwt/LightweightTheme.java',
     'lwt/LightweightThemeDrawable.java',
     'mdns/MulticastDNSManager.java',
     'media/AsyncCodec.java',
     'media/AsyncCodecFactory.java',
     'media/AudioFocusAgent.java',
     'media/Codec.java',
     'media/CodecManager.java',
     'media/CodecProxy.java',
     'media/FormatParam.java',
     'media/JellyBeanAsyncCodec.java',
     'media/MediaControlService.java',
+    'media/RemoteManager.java',
     'media/Sample.java',
     'MediaCastingBar.java',
     'MemoryMonitor.java',
     'menu/GeckoMenu.java',
     'menu/GeckoMenuInflater.java',
     'menu/GeckoMenuItem.java',
     'menu/GeckoSubMenu.java',
     'menu/MenuItemActionBar.java',
@@ -668,16 +676,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'widget/DoorHanger.java',
     'widget/DoorhangerConfig.java',
     'widget/EllipsisTextView.java',
     'widget/ExternalIntentDuringPrivateBrowsingPromptFragment.java',
     'widget/FadedMultiColorTextView.java',
     'widget/FadedSingleColorTextView.java',
     'widget/FadedTextView.java',
     'widget/FaviconView.java',
+    'widget/FilledCardView.java',
     'widget/FlowLayout.java',
     'widget/GeckoActionProvider.java',
     'widget/GeckoPopupMenu.java',
     'widget/HistoryDividerItemDecoration.java',
     'widget/IconTabWidget.java',
     'widget/LoginDoorHanger.java',
     'widget/RecyclerViewClickSupport.java',
     'widget/ResizablePathDrawable.java',
--- a/mobile/android/base/resources/layout/activity_stream.xml
+++ b/mobile/android/base/resources/layout/activity_stream.xml
@@ -1,15 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
-<org.mozilla.gecko.home.activitystream.ActivityStream xmlns:android="http://schemas.android.com/apk/res/android"
+<org.mozilla.gecko.home.activitystream.ActivityStreamHomeScreen xmlns:android="http://schemas.android.com/apk/res/android"
                                                       android:orientation="vertical"
                                                       android:layout_width="match_parent"
                                                       android:layout_height="match_parent"
                                                       android:background="#FAFAFA">
     <android.support.v7.widget.RecyclerView
         android:id="@+id/activity_stream_main_recyclerview"
-        android:layout_marginTop="12dp"
-        android:layout_marginLeft="12dp"
-        android:layout_marginStart="12dp"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
 
-</org.mozilla.gecko.home.activitystream.ActivityStream>
+</org.mozilla.gecko.home.activitystream.ActivityStreamHomeScreen>
--- a/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
+++ b/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
@@ -1,15 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:orientation="vertical">
-
+                xmlns:app="http://schemas.android.com/apk/res-auto"
+                android:orientation="vertical"
+                android:padding="4dp">
 
     <TextView
         android:id="@+id/title_topsites"
         android:text="@string/activity_stream_topsites"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentLeft="true"
         android:layout_alignParentStart="true"
@@ -26,31 +27,44 @@
         android:layout_alignParentRight="true"
         android:textAllCaps="true"
         android:textColor="@android:color/holo_orange_dark"
         android:textSize="14sp"
         android:text="@string/activity_stream_more"
         tools:text="More"
         android:layout_alignBottom="@+id/title_topsites"/>
 
-    <android.support.v7.widget.RecyclerView
+    <android.support.v4.view.ViewPager
         android:layout_width="match_parent"
         android:layout_height="115dp"
-        android:id="@+id/android.support.v7.widget.RecyclerView"
+        android:id="@+id/topsites_pager"
         android:layout_below="@+id/title_topsites"
         android:layout_alignParentLeft="true"
         android:layout_alignParentStart="true"/>
 
+    <org.mozilla.gecko.home.activitystream.topsites.CirclePageIndicator
+        android:id="@+id/topsites_indicator"
+        android:padding="10dip"
+        app:fillColor="#444444"
+        app:pageColor="#FFFFFF"
+        app:strokeWidth="1dp"
+        app:radius="2dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/topsites_pager"
+        android:layout_alignParentRight="true"
+        android:layout_alignParentEnd="true"/>
+
     <TextView
         android:id="@+id/title_highlights"
         android:text="@string/activity_stream_highlights"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:textStyle="bold"
-        android:layout_below="@+id/android.support.v7.widget.RecyclerView"
+        android:layout_below="@+id/topsites_indicator"
         android:layout_alignParentLeft="true"
         android:layout_alignParentStart="true"
         android:layout_toLeftOf="@+id/more_highlights"
         android:layout_toStartOf="@+id/more_highlights"/>
 
     <TextView
         android:id="@+id/more_highlights"
         android:layout_width="wrap_content"
@@ -58,10 +72,9 @@
         android:textAllCaps="true"
         android:textColor="@android:color/holo_orange_dark"
         android:textSize="14sp"
         android:text="@string/activity_stream_more"
         android:layout_alignTop="@+id/title_highlights"
         android:layout_alignLeft="@+id/more_topsites"
         android:layout_alignStart="@+id/more_topsites"/>
 
-
 </RelativeLayout>
\ No newline at end of file
rename from mobile/android/base/resources/layout/activity_stream_card_top_sites_item.xml
rename to mobile/android/base/resources/layout/activity_stream_topsites_card.xml
--- a/mobile/android/base/resources/layout/activity_stream_card_top_sites_item.xml
+++ b/mobile/android/base/resources/layout/activity_stream_topsites_card.xml
@@ -1,46 +1,51 @@
 <?xml version="1.0" encoding="utf-8"?>
-<android.support.v7.widget.CardView
+<org.mozilla.gecko.widget.FilledCardView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
-    android:layout_marginRight="5dp"
-    android:layout_marginEnd="5dp"
-    android:layout_marginTop="10dp"
-    android:layout_marginBottom="10dp"
-    android:orientation="vertical"
-    android:layout_width="90dp"
-    android:layout_height="match_parent">
+    android:layout_width="wrap_content"
+    android:layout_height="115dp"
+    android:layout_margin="1dp">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
 
-    <LinearLayout
-        android:orientation="vertical"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:baselineAligned="false">
-
-        <FrameLayout
+        <org.mozilla.gecko.widget.FaviconView
+            android:id="@+id/favicon"
             android:layout_width="match_parent"
-            android:background="@color/disabled_grey"
-            android:layout_height="70dp">
+            android:layout_height="wrap_content"
+            android:layout_above="@+id/title"
+            android:layout_alignParentTop="true"
+            android:layout_centerHorizontal="true"
+            android:layout_gravity="center"
+            tools:background="@drawable/favicon_globe"/>
 
-            <ImageView
-                android:src="@drawable/favicon_globe"
-                android:scaleType="fitCenter"
-                android:layout_gravity="center"
-                android:layout_width="40dp"
-                android:layout_height="40dp"/>
-        </FrameLayout>
-
-        <FrameLayout
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentStart="true"
+            android:ellipsize="end"
+            android:gravity="center"
+            android:lines="1"
+            android:padding="4dp"
+            android:textColor="@android:color/black"
+            tools:text="Lorem Ipsum here is a title"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentEnd="true"/>
 
-            <TextView
-                android:id="@+id/card_row_label"
-                tools:text="Firefox"
-                android:textSize="10sp"
-                android:textStyle="bold"
-                android:layout_gravity="center"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"/>
-        </FrameLayout>
-    </LinearLayout>
-</android.support.v7.widget.CardView>
\ No newline at end of file
+        <ImageView
+            android:id="@+id/menu_button"
+            android:layout_width="wrap_content"
+            android:layout_height="32dp"
+            android:layout_gravity="right|top"
+            android:padding="6dp"
+            android:src="@drawable/menu"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentEnd="true"/>
+
+    </RelativeLayout>
+</org.mozilla.gecko.widget.FilledCardView>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/activity_stream_topsites_page.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<org.mozilla.gecko.home.activitystream.topsites.TopSitesPage xmlns:android="http://schemas.android.com/apk/res/android"
+              android:orientation="vertical"
+              android:layout_width="match_parent"
+              android:layout_height="match_parent"/>
--- a/mobile/android/base/resources/layout/menu_popup.xml
+++ b/mobile/android/base/resources/layout/menu_popup.xml
@@ -1,18 +1,18 @@
 <?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/. -->
 
-<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+<org.mozilla.gecko.widget.FilledCardView xmlns:android="http://schemas.android.com/apk/res/android"
               xmlns:app="http://schemas.android.com/apk/res-auto"
               android:id="@+id/menu_panel"
               android:layout_width="@dimen/menu_popup_width"
               android:layout_height="wrap_content"
               android:layout_alignParentRight="true"
               android:minWidth="@dimen/menu_popup_width"
               app:cardBackgroundColor="@color/toolbar_grey"
               app:cardUseCompatPadding="true">
 
     <!-- MenuPanel will be added here dynamically -->
 
-</android.support.v7.widget.CardView>
\ No newline at end of file
+</org.mozilla.gecko.widget.FilledCardView>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/values/vpi__attrs.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 Jake Wharton
+     Copyright (C) 2011 Patrik Åkerfeldt
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <declare-styleable name="ViewPagerIndicator">
+        <!-- Style of the circle indicator. -->
+        <attr name="vpiCirclePageIndicatorStyle" format="reference"/>
+        <!-- Style of the icon indicator's views. -->
+        <attr name="vpiIconPageIndicatorStyle" format="reference"/>
+        <!-- Style of the line indicator. -->
+        <attr name="vpiLinePageIndicatorStyle" format="reference"/>
+        <!-- Style of the title indicator. -->
+        <attr name="vpiTitlePageIndicatorStyle" format="reference"/>
+        <!-- Style of the tab indicator's tabs. -->
+        <attr name="vpiTabPageIndicatorStyle" format="reference"/>
+        <!-- Style of the underline indicator. -->
+        <attr name="vpiUnderlinePageIndicatorStyle" format="reference"/>
+    </declare-styleable>
+
+    <attr name="centered" format="boolean" />
+    <attr name="selectedColor" format="color" />
+    <attr name="strokeWidth" format="dimension" />
+    <attr name="unselectedColor" format="color" />
+
+    <declare-styleable name="CirclePageIndicator">
+        <!-- Whether or not the indicators should be centered. -->
+        <attr name="centered" />
+        <!-- Color of the filled circle that represents the current page. -->
+        <attr name="fillColor" format="color" />
+        <!-- Color of the filled circles that represents pages. -->
+        <attr name="pageColor" format="color" />
+        <!-- Orientation of the indicator. -->
+        <attr name="android:orientation"/>
+        <!-- Radius of the circles. This is also the spacing between circles. -->
+        <attr name="radius" format="dimension" />
+        <!-- Whether or not the selected indicator snaps to the circles. -->
+        <attr name="snap" format="boolean" />
+        <!-- Color of the open circles. -->
+        <attr name="strokeColor" format="color" />
+        <!-- Width of the stroke used to draw the circles. -->
+        <attr name="strokeWidth" />
+        <!-- View background -->
+        <attr name="android:background"/>
+    </declare-styleable>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/values/vpi__defaults.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 Jake Wharton
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+    <bool name="default_circle_indicator_centered">true</bool>
+    <color name="default_circle_indicator_fill_color">#FFFFFFFF</color>
+    <color name="default_circle_indicator_page_color">#00000000</color>
+    <integer name="default_circle_indicator_orientation">0</integer>
+    <dimen name="default_circle_indicator_radius">3dp</dimen>
+    <bool name="default_circle_indicator_snap">false</bool>
+    <color name="default_circle_indicator_stroke_color">#FFDDDDDD</color>
+    <dimen name="default_circle_indicator_stroke_width">1dp</dimen>
+</resources>
\ No newline at end of file
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoAccessibility.java
@@ -4,25 +4,25 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.AppConstants.Versions;
-import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.View;
+import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeProvider;
 
 import com.googlecode.eyesfree.braille.selfbraille.SelfBrailleClient;
 import com.googlecode.eyesfree.braille.selfbraille.WriteData;
 
@@ -47,17 +47,17 @@ public class GeckoAccessibility {
                 @Override
                 public Void doInBackground() {
                     JSONObject ret = new JSONObject();
                     sEnabled = false;
                     AccessibilityManager accessibilityManager =
                         (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
                     sEnabled = accessibilityManager.isEnabled() && accessibilityManager.isTouchExplorationEnabled();
                     if (Versions.feature16Plus && sEnabled && sSelfBrailleClient == null) {
-                        sSelfBrailleClient = new SelfBrailleClient(GeckoAppShell.getContext(), false);
+                        sSelfBrailleClient = new SelfBrailleClient(context, false);
                     }
 
                     try {
                         ret.put("enabled", sEnabled);
                     } catch (Exception ex) {
                         Log.e(LOGTAG, "Error building JSON arguments for Accessibility:Settings:", ex);
                     }
 
@@ -101,22 +101,23 @@ public class GeckoAccessibility {
         }
         if (Versions.feature15Plus) {
             event.setMaxScrollX(message.optInt("maxScrollX", -1));
             event.setMaxScrollY(message.optInt("maxScrollY", -1));
         }
     }
 
     private static void sendDirectAccessibilityEvent(int eventType, JSONObject message) {
+        final Context context = GeckoAppShell.getApplicationContext();
         final AccessibilityEvent accEvent = AccessibilityEvent.obtain(eventType);
         accEvent.setClassName(GeckoAccessibility.class.getName());
-        accEvent.setPackageName(GeckoAppShell.getContext().getPackageName());
+        accEvent.setPackageName(context.getPackageName());
         populateEventFromJSON(accEvent, message);
         AccessibilityManager accessibilityManager =
-            (AccessibilityManager) GeckoAppShell.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+            (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
         try {
             accessibilityManager.sendAccessibilityEvent(accEvent);
         } catch (IllegalStateException e) {
             // Accessibility is off.
         }
     }
 
     public static boolean isEnabled() {
@@ -156,17 +157,17 @@ public class GeckoAccessibility {
                     @Override
                     public void run() {
                         sendDirectAccessibilityEvent(eventType, message);
                 }
             });
         } else {
             // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
             // it work with TalkBack.
-            final LayerView view = GeckoAppShell.getLayerView();
+            final View view = GeckoAppShell.getLayerView();
             if (view == null)
                 return;
 
             if (sVirtualCursorNode == null)
                 sVirtualCursorNode = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
             sVirtualCursorNode.setEnabled(message.optBoolean("enabled", true));
             sVirtualCursorNode.setClickable(message.optBoolean("clickable"));
             sVirtualCursorNode.setCheckable(message.optBoolean("checkable"));
@@ -205,47 +206,47 @@ public class GeckoAccessibility {
             if (eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER) {
                 sHoverEnter = message;
             }
 
             ThreadUtils.postToUiThread(new Runnable() {
                     @Override
                     public void run() {
                         final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
-                        event.setPackageName(GeckoAppShell.getContext().getPackageName());
+                        event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
                         event.setClassName(GeckoAccessibility.class.getName());
                         if (eventType == AccessibilityEvent.TYPE_ANNOUNCEMENT ||
                             eventType == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
                             event.setSource(view, View.NO_ID);
                         } else {
                             event.setSource(view, VIRTUAL_CURSOR_POSITION);
                         }
                         populateEventFromJSON(event, message);
-                        view.requestSendAccessibilityEvent(view, event);
+                        ((ViewParent) view).requestSendAccessibilityEvent(view, event);
                     }
                 });
 
         }
     }
 
     private static void sendBrailleText(final View view, final String text, final int selectionStart, final int selectionEnd) {
         AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain(view, VIRTUAL_CURSOR_POSITION);
         WriteData data = WriteData.forInfo(info);
         data.setText(text);
         // Set either the focus blink or the current caret position/selection
         data.setSelectionStart(selectionStart);
         data.setSelectionEnd(selectionEnd);
         sSelfBrailleClient.write(data);
     }
 
-    public static void setDelegate(LayerView layerview) {
+    public static void setDelegate(View view) {
         // Only use this delegate in Jelly Bean.
         if (Versions.feature16Plus) {
-            layerview.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
-            layerview.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+            view.setAccessibilityDelegate(new GeckoAccessibilityDelegate());
+            view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
         }
     }
 
     public static void setAccessibilityManagerListeners(final Context context) {
         AccessibilityManager accessibilityManager =
             (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
 
         accessibilityManager.addAccessibilityStateChangeListener(new AccessibilityManager.AccessibilityStateChangeListener() {
@@ -260,17 +261,17 @@ public class GeckoAccessibility {
                 @Override
                 public void onTouchExplorationStateChanged(boolean enabled) {
                     updateAccessibilitySettings(context);
                 }
             });
         }
     }
 
-    public static void onLayerViewFocusChanged(LayerView layerview, boolean gainFocus) {
+    public static void onLayerViewFocusChanged(boolean gainFocus) {
         if (sEnabled)
             GeckoAppShell.notifyObservers("Accessibility:Focus", gainFocus ? "true" : "false");
     }
 
     public static class GeckoAccessibilityDelegate extends View.AccessibilityDelegate {
         AccessibilityNodeProvider mAccessibilityNodeProvider;
 
         @Override
@@ -294,17 +295,17 @@ public class GeckoAccessibility {
                                 info.addChild(host, VIRTUAL_ENTRY_POINT_BEFORE);
                                 info.addChild(host, VIRTUAL_CURSOR_POSITION);
                                 info.addChild(host, VIRTUAL_ENTRY_POINT_AFTER);
                                 break;
                             default:
                                 info.setParent(host);
                                 info.setSource(host, virtualDescendantId);
                                 info.setVisibleToUser(host.isShown());
-                                info.setPackageName(GeckoAppShell.getContext().getPackageName());
+                                info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
                                 info.setClassName(host.getClass().getName());
                                 info.setEnabled(true);
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
                                 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_LONG_CLICK);
                                 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
                                 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -184,17 +184,16 @@ public class LayerView extends ScrollVie
         mToolbarAnimator = mLayerClient.getDynamicToolbarAnimator();
 
         mRenderer = new LayerRenderer(this);
 
         setFocusable(true);
         setFocusableInTouchMode(true);
 
         GeckoAccessibility.setDelegate(this);
-        GeckoAccessibility.setAccessibilityManagerListeners(getContext());
     }
 
     /**
      * MotionEventHelper dragAsync() robocop tests can instruct
      * PanZoomController not to generate longpress events.
      */
     public void setIsLongpressEnabled(boolean isLongpressEnabled) {
         mPanZoomController.setIsLongpressEnabled(isLongpressEnabled);
@@ -703,17 +702,17 @@ public class LayerView extends ScrollVie
 
     public float getZoomFactor() {
         return getLayerClient().getViewportMetrics().zoomFactor;
     }
 
     @Override
     public void onFocusChanged (boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
-        GeckoAccessibility.onLayerViewFocusChanged(this, gainFocus);
+        GeckoAccessibility.onLayerViewFocusChanged(gainFocus);
     }
 
     public void setFullScreenState(FullScreenState state) {
         mFullScreenState = state;
     }
 
     public boolean isFullScreen() {
         return mFullScreenState != FullScreenState.NONE;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/NativePanZoomController.java
@@ -24,17 +24,17 @@ import android.view.InputDevice;
 
 class NativePanZoomController extends JNIObject implements PanZoomController {
     private final PanZoomTarget mTarget;
     private final LayerView mView;
     private boolean mDestroyed;
     private Overscroll mOverscroll;
     boolean mNegateWheelScroll;
     private float mPointerScrollFactor;
-    private final PrefsHelper.PrefHandler mPrefsObserver;
+    private PrefsHelper.PrefHandler mPrefsObserver;
     private long mLastDownTime;
     private static final float MAX_SCROLL = 0.075f * GeckoAppShell.getDpi();
 
     @WrapForJNI(calledFrom = "ui")
     private native boolean handleMotionEvent(
             int action, int actionIndex, long time, int metaState,
             int pointerId[], float x[], float y[], float orientation[], float pressure[],
             float toolMajor[], float toolMinor[]);
@@ -192,16 +192,20 @@ class NativePanZoomController extends JN
 
     @Override
     public void onMotionEventVelocity(final long aEventTime, final float aSpeedY) {
         handleMotionEventVelocity(aEventTime, aSpeedY);
     }
 
     @Override @WrapForJNI(calledFrom = "ui") // PanZoomController
     public void destroy() {
+        if (mPrefsObserver != null) {
+            PrefsHelper.removeObserver(mPrefsObserver);
+            mPrefsObserver = null;
+        }
         if (mDestroyed || !mTarget.isGeckoReady()) {
             return;
         }
         mDestroyed = true;
         disposeNative();
     }
 
     @WrapForJNI(calledFrom = "ui", dispatchTo = "gecko") @Override // JNIObject
--- a/mobile/android/search/java/org/mozilla/search/SearchActivity.java
+++ b/mobile/android/search/java/org/mozilla/search/SearchActivity.java
@@ -109,17 +109,17 @@ public class SearchActivity extends Loca
         GeckoAppShell.ensureCrashHandling();
 
         super.onCreate(savedInstanceState);
         setContentView(R.layout.search_activity_main);
 
         suggestionsFragment = (SuggestionsFragment) getSupportFragmentManager().findFragmentById(R.id.suggestions);
         postSearchFragment = (PostSearchFragment)  getSupportFragmentManager().findFragmentById(R.id.postsearch);
 
-        searchEngineManager = new SearchEngineManager(this, Distribution.init(this));
+        searchEngineManager = new SearchEngineManager(this, Distribution.init(getApplicationContext()));
         searchEngineManager.setChangeCallback(this);
 
         // Initialize the fragments with the selected search engine.
         searchEngineManager.getEngine(this);
 
         queryHandler = new AsyncQueryHandlerImpl(getContentResolver());
 
         searchBar = (SearchBar) findViewById(R.id.search_bar);
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5650,16 +5650,27 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": 20000,
     "n_buckets": 20,
     "description": "Time for the home screen Top Sites query to return with no filter set (ms)",
     "cpp_guard": "ANDROID"
   },
+  "FENNEC_ACTIVITY_STREAM_TOPSITES_LOADER_TIME_MS": {
+    "expires_in_version": "never",
+    "kind": "exponential",
+    "low": 10,
+    "high": 20000,
+    "n_buckets": 20,
+    "description": "Time for the Activity Stream home screen Top Sites query to return (ms)",
+    "alert_emails": ["mobile-frontend@mozilla.com"],
+    "bug_numbers": [1293790],
+    "cpp_guard": "ANDROID"
+  },
   "FENNEC_HOMEPANELS_CUSTOM": {
     "expires_in_version": "54",
     "kind": "boolean",
     "bug_numbers": [1245368],
     "description": "Whether the user has customized their homepanels",
     "cpp_guard": "ANDROID"
   },
   "FENNEC_WAS_KILLED": {