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