Zine: support multiple sections in the ui

* The adapter now manages groups of items.
* A SuggestionsSection is a group that holds suggestions, a header, and a status card.
* Add @SuggestionsCategory and @SuggestionsCategoryStatus annotations.
* SnippetsObserver now specifies what category it is talking about.

Based on http://codereview.chromium.org/2194433002#ps20001 by mvanouwerkerk@chromium.org.

BUG=616090

Review-Url: https://codereview.chromium.org/2196273002
Cr-Commit-Position: refs/heads/master@{#410075}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/AboveTheFoldListItem.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/AboveTheFoldListItem.java
index 3e1efb8..98604a1 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/AboveTheFoldListItem.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/AboveTheFoldListItem.java
@@ -13,9 +13,9 @@
  * Other elements coming after it and initially off-screen are just added to the RecyclerView after
  * that.
  */
-class AboveTheFoldListItem implements NewTabPageListItem {
+class AboveTheFoldListItem extends SingleItemGroup {
     @Override
     public int getType() {
         return NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD;
     }
-}
\ No newline at end of file
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/ItemGroup.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/ItemGroup.java
new file mode 100644
index 0000000..afa54b0
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/ItemGroup.java
@@ -0,0 +1,17 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ntp.cards;
+
+import java.util.List;
+
+/**
+ * A group of items.
+ */
+public interface ItemGroup {
+    /**
+     * @return A list of items contained in this group. The list should not be modified.
+     */
+    List<NewTabPageListItem> getItems();
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java
index 2f8c5ff..61285b3c 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java
@@ -18,6 +18,9 @@
 import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
 import org.chromium.chrome.browser.ntp.UiConfig;
 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories.KnownCategoriesEnum;
 import org.chromium.chrome.browser.ntp.snippets.SnippetArticleListItem;
 import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder;
 import org.chromium.chrome.browser.ntp.snippets.SnippetHeaderListItem;
@@ -27,7 +30,10 @@
 import org.chromium.chrome.browser.ntp.snippets.SnippetsSource.SnippetsObserver;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
 
 /**
  * A class that handles merging above the fold elements and below the fold cards into an adapter
@@ -40,18 +46,21 @@
 
     private final NewTabPageManager mNewTabPageManager;
     private final NewTabPageLayout mNewTabPageLayout;
-    private final AboveTheFoldListItem mAboveTheFold;
-    private final SnippetHeaderListItem mHeader;
-    private final UiConfig mUiConfig;
-    private final ProgressListItem mProgressIndicator;
-    private StatusListItem mStatusCard;
-    private final SpacingListItem mBottomSpacer;
-    private final List<NewTabPageListItem> mItems;
-    private final ItemTouchCallbacks mItemTouchCallbacks;
-    private NewTabPageRecyclerView mRecyclerView;
-    private int mProviderStatus;
-
     private SnippetsSource mSnippetsSource;
+    private final UiConfig mUiConfig;
+    private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallbacks();
+    private NewTabPageRecyclerView mRecyclerView;
+
+    /**
+     * List of all item groups (which can themselves contain multiple items. When flattened, this
+     * will be a list of all items the adapter exposes.
+     */
+    private final List<ItemGroup> mGroups = new ArrayList<>();
+    private final AboveTheFoldListItem mAboveTheFold = new AboveTheFoldListItem();
+    private final SpacingListItem mBottomSpacer = new SpacingListItem();
+
+    /** Maps suggestion categories to sections, with stable iteration ordering. */
+    private final Map<Integer, SuggestionsSection> mSections = new TreeMap<>();
 
     private class ItemTouchCallbacks extends ItemTouchHelper.Callback {
         @Override
@@ -114,17 +123,14 @@
             SnippetsSource snippetsSource, UiConfig uiConfig) {
         mNewTabPageManager = manager;
         mNewTabPageLayout = newTabPageLayout;
-        mAboveTheFold = new AboveTheFoldListItem();
-        mHeader = new SnippetHeaderListItem();
-        mProgressIndicator = new ProgressListItem();
-        mBottomSpacer = new SpacingListItem();
-        mItemTouchCallbacks = new ItemTouchCallbacks();
-        mItems = new ArrayList<>();
-        mProviderStatus = snippetsSource.getCategoryStatus();
         mSnippetsSource = snippetsSource;
         mUiConfig = uiConfig;
-        mStatusCard = StatusListItem.create(mProviderStatus, this);
-        loadSnippets(new ArrayList<SnippetArticleListItem>());
+
+        // TODO(mvanouwerkerk): Do not hard code ARTICLES. Maybe do not initialize an empty
+        // section in the constructor.
+        setSuggestions(KnownCategories.ARTICLES,
+                Collections.<SnippetArticleListItem>emptyList(),
+                snippetsSource.getCategoryStatus(KnownCategories.ARTICLES));
         snippetsSource.setObserver(this);
     }
 
@@ -134,54 +140,50 @@
     }
 
     @Override
-    public void onSnippetsReceived(List<SnippetArticleListItem> snippets) {
+    public void onSuggestionsReceived(
+            @KnownCategoriesEnum int category, List<SnippetArticleListItem> suggestions) {
         // We never want to refresh the suggestions if we already have some content.
-        if (hasSuggestions()) return;
+        if (mSections.containsKey(category) && mSections.get(category).hasSuggestions()) return;
 
-        if (!SnippetsBridge.isCategoryStatusInitOrAvailable(mProviderStatus)) {
+        // The status may have changed while the suggestions were loading, perhaps they should not
+        // be displayed any more.
+        if (!SnippetsBridge.isCategoryStatusInitOrAvailable(
+                    mSnippetsSource.getCategoryStatus(category))) {
             return;
         }
 
-        Log.d(TAG, "Received %d new snippets.", snippets.size());
+        Log.d(TAG, "Received %d new suggestions for category %d.", suggestions.size(), category);
 
-        // At first, there might be no snippets available, we wait until they have been fetched.
-        if (snippets.isEmpty()) return;
+        // At first, there might be no suggestions available, we wait until they have been fetched.
+        if (suggestions.isEmpty()) return;
 
-        loadSnippets(snippets);
+        setSuggestions(category, suggestions, mSnippetsSource.getCategoryStatus(category));
 
         NewTabPageUma.recordSnippetAction(NewTabPageUma.SNIPPETS_ACTION_SHOWN);
     }
 
     @Override
-    public void onCategoryStatusChanged(int categoryStatus) {
-        // Observers should not be registered for that state
-        assert categoryStatus != CategoryStatus.ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
+    public void onCategoryStatusChanged(
+            @KnownCategoriesEnum int category, @CategoryStatusEnum int status) {
+        // Observers should not be registered for this state.
+        assert status != CategoryStatus.ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
 
-        mProviderStatus = categoryStatus;
-        mStatusCard = StatusListItem.create(mProviderStatus, this);
-        mProgressIndicator.setVisible(SnippetsBridge.isCategoryLoading(mProviderStatus));
+        // If there is no section for this category there is nothing to do.
+        if (!mSections.containsKey(category)) return;
 
-        // We had suggestions but we just got notified about the provider being enabled. Nothing to
-        // do then.
-        if (SnippetsBridge.isCategoryStatusAvailable(mProviderStatus) && hasSuggestions()) return;
+        SuggestionsSection section = mSections.get(category);
 
-        if (hasSuggestions()) {
-            // We have suggestions, this implies that the service was previously enabled and just
-            // transitioned to a disabled state. Clear them.
-            loadSnippets(new ArrayList<SnippetArticleListItem>());
-        } else {
-            // If there are no suggestions there is an old status card that must be replaced.
-            int firstCardPosition = getFirstCardPosition();
-            mItems.set(firstCardPosition, mStatusCard);
-            // Update both the status card, the progress indicator and the spacer after it.
-            notifyItemRangeChanged(firstCardPosition, 3);
-        }
+        // The section already has suggestions but we just got notified about the provider being
+        // enabled. Nothing to do.
+        if (SnippetsBridge.isCategoryStatusAvailable(status) && section.hasSuggestions()) return;
+
+        setSuggestions(category, Collections.<SnippetArticleListItem>emptyList(), status);
     }
 
     @Override
     @NewTabPageListItem.ViewType
     public int getItemViewType(int position) {
-        return mItems.get(position).getType();
+        return getItems().get(position).getType();
     }
 
     @Override
@@ -218,32 +220,36 @@
 
     @Override
     public void onBindViewHolder(NewTabPageViewHolder holder, final int position) {
-        holder.onBindViewHolder(mItems.get(position));
+        holder.onBindViewHolder(getItems().get(position));
     }
 
     @Override
     public int getItemCount() {
-        return mItems.size();
+        return getItems().size();
     }
 
     public int getAboveTheFoldPosition() {
-        return mItems.indexOf(mAboveTheFold);
+        return getGroupPositionOffset(mAboveTheFold);
     }
 
-    public int getHeaderPosition() {
-        return mItems.indexOf(mHeader);
+    public int getFirstHeaderPosition() {
+        List<NewTabPageListItem> items = getItems();
+        for (int i = 0; i < items.size(); i++) {
+            if (items.get(i) instanceof SnippetHeaderListItem) return i;
+        }
+        return RecyclerView.NO_POSITION;
     }
 
     public int getFirstCardPosition() {
-        return getHeaderPosition() + 1;
+        return getFirstHeaderPosition() + 1;
     }
 
-    public int getLastCardPosition() {
+    public int getLastContentItemPosition() {
         return getBottomSpacerPosition() - 1;
     }
 
     public int getBottomSpacerPosition() {
-        return mItems.indexOf(mBottomSpacer);
+        return getGroupPositionOffset(mBottomSpacer);
     }
 
     /** Start a request for new snippets. */
@@ -251,34 +257,23 @@
         SnippetsBridge.fetchSnippets(/*forceRequest=*/true);
     }
 
-    private void loadSnippets(List<SnippetArticleListItem> snippets) {
-        // Copy thumbnails over
-        for (SnippetArticleListItem snippet : snippets) {
-            int existingSnippetIdx = mItems.indexOf(snippet);
-            if (existingSnippetIdx == -1) continue;
+    private void setSuggestions(@KnownCategoriesEnum int category,
+            List<SnippetArticleListItem> suggestions, @CategoryStatusEnum int status) {
+        mGroups.clear();
+        mGroups.add(mAboveTheFold);
 
-            snippet.setThumbnailBitmap(
-                    ((SnippetArticleListItem) mItems.get(existingSnippetIdx)).getThumbnailBitmap());
-        }
-
-        boolean hasContentToShow = !snippets.isEmpty();
-
-        // TODO(mvanouwerkerk): Make it so that the header does not need to be manipulated
-        // separately from the cards to which it belongs - crbug.com/616090.
-        mHeader.setVisible(hasContentToShow);
-
-        mItems.clear();
-        mItems.add(mAboveTheFold);
-        mItems.add(mHeader);
-        if (hasContentToShow) {
-            mItems.addAll(snippets);
+        if (!mSections.containsKey(category)) {
+            mSections.put(category,
+                    new SuggestionsSection(suggestions, status, this));
         } else {
-            mItems.add(mStatusCard);
-            mItems.add(mProgressIndicator);
+            mSections.get(category).setSuggestions(suggestions, status, this);
         }
 
-        mItems.add(mBottomSpacer);
+        mGroups.addAll(mSections.values());
 
+        mGroups.add(mBottomSpacer);
+
+        // TODO(bauerb): Notify about a smaller range.
         notifyDataSetChanged();
     }
 
@@ -300,9 +295,9 @@
         assert itemViewHolder.getItemViewType() == NewTabPageListItem.VIEW_TYPE_SNIPPET;
 
         int position = itemViewHolder.getAdapterPosition();
-        SnippetArticleListItem dismissedSnippet = (SnippetArticleListItem) mItems.get(position);
+        SnippetArticleListItem suggestion = (SnippetArticleListItem) getItems().get(position);
 
-        mSnippetsSource.getSnippedVisited(dismissedSnippet, new Callback<Boolean>() {
+        mSnippetsSource.getSnippedVisited(suggestion, new Callback<Boolean>() {
             @Override
             public void onResult(Boolean result) {
                 NewTabPageUma.recordSnippetAction(result
@@ -311,33 +306,48 @@
             }
         });
 
-        mSnippetsSource.discardSnippet(dismissedSnippet);
-        mItems.remove(position);
-        notifyItemRemoved(position);
+        mSnippetsSource.discardSnippet(suggestion);
+        SuggestionsSection section = (SuggestionsSection) getGroup(position);
+        section.dismissSuggestion(suggestion);
 
-        addStatusCardIfNecessary();
-    }
-
-    private void addStatusCardIfNecessary() {
-        if (!hasSuggestions() && !mItems.contains(mStatusCard)) {
-            mItems.add(getFirstCardPosition(), mStatusCard);
-            mItems.add(getFirstCardPosition() + 1, mProgressIndicator);
-
-            // We also want to refresh the header and the bottom padding.
-            mHeader.setVisible(false);
+        if (section.hasSuggestions()) {
+            // If one of many suggestions was dismissed, it's a simple item removal, which can be
+            // animated smoothly by the RecyclerView.
+            notifyItemRemoved(position);
+        } else {
+            // If the last suggestion was dismissed, multiple items will have changed, so mark
+            // everything as changed.
             notifyDataSetChanged();
         }
     }
 
-    /** Returns whether we have some suggested content to display. */
-    private boolean hasSuggestions() {
-        for (NewTabPageListItem item : mItems) {
-            if (item instanceof SnippetArticleListItem) return true;
+    /**
+     * Returns an unmodifiable list containing all items in the adapter.
+     */
+    List<NewTabPageListItem> getItems() {
+        List<NewTabPageListItem> items = new ArrayList<>();
+        for (ItemGroup group : mGroups) {
+            items.addAll(group.getItems());
         }
-        return false;
+        return Collections.unmodifiableList(items);
     }
 
-    List<NewTabPageListItem> getItemsForTesting() {
-        return mItems;
+    private ItemGroup getGroup(int itemPosition) {
+        int itemsSkipped = 0;
+        for (ItemGroup group : mGroups) {
+            List<NewTabPageListItem> items = group.getItems();
+            itemsSkipped += items.size();
+            if (itemPosition < itemsSkipped) return group;
+        }
+        return null;
+    }
+
+    private int getGroupPositionOffset(ItemGroup group) {
+        int positionOffset = 0;
+        for (ItemGroup candidateGroup : mGroups) {
+            if (candidateGroup == group) return positionOffset;
+            positionOffset += candidateGroup.getItems().size();
+        }
+        return RecyclerView.NO_POSITION;
     }
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageRecyclerView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageRecyclerView.java
index ca9042f..951e988 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageRecyclerView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageRecyclerView.java
@@ -147,12 +147,12 @@
         int firstVisiblePos = mLayoutManager.findFirstVisibleItemPosition();
 
         // We have enough items to fill the view, since the snap point item is not even visible.
-        if (firstVisiblePos > getNewTabPageAdapter().getHeaderPosition()) {
+        if (firstVisiblePos > getNewTabPageAdapter().getFirstHeaderPosition()) {
             return mMinBottomSpacing;
         }
 
         ViewHolder lastContentItem = findLastContentItem();
-        ViewHolder firstHeader = findHeader();
+        ViewHolder firstHeader = findFirstHeader();
 
         int bottomSpacing = getHeight() - mToolbarHeight;
         if (lastContentItem == null || firstHeader == null) {
@@ -199,7 +199,7 @@
      * top of the screen.
      */
     public void updateSnippetsHeaderDisplay() {
-        SnippetHeaderViewHolder header = findHeader();
+        SnippetHeaderViewHolder header = findFirstHeader();
         if (header == null) return;
 
         if (findAboveTheFoldView() == null) return;
@@ -219,9 +219,9 @@
      * Finds the view holder for the first header.
      * @return The {@link ViewHolder} of the header, or null if it is not present.
      */
-    private SnippetHeaderViewHolder findHeader() {
+    private SnippetHeaderViewHolder findFirstHeader() {
         ViewHolder viewHolder =
-                findViewHolderForAdapterPosition(getNewTabPageAdapter().getHeaderPosition());
+                findViewHolderForAdapterPosition(getNewTabPageAdapter().getFirstHeaderPosition());
         if (!(viewHolder instanceof SnippetHeaderViewHolder)) return null;
 
         return (SnippetHeaderViewHolder) viewHolder;
@@ -244,8 +244,8 @@
      * @return The {@link ViewHolder} of the last content item, or null if it is not present.
      */
     private ViewHolder findLastContentItem() {
-        ViewHolder viewHolder =
-                findViewHolderForAdapterPosition(getNewTabPageAdapter().getLastCardPosition());
+        ViewHolder viewHolder = findViewHolderForAdapterPosition(
+                getNewTabPageAdapter().getLastContentItemPosition());
         if (viewHolder instanceof CardViewHolder) return viewHolder;
         if (viewHolder instanceof ProgressViewHolder) return viewHolder;
 
@@ -329,13 +329,13 @@
 
         // Snap scroll to prevent resting in the middle of the peeking card transition
         // and to allow the peeking card to peek a bit before snapping back.
-        if (findFirstCard() != null && isFirstItemVisible()) {
-            CardViewHolder peekingCardViewHolder = findFirstCard();
+        CardViewHolder peekingCardViewHolder = findFirstCard();
+        if (peekingCardViewHolder != null && isFirstItemVisible()) {
 
             if (!peekingCardViewHolder.getCanPeek()) return;
 
-            View peekingCardView = findFirstCard().itemView;
-            View headerView = findHeader().itemView;
+            View peekingCardView = peekingCardViewHolder.itemView;
+            View headerView = findFirstHeader().itemView;
             final int peekingHeight = getResources().getDimensionPixelSize(
                     R.dimen.snippets_padding_and_peeking_card_height);
 
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SingleItemGroup.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SingleItemGroup.java
new file mode 100644
index 0000000..dd93ff7
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SingleItemGroup.java
@@ -0,0 +1,21 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ntp.cards;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A single item that represents itself as a group.
+ */
+public abstract class SingleItemGroup implements ItemGroup, NewTabPageListItem {
+    private final List<NewTabPageListItem> mItems =
+            Collections.<NewTabPageListItem>singletonList(this);
+
+    @Override
+    public List<NewTabPageListItem> getItems() {
+        return mItems;
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SpacingListItem.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SpacingListItem.java
index 2153d09..e23c0b9 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SpacingListItem.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SpacingListItem.java
@@ -13,7 +13,7 @@
  * contain enough of them. It is displayed as a dummy item with variable height that just occupies
  * the remaining space between the last item in the RecyclerView and the bottom of the screen.
  */
-public class SpacingListItem implements NewTabPageListItem {
+public class SpacingListItem extends SingleItemGroup {
     private static class SpacingListItemView extends View {
         public SpacingListItemView(Context context) {
             super(context);
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/StatusListItem.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/StatusListItem.java
index 6e05119..dac9ab5 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/StatusListItem.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/StatusListItem.java
@@ -14,6 +14,7 @@
 import org.chromium.chrome.R;
 import org.chromium.chrome.browser.ntp.UiConfig;
 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
 import org.chromium.chrome.browser.signin.AccountSigninActivity;
 import org.chromium.chrome.browser.signin.SigninAccessPoint;
 
@@ -135,7 +136,8 @@
     private final int mDescriptionStringId;
     private final int mActionStringId;
 
-    public static StatusListItem create(int categoryStatus, NewTabPageAdapter adapter) {
+    public static StatusListItem create(@CategoryStatusEnum int categoryStatus,
+            NewTabPageAdapter adapter) {
         switch (categoryStatus) {
             // TODO(dgn): AVAILABLE_LOADING and INITIALIZING should show a progress indicator.
             case CategoryStatus.AVAILABLE:
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java
new file mode 100644
index 0000000..f3d98d2
--- /dev/null
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java
@@ -0,0 +1,76 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package org.chromium.chrome.browser.ntp.cards;
+
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
+import org.chromium.chrome.browser.ntp.snippets.SnippetArticleListItem;
+import org.chromium.chrome.browser.ntp.snippets.SnippetHeaderListItem;
+import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A group of suggestions, with a header, a status card, and a progress indicator.
+ */
+public class SuggestionsSection implements ItemGroup {
+    private final List<SnippetArticleListItem> mSuggestions = new ArrayList<>();
+    private final SnippetHeaderListItem mHeader = new SnippetHeaderListItem();
+    private StatusListItem mStatus;
+    private final ProgressListItem mProgressIndicator = new ProgressListItem();
+
+    public SuggestionsSection(List<SnippetArticleListItem> suggestions,
+            @CategoryStatusEnum int status, NewTabPageAdapter adapter) {
+        // TODO(mvanouwerkerk): Pass in the header text.
+        setSuggestions(suggestions, status, adapter);
+    }
+
+    @Override
+    public List<NewTabPageListItem> getItems() {
+        List<NewTabPageListItem> items = new ArrayList<>();
+        items.add(mHeader);
+        items.addAll(mSuggestions);
+        if (mSuggestions.isEmpty()) {
+            items.add(mStatus);
+            items.add(mProgressIndicator);
+        }
+        return Collections.unmodifiableList(items);
+    }
+
+    public void dismissSuggestion(SnippetArticleListItem suggestion) {
+        mSuggestions.remove(suggestion);
+
+        if (mSuggestions.isEmpty()) {
+            mHeader.setVisible(false);
+        }
+    }
+
+    public boolean hasSuggestions() {
+        return !mSuggestions.isEmpty();
+    }
+
+    public void setSuggestions(List<SnippetArticleListItem> suggestions,
+            @CategoryStatusEnum int status, NewTabPageAdapter adapter) {
+        copyThumbnails(suggestions);
+
+        mHeader.setVisible(!suggestions.isEmpty());
+
+        mStatus = StatusListItem.create(status, adapter);
+        mProgressIndicator.setVisible(SnippetsBridge.isCategoryLoading(status));
+
+        mSuggestions.clear();
+        mSuggestions.addAll(suggestions);
+    }
+
+    private void copyThumbnails(List<SnippetArticleListItem> suggestions) {
+        for (SnippetArticleListItem suggestion : suggestions) {
+            int index = mSuggestions.indexOf(suggestion);
+            if (index == -1) continue;
+
+            suggestion.setThumbnailBitmap(mSuggestions.get(index).getThumbnailBitmap());
+        }
+    }
+}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java
index 00e6715..e10051f 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java
@@ -13,14 +13,31 @@
  * Represents the data for an article card on the NTP.
  */
 public class SnippetArticleListItem implements NewTabPageListItem {
+    /** The unique identifier for this article. */
     public final String mId;
+
+    /** The title of this article. */
     public final String mTitle;
+
+    /** The canonical publisher name (e.g., New York Times). */
     public final String mPublisher;
+
+    /** The snippet preview text. */
     public final String mPreviewText;
+
+    /** The URL of this article. */
     public final String mUrl;
+
+    /** the AMP url for this article (possible for this to be empty). */
     public final String mAmpUrl;
+
+    /** The time when this article was published. */
     public final long mPublishTimestampMilliseconds;
+
+    /** The score expressing relative quality of the article for the user. */
     public final float mScore;
+
+    /** The position of this article in the whole list of snippets. */
     public final int mPosition;
 
     /** Bitmap of the thumbnail, fetched lazily, when the RecyclerView wants to show the snippet. */
@@ -34,14 +51,6 @@
 
     /**
      * Creates a SnippetArticleListItem object that will hold the data.
-     * @param title the title of the article
-     * @param publisher the canonical publisher name (e.g., New York Times)
-     * @param previewText the snippet preview text
-     * @param url the URL of the article
-     * @param ampUrl the AMP url for the article (possible for this to be empty)
-     * @param timestamp the time in ms when this article was published
-     * @param score the score expressing relative quality of the article for the user
-     * @param position the position of this article in the list of snippets
      */
     public SnippetArticleListItem(String id, String title, String publisher, String previewText,
             String url, String ampUrl, long timestamp, float score, int position) {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java
index 8e79401..98a8415 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsBridge.java
@@ -8,6 +8,8 @@
 
 import org.chromium.base.Callback;
 import org.chromium.base.annotations.CalledByNative;
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories.KnownCategoriesEnum;
 import org.chromium.chrome.browser.profiles.Profile;
 
 import java.util.ArrayList;
@@ -22,17 +24,17 @@
     private long mNativeSnippetsBridge;
     private SnippetsObserver mObserver;
 
-    public static boolean isCategoryStatusAvailable(int status) {
+    public static boolean isCategoryStatusAvailable(@CategoryStatusEnum int status) {
         // Note: This code is duplicated in content_suggestions_category_status.cc.
         return status == CategoryStatus.AVAILABLE_LOADING || status == CategoryStatus.AVAILABLE;
     }
 
-    public static boolean isCategoryStatusInitOrAvailable(int status) {
+    public static boolean isCategoryStatusInitOrAvailable(@CategoryStatusEnum int status) {
         // Note: This code is duplicated in content_suggestions_category_status.cc.
         return status == CategoryStatus.INITIALIZING || isCategoryStatusAvailable(status);
     }
 
-    public static boolean isCategoryLoading(int status) {
+    public static boolean isCategoryLoading(@CategoryStatusEnum int status) {
         return status == CategoryStatus.AVAILABLE_LOADING || status == CategoryStatus.INITIALIZING;
     }
 
@@ -118,29 +120,39 @@
     }
 
     @Override
-    public int getCategoryStatus() {
+    @CategoryStatusEnum
+    public int getCategoryStatus(@KnownCategoriesEnum int category) {
         assert mNativeSnippetsBridge != 0;
-        return nativeGetCategoryStatus(mNativeSnippetsBridge);
+        return nativeGetCategoryStatus(mNativeSnippetsBridge, category);
     }
 
     @CalledByNative
-    private void onSnippetsAvailable(String[] ids, String[] titles, String[] urls, String[] ampUrls,
-            String[] previewText, long[] timestamps, String[] publishers, float[] scores) {
+    private static List<SnippetArticleListItem> createSuggestionList() {
+        return new ArrayList<>();
+    }
+
+    @CalledByNative
+    private static void addSuggestion(List<SnippetArticleListItem> suggestions, String id,
+            String title, String publisher, String previewText, String url, String ampUrl,
+            long timestamp, float score) {
+        int position = suggestions.size();
+        suggestions.add(new SnippetArticleListItem(id, title, publisher, previewText, url, ampUrl,
+                timestamp, score, position));
+    }
+
+    @CalledByNative
+    private void onSuggestionsAvailable(/* @KnownCategoriesEnum */ int category,
+            List<SnippetArticleListItem> suggestions) {
         assert mNativeSnippetsBridge != 0;
         assert mObserver != null;
 
-        List<SnippetArticleListItem> newSnippets = new ArrayList<>(ids.length);
-        for (int i = 0; i < ids.length; i++) {
-            newSnippets.add(new SnippetArticleListItem(ids[i], titles[i], publishers[i],
-                    previewText[i], urls[i], ampUrls[i], timestamps[i], scores[i], i));
-        }
-
-        mObserver.onSnippetsReceived(newSnippets);
+        mObserver.onSuggestionsReceived(category, suggestions);
     }
 
     @CalledByNative
-    private void onCategoryStatusChanged(int newStatus) {
-        if (mObserver != null) mObserver.onCategoryStatusChanged(newStatus);
+    private void onCategoryStatusChanged(/* @KnownCategoriesEnum */ int category,
+            /* @CategoryStatusEnum */ int newStatus) {
+        if (mObserver != null) mObserver.onCategoryStatusChanged(category, newStatus);
     }
 
     private native long nativeInit(Profile profile);
@@ -153,5 +165,5 @@
             Callback<Boolean> callback, String url);
     private native void nativeFetchImage(
             long nativeNTPSnippetsBridge, String snippetId, Callback<Bitmap> callback);
-    private native int nativeGetCategoryStatus(long nativeNTPSnippetsBridge);
+    private native int nativeGetCategoryStatus(long nativeNTPSnippetsBridge, int category);
 }
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsSource.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsSource.java
index 5440249..baab92e4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsSource.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsSource.java
@@ -7,22 +7,25 @@
 import android.graphics.Bitmap;
 
 import org.chromium.base.Callback;
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories.KnownCategoriesEnum;
 
 import java.util.List;
 
-
 /**
- * An interface for classes that provide Snippets.
+ * An interface for classes that provide snippets.
  */
 public interface SnippetsSource {
     /**
      * An observer for events in the snippets service.
      */
     public interface SnippetsObserver {
-        void onSnippetsReceived(List<SnippetArticleListItem> snippets);
+        void onSuggestionsReceived(@KnownCategoriesEnum int category,
+                List<SnippetArticleListItem> snippets);
 
-        /** Called when the ARTICLES category changed its status. */
-        void onCategoryStatusChanged(int newStatus);
+        /** Called when a category changed its status. */
+        void onCategoryStatusChanged(@KnownCategoriesEnum int category,
+                @CategoryStatusEnum int newStatus);
     }
 
     /**
@@ -48,5 +51,5 @@
     /**
      * Gives the reason snippets are disabled.
      */
-    public int getCategoryStatus();
+    public int getCategoryStatus(@KnownCategoriesEnum int category);
 }
\ No newline at end of file
diff --git a/chrome/android/java_sources.gni b/chrome/android/java_sources.gni
index d5149ae8..8f364be 100644
--- a/chrome/android/java_sources.gni
+++ b/chrome/android/java_sources.gni
@@ -528,8 +528,8 @@
   "java/src/org/chromium/chrome/browser/ntp/RecentTabsRowAdapter.java",
   "java/src/org/chromium/chrome/browser/ntp/RecentlyClosedBridge.java",
   "java/src/org/chromium/chrome/browser/ntp/TitleUtil.java",
-  "java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java",
   "java/src/org/chromium/chrome/browser/ntp/UiConfig.java",
+  "java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleListItem.java",
   "java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleViewHolder.java",
   "java/src/org/chromium/chrome/browser/ntp/snippets/SnippetHeaderListItem.java",
   "java/src/org/chromium/chrome/browser/ntp/snippets/SnippetHeaderViewHolder.java",
@@ -540,6 +540,7 @@
   "java/src/org/chromium/chrome/browser/ntp/cards/AboveTheFoldListItem.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/CardViewHolder.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/DisplayStyleObserverAdapter.java",
+  "java/src/org/chromium/chrome/browser/ntp/cards/ItemGroup.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/MarginResizer.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageListItem.java",
@@ -548,8 +549,10 @@
   "java/src/org/chromium/chrome/browser/ntp/cards/ProgressIndicatorView.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/ProgressViewHolder.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/ProgressListItem.java",
+  "java/src/org/chromium/chrome/browser/ntp/cards/SingleItemGroup.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/SpacingListItem.java",
   "java/src/org/chromium/chrome/browser/ntp/cards/StatusListItem.java",
+  "java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java",
   "java/src/org/chromium/chrome/browser/offlinepages/DeviceConditions.java",
   "java/src/org/chromium/chrome/browser/offlinepages/BackgroundOfflinerTask.java",
   "java/src/org/chromium/chrome/browser/offlinepages/BackgroundScheduler.java",
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
index c2e8c066..77970749 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
@@ -7,28 +7,29 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.any;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
 
+import android.graphics.Bitmap;
+
+import org.chromium.base.Callback;
 import org.chromium.base.metrics.RecordHistogram;
 import org.chromium.base.test.util.Feature;
-import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
 import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
+import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories;
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories.KnownCategoriesEnum;
 import org.chromium.chrome.browser.ntp.snippets.SnippetArticleListItem;
-import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge;
-import org.chromium.chrome.browser.ntp.snippets.SnippetsSource.SnippetsObserver;
+import org.chromium.chrome.browser.ntp.snippets.SnippetsSource;
 import org.chromium.testing.local.LocalRobolectricTestRunner;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 import org.robolectric.annotation.Config;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 
 /**
@@ -37,25 +38,58 @@
 @RunWith(LocalRobolectricTestRunner.class)
 @Config(manifest = Config.NONE)
 public class NewTabPageAdapterTest {
+    private static class FakeSnippetsSource implements SnippetsSource {
+        private SnippetsSource.SnippetsObserver mObserver;
+        private final Map<Integer, Integer> mCategoryStatus = new HashMap<>();
 
-    private NewTabPageManager mNewTabPageManager;
-    private SnippetsObserver mSnippetsObserver;
-    private SnippetsBridge mSnippetsBridge;
+        public void setStatusForCategory(@KnownCategoriesEnum int category,
+                @CategoryStatusEnum int status) {
+            mCategoryStatus.put(category, status);
+            if (mObserver != null) mObserver.onCategoryStatusChanged(category, status);
+        }
+
+        public void setSnippetsForCategory(@KnownCategoriesEnum int category,
+                List<SnippetArticleListItem> snippets) {
+            mObserver.onSuggestionsReceived(category, snippets);
+        }
+
+        @Override
+        public void discardSnippet(SnippetArticleListItem snippet) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void fetchSnippetImage(SnippetArticleListItem snippet, Callback<Bitmap> callback) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void getSnippedVisited(SnippetArticleListItem snippet, Callback<Boolean> callback) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void setObserver(SnippetsObserver observer) {
+            mObserver = observer;
+        }
+
+        @CategoryStatusEnum
+        @Override
+        public int getCategoryStatus(@KnownCategoriesEnum int category) {
+            return mCategoryStatus.get(category);
+        }
+    }
+
+    private FakeSnippetsSource mSnippetsSource = new FakeSnippetsSource();
+    private NewTabPageAdapter mNtpAdapter;
 
     @Before
     public void setUp() {
         RecordHistogram.disableForTests();
-        mNewTabPageManager = mock(NewTabPageManager.class);
-        mSnippetsObserver = null;
-        mSnippetsBridge = mock(SnippetsBridge.class);
 
-        // Intercept the observers so that we can mock invocations.
-        doAnswer(new Answer<Void>() {
-            @Override
-            public Void answer(InvocationOnMock invocation) throws Throwable {
-                mSnippetsObserver = invocation.getArgumentAt(0, SnippetsObserver.class);
-                return null;
-            }}).when(mSnippetsBridge).setObserver(any(SnippetsObserver.class));
+        mSnippetsSource = new FakeSnippetsSource();
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES, CategoryStatus.INITIALIZING);
+        mNtpAdapter = new NewTabPageAdapter(null, null, mSnippetsSource, null);
     }
 
     /**
@@ -65,31 +99,28 @@
     @Test
     @Feature({"Ntp"})
     public void testSnippetLoading() {
-        NewTabPageAdapter ntpa =
-                new NewTabPageAdapter(mNewTabPageManager, null, mSnippetsBridge, null);
-
-        assertEquals(5, ntpa.getItemCount());
-        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, ntpa.getItemViewType(0));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, ntpa.getItemViewType(1));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_STATUS, ntpa.getItemViewType(2));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_PROGRESS, ntpa.getItemViewType(3));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING, ntpa.getItemViewType(4));
+        assertEquals(5, mNtpAdapter.getItemCount());
+        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, mNtpAdapter.getItemViewType(0));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, mNtpAdapter.getItemViewType(1));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_STATUS, mNtpAdapter.getItemViewType(2));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_PROGRESS, mNtpAdapter.getItemViewType(3));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING, mNtpAdapter.getItemViewType(4));
 
         List<SnippetArticleListItem> snippets = createDummySnippets();
-        mSnippetsObserver.onSnippetsReceived(snippets);
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
 
-        List<NewTabPageListItem> loadedItems = new ArrayList<>(ntpa.getItemsForTesting());
-        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, ntpa.getItemViewType(0));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, ntpa.getItemViewType(1));
+        List<NewTabPageListItem> loadedItems = new ArrayList<>(mNtpAdapter.getItems());
+        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, mNtpAdapter.getItemViewType(0));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, mNtpAdapter.getItemViewType(1));
         assertEquals(snippets, loadedItems.subList(2, loadedItems.size() - 1));
-        assertEquals(
-                NewTabPageListItem.VIEW_TYPE_SPACING, ntpa.getItemViewType(loadedItems.size() - 1));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING,
+                mNtpAdapter.getItemViewType(loadedItems.size() - 1));
 
         // The adapter should ignore any new incoming data.
-        mSnippetsObserver.onSnippetsReceived(
-                Arrays.asList(new SnippetArticleListItem[] {new SnippetArticleListItem(
-                        "foo", "title1", "pub1", "txt1", "foo", "bar", 0, 0, 0)}));
-        assertEquals(loadedItems, ntpa.getItemsForTesting());
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES,
+                Arrays.asList(new SnippetArticleListItem[] { new SnippetArticleListItem(
+                        "foo", "title1", "pub1", "txt1", "foo", "bar", 0, 0, 0) }));
+        assertEquals(loadedItems, mNtpAdapter.getItems());
     }
 
     /**
@@ -99,33 +130,32 @@
     @Test
     @Feature({"Ntp"})
     public void testSnippetLoadingInitiallyEmpty() {
-        NewTabPageAdapter ntpa =
-                new NewTabPageAdapter(mNewTabPageManager, null, mSnippetsBridge, null);
-
         // If we don't get anything, we should be in the same situation as the initial one.
-        mSnippetsObserver.onSnippetsReceived(new ArrayList<SnippetArticleListItem>());
-        assertEquals(5, ntpa.getItemCount());
-        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, ntpa.getItemViewType(0));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, ntpa.getItemViewType(1));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_STATUS, ntpa.getItemViewType(2));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_PROGRESS, ntpa.getItemViewType(3));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING, ntpa.getItemViewType(4));
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES,
+                new ArrayList<SnippetArticleListItem>());
+        assertEquals(5, mNtpAdapter.getItemCount());
+        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, mNtpAdapter.getItemViewType(0));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, mNtpAdapter.getItemViewType(1));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_STATUS, mNtpAdapter.getItemViewType(2));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_PROGRESS, mNtpAdapter.getItemViewType(3));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING, mNtpAdapter.getItemViewType(4));
 
         // We should load new snippets when we get notified about them.
         List<SnippetArticleListItem> snippets = createDummySnippets();
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        List<NewTabPageListItem> loadedItems = new ArrayList<>(ntpa.getItemsForTesting());
-        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, ntpa.getItemViewType(0));
-        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, ntpa.getItemViewType(1));
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        List<NewTabPageListItem> loadedItems = new ArrayList<>(mNtpAdapter.getItems());
+        assertEquals(NewTabPageListItem.VIEW_TYPE_ABOVE_THE_FOLD, mNtpAdapter.getItemViewType(0));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_HEADER, mNtpAdapter.getItemViewType(1));
         assertEquals(snippets, loadedItems.subList(2, loadedItems.size() - 1));
-        assertEquals(
-                NewTabPageListItem.VIEW_TYPE_SPACING, ntpa.getItemViewType(loadedItems.size() - 1));
+        assertEquals(NewTabPageListItem.VIEW_TYPE_SPACING,
+                mNtpAdapter.getItemViewType(loadedItems.size() - 1));
 
         // The adapter should ignore any new incoming data.
-        mSnippetsObserver.onSnippetsReceived(
+        mSnippetsSource.setSnippetsForCategory(
+                KnownCategories.ARTICLES,
                 Arrays.asList(new SnippetArticleListItem[] {new SnippetArticleListItem(
                         "foo", "title1", "pub1", "txt1", "foo", "bar", 0, 0, 0)}));
-        assertEquals(loadedItems, ntpa.getItemsForTesting());
+        assertEquals(loadedItems, mNtpAdapter.getItems());
     }
 
     /**
@@ -134,27 +164,27 @@
     @Test
     @Feature({"Ntp"})
     public void testSnippetClearing() {
-        NewTabPageAdapter ntpa =
-                new NewTabPageAdapter(mNewTabPageManager, null, mSnippetsBridge, null);
-
         List<SnippetArticleListItem> snippets = createDummySnippets();
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
 
         // If we get told that snippets are enabled, we just leave the current
         // ones there and not clear.
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.AVAILABLE);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.AVAILABLE);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
 
         // When snippets are disabled, we clear them and we should go back to
         // the situation with the status card.
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.SIGNED_OUT);
-        assertEquals(5, ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.SIGNED_OUT);
+        assertEquals(5, mNtpAdapter.getItemCount());
 
         // The adapter should now be waiting for new snippets.
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.AVAILABLE);
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.AVAILABLE);
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
     }
 
     /**
@@ -163,35 +193,35 @@
     @Test
     @Feature({"Ntp"})
     public void testSnippetLoadingBlock() {
-        NewTabPageAdapter ntpa =
-                new NewTabPageAdapter(mNewTabPageManager, null, mSnippetsBridge, null);
-
         List<SnippetArticleListItem> snippets = createDummySnippets();
 
         // By default, status is INITIALIZING, so we can load snippets
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
 
         // If we have snippets, we should not load the new list.
         snippets.add(new SnippetArticleListItem("https://site.com/url1", "title1", "pub1", "txt1",
                 "https://site.com/url1", "https://amp.site.com/url1", 0, 0, 0));
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size() - 1, ntpa.getItemCount());
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size() - 1, mNtpAdapter.getItemCount());
 
         // When snippets are disabled, we should not be able to load them
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.SIGNED_OUT);
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(5, ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.SIGNED_OUT);
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(5, mNtpAdapter.getItemCount());
 
         // INITIALIZING lets us load snippets still.
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.INITIALIZING);
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.INITIALIZING);
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
 
         // The adapter should now be waiting for new snippets.
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.AVAILABLE);
-        mSnippetsObserver.onSnippetsReceived(snippets);
-        assertEquals(3 + snippets.size(), ntpa.getItemCount());
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.AVAILABLE);
+        mSnippetsSource.setSnippetsForCategory(KnownCategories.ARTICLES, snippets);
+        assertEquals(3 + snippets.size(), mNtpAdapter.getItemCount());
     }
 
     /**
@@ -200,30 +230,35 @@
     @Test
     @Feature({"Ntp"})
     public void testProgressIndicatorDisplay() {
-        NewTabPageAdapter ntpa =
-                new NewTabPageAdapter(mNewTabPageManager, null, mSnippetsBridge, null);
-        int progressPos = ntpa.getBottomSpacerPosition() - 1;
-        ProgressListItem progress = (ProgressListItem) ntpa.getItemsForTesting().get(progressPos);
+        int progressPos = mNtpAdapter.getBottomSpacerPosition() - 1;
+        ProgressListItem progress = (ProgressListItem) mNtpAdapter.getItems().get(progressPos);
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.INITIALIZING);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.INITIALIZING);
         assertTrue(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.AVAILABLE);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.AVAILABLE);
         assertFalse(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.AVAILABLE_LOADING);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.AVAILABLE_LOADING);
         assertTrue(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.NOT_PROVIDED);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.NOT_PROVIDED);
         assertFalse(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.CATEGORY_EXPLICITLY_DISABLED);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.CATEGORY_EXPLICITLY_DISABLED);
         assertFalse(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.SIGNED_OUT);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.SIGNED_OUT);
         assertFalse(progress.isVisible());
 
-        mSnippetsObserver.onCategoryStatusChanged(CategoryStatus.LOADING_ERROR);
+        mSnippetsSource.setStatusForCategory(KnownCategories.ARTICLES,
+                CategoryStatus.LOADING_ERROR);
         assertFalse(progress.isVisible());
     }
 
diff --git a/chrome/browser/android/ntp/ntp_snippets_bridge.cc b/chrome/browser/android/ntp/ntp_snippets_bridge.cc
index d8e8110..a8d487da 100644
--- a/chrome/browser/android/ntp/ntp_snippets_bridge.cc
+++ b/chrome/browser/android/ntp/ntp_snippets_bridge.cc
@@ -25,10 +25,9 @@
 
 using base::android::AttachCurrentThread;
 using base::android::ConvertJavaStringToUTF8;
+using base::android::ConvertUTF8ToJavaString;
+using base::android::ConvertUTF16ToJavaString;
 using base::android::JavaParamRef;
-using base::android::ToJavaArrayOfStrings;
-using base::android::ToJavaLongArray;
-using base::android::ToJavaFloatArray;
 using base::android::ScopedJavaGlobalRef;
 using base::android::ScopedJavaLocalRef;
 using ntp_snippets::Category;
@@ -129,7 +128,8 @@
 }
 
 int NTPSnippetsBridge::GetCategoryStatus(JNIEnv* env,
-                                         const JavaParamRef<jobject>& obj) {
+                                         const JavaParamRef<jobject>& obj,
+                                         jint category) {
   return static_cast<int>(content_suggestions_service_->GetCategoryStatus(
       content_suggestions_service_->category_factory()->FromKnownCategory(
           KnownCategories::ARTICLES)));
@@ -141,62 +141,51 @@
   if (observer_.is_null())
     return;
 
-  std::vector<std::string> ids;
-  std::vector<base::string16> titles;
-  // URL for the article. This will also be used to find the favicon for the
-  // article.
-  std::vector<std::string> urls;
-  // URL for the AMP version of the article if it exists. This will be used as
-  // the URL to direct the user to on tap.
-  std::vector<std::string> amp_urls;
-  std::vector<base::string16> snippet_texts;
-  std::vector<int64_t> timestamps;
-  std::vector<base::string16> publisher_names;
-  std::vector<float> scores;
-
   // Show all suggestions from all categories, even though we currently display
   // them in a single section on the UI.
   // TODO(pke): This is only for debugging new sections and will be replaced
   // with proper multi-section UI support.
-  for (Category category : content_suggestions_service_->GetCategories()) {
+  JNIEnv* env = base::android::AttachCurrentThread();
+  ScopedJavaLocalRef<jobject> suggestions =
+      Java_SnippetsBridge_createSuggestionList(env);
+  for (Category category :
+       content_suggestions_service_->GetCategories()) {
     if (content_suggestions_service_->GetCategoryStatus(category) !=
         CategoryStatus::AVAILABLE) {
       continue;
     }
     for (const ntp_snippets::ContentSuggestion& suggestion :
          content_suggestions_service_->GetSuggestionsForCategory(category)) {
-      ids.push_back(suggestion.id());
-      titles.push_back(suggestion.title());
-      // The url from source_info is a url for a site that is one of the
-      // HOST_RESTRICT parameters, so this is preferred.
-      urls.push_back(suggestion.url().spec());
-      amp_urls.push_back(suggestion.amp_url().spec());
-      snippet_texts.push_back(suggestion.snippet_text());
-      timestamps.push_back(suggestion.publish_date().ToJavaTime());
-      publisher_names.push_back(suggestion.publisher_name());
-      scores.push_back(suggestion.score());
+      Java_SnippetsBridge_addSuggestion(
+          env, suggestions.obj(),
+          ConvertUTF8ToJavaString(env, suggestion.id()).obj(),
+          ConvertUTF16ToJavaString(env, suggestion.title()).obj(),
+          ConvertUTF16ToJavaString(env, suggestion.publisher_name()).obj(),
+          ConvertUTF16ToJavaString(env, suggestion.snippet_text()).obj(),
+          ConvertUTF8ToJavaString(env, suggestion.url().spec()).obj(),
+          ConvertUTF8ToJavaString(env, suggestion.amp_url().spec()).obj(),
+          suggestion.publish_date().ToJavaTime(), suggestion.score());
     }
   }
 
-  JNIEnv* env = base::android::AttachCurrentThread();
-  Java_SnippetsBridge_onSnippetsAvailable(
-      env, observer_.obj(), ToJavaArrayOfStrings(env, ids).obj(),
-      ToJavaArrayOfStrings(env, titles).obj(),
-      ToJavaArrayOfStrings(env, urls).obj(),
-      ToJavaArrayOfStrings(env, amp_urls).obj(),
-      ToJavaArrayOfStrings(env, snippet_texts).obj(),
-      ToJavaLongArray(env, timestamps).obj(),
-      ToJavaArrayOfStrings(env, publisher_names).obj(),
-      ToJavaFloatArray(env, scores).obj());
+  // TODO(mvanouwerkerk): Do not hard code ARTICLES.
+  Java_SnippetsBridge_onSuggestionsAvailable(
+      env, observer_.obj(),
+      static_cast<int>(
+          content_suggestions_service_->category_factory()->FromKnownCategory(
+              KnownCategories::ARTICLES).id()),
+      suggestions.obj());
 }
 
 void NTPSnippetsBridge::OnCategoryStatusChanged(Category category,
                                                 CategoryStatus new_status) {
+  // TODO(mvanouwerkerk): Do not hard code ARTICLES.
   if (!category.IsKnownCategory(KnownCategories::ARTICLES))
     return;
 
   JNIEnv* env = base::android::AttachCurrentThread();
   Java_SnippetsBridge_onCategoryStatusChanged(env, observer_.obj(),
+                                              static_cast<int>(category.id()),
                                               static_cast<int>(new_status));
 }
 
diff --git a/chrome/browser/android/ntp/ntp_snippets_bridge.h b/chrome/browser/android/ntp/ntp_snippets_bridge.h
index df93620..5c2715d 100644
--- a/chrome/browser/android/ntp/ntp_snippets_bridge.h
+++ b/chrome/browser/android/ntp/ntp_snippets_bridge.h
@@ -49,10 +49,11 @@
                       const base::android::JavaParamRef<jobject>& callback,
                       const base::android::JavaParamRef<jstring>& jurl);
 
-  // Returns the status of the ARTICLES category.
+  // Returns the status of |category|.
   // See CategoryStatus for more info.
   int GetCategoryStatus(JNIEnv* env,
-                        const base::android::JavaParamRef<jobject>& obj);
+                        const base::android::JavaParamRef<jobject>& obj,
+                        jint category);
 
   static bool Register(JNIEnv* env);
 
diff --git a/components/ntp_snippets/BUILD.gn b/components/ntp_snippets/BUILD.gn
index 59e4c270..79449b73 100644
--- a/components/ntp_snippets/BUILD.gn
+++ b/components/ntp_snippets/BUILD.gn
@@ -83,6 +83,7 @@
 if (is_android) {
   java_cpp_enum("ntp_snippets_java_enums_srcjar") {
     sources = [
+      "category.h",
       "category_status.h",
     ]
   }
diff --git a/components/ntp_snippets/category.h b/components/ntp_snippets/category.h
index 877097e..0991ab2 100644
--- a/components/ntp_snippets/category.h
+++ b/components/ntp_snippets/category.h
@@ -16,13 +16,22 @@
 // locally on the device. Categories provided by the server (IDs strictly larger
 // than REMOTE_CATEGORIES_OFFSET) only need to be hard-coded here if they need
 // to be recognized by the client implementation.
+// On Android builds, a Java counterpart will be generated for this enum.
+// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.ntp.snippets
 enum class KnownCategories {
+  // Pages downloaded for offline consumption.
   OFFLINE_PAGES,
+
+  // Recently used bookmarks.
   BOOKMARKS,
+
+  // Follows the last local category.
   LOCAL_CATEGORIES_COUNT,
 
   REMOTE_CATEGORIES_OFFSET = 10000,
-  ARTICLES = REMOTE_CATEGORIES_OFFSET + 1,
+
+  // Articles for you.
+  ARTICLES,
 };
 
 // A category groups ContentSuggestions which belong together. Use the