Bug 1293790 - WIP: Implement Paged TopSites View draft
authorAndrzej Hunt <ahunt@mozilla.com>
Wed, 24 Aug 2016 11:28:45 -0700
changeset 405104 6030830158c601ad80cee3b666b0b6d36f468ee8
parent 404595 5aa1b18fcab2246f55bb89b431433daf34b56a73
child 405105 de4a93f4ec1731875d08febd666215a0f1b5c555
push id27385
push userahunt@mozilla.com
push dateWed, 24 Aug 2016 18:42:24 +0000
bugs1293790
milestone51.0a1
Bug 1293790 - WIP: Implement Paged TopSites View This uses a ViewPager, with each page containing a grid managed by a separate RecyclerView. One main adapter splits the data into appropriately sized groups for each RecyclerView to handle. MozReview-Commit-ID: 9XGuw0NckD4
mobile/android/base/java/org/mozilla/gecko/home/activitystream/ActivityStream.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamRecyclerAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/TopSitesRecyclerAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPage.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
mobile/android/base/moz.build
mobile/android/base/resources/layout/activity_stream_card_top_sites_item.xml
mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
mobile/android/base/resources/layout/activity_stream_topsites_card.xml
mobile/android/base/resources/layout/activity_stream_topsites_page.xml
--- 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,20 +17,24 @@ 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 {
     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() {
@@ -66,26 +70,32 @@ public class ActivityStream extends Fram
     }
 
     @Override
     public void load(LoaderManager lm, FragmentManager fm, String panelId, Bundle restoreData,
                      PropertyAnimator animator) {
         // 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 +110,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);
+            }
         }
     }
 }
--- 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,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;
 
 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.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));
+        }
+
+        @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/TopSitesCard.java
@@ -0,0 +1,60 @@
+/* -*- 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);
+
+        card.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                throw new IllegalStateException("foobar");
+            }
+        });
+    }
+
+    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,119 @@
+/* -*- 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) {
+        // assert startIndex < size?
+        if (startIndex >= cursor.getCount()) {
+            throw new IllegalArgumentException("startIndex must be within Cursor range");
+        }
+
+        if (cursor != null) {
+            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/moz.build
+++ b/mobile/android/base/moz.build
@@ -427,17 +427,20 @@ gbjar.sources += ['java/org/mozilla/geck
     'GlobalHistory.java',
     'GuestSession.java',
     'health/HealthRecorder.java',
     'health/SessionInformation.java',
     'health/StubbedHealthRecorder.java',
     'home/activitystream/ActivityStream.java',
     'home/activitystream/StreamItem.java',
     'home/activitystream/StreamRecyclerAdapter.java',
-    'home/activitystream/TopSitesRecyclerAdapter.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',
--- a/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
+++ b/mobile/android/base/resources/layout/activity_stream_main_toppanel.xml
@@ -26,31 +26,31 @@
         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"/>
 
     <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_pager"
         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"
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"/>