Index: chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
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 723636d10b3c3e4ed074aa9d1b61c30d170242a9..3348fcc0d3f1bb9e30ef2bc693e78ee73a5bfe89 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 |
@@ -17,6 +17,7 @@ |
import org.chromium.chrome.browser.ntp.NewTabPageUma; |
import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; |
import org.chromium.chrome.browser.ntp.UiConfig; |
+import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsCategory; |
import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsCategoryStatus; |
import org.chromium.chrome.browser.ntp.snippets.SnippetArticleListItem; |
import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder; |
@@ -24,9 +25,14 @@ |
import org.chromium.chrome.browser.ntp.snippets.SnippetHeaderViewHolder; |
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; |
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge.SnippetsObserver; |
+import org.chromium.chrome.browser.ntp.snippets.SuggestionsCategory; |
+import org.chromium.chrome.browser.ntp.snippets.SuggestionsCategoryStatus; |
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 |
@@ -39,17 +45,17 @@ |
private final NewTabPageManager mNewTabPageManager; |
private final NewTabPageLayout mNewTabPageLayout; |
- private final AboveTheFoldListItem mAboveTheFold; |
- private final SnippetHeaderListItem mHeader; |
+ private final SnippetsBridge mSnippetsBridge; |
private final UiConfig mUiConfig; |
- private StatusListItem mStatusCard; |
- private final SpacingListItem mBottomSpacer; |
- private final List<NewTabPageListItem> mItems; |
private final ItemTouchCallbacks mItemTouchCallbacks; |
private NewTabPageRecyclerView mRecyclerView; |
- private int mProviderStatus; |
- private SnippetsBridge mSnippetsBridge; |
+ private final List<NewTabPageItemGroup> mGroups; |
+ private final AboveTheFoldListItem mAboveTheFold; |
+ private final SpacingListItem mBottomSpacer; |
+ |
+ // Maps suggestion categories to sections, with stable iteration ordering. |
+ private final Map<Integer, SuggestionsSection> mSections; |
private class ItemTouchCallbacks extends ItemTouchHelper.Callback { |
@Override |
@@ -57,7 +63,6 @@ public void onSwiped(ViewHolder viewHolder, int direction) { |
mRecyclerView.onItemDismissStarted(viewHolder.itemView); |
NewTabPageAdapter.this.dismissItem(viewHolder); |
- addStatusCardIfNecessary(); |
} |
@Override |
@@ -113,17 +118,20 @@ public NewTabPageAdapter(NewTabPageManager manager, NewTabPageLayout newTabPageL |
SnippetsBridge snippetsBridge, UiConfig uiConfig) { |
mNewTabPageManager = manager; |
mNewTabPageLayout = newTabPageLayout; |
- mAboveTheFold = new AboveTheFoldListItem(); |
- mHeader = new SnippetHeaderListItem(); |
- mBottomSpacer = new SpacingListItem(); |
- mItemTouchCallbacks = new ItemTouchCallbacks(); |
- mItems = new ArrayList<>(); |
- mProviderStatus = ContentSuggestionsCategoryStatus.INITIALIZING; |
mSnippetsBridge = snippetsBridge; |
mUiConfig = uiConfig; |
- mStatusCard = StatusListItem.create(snippetsBridge.getCategoryStatus(), this, manager); |
+ mItemTouchCallbacks = new ItemTouchCallbacks(); |
- loadSnippets(new ArrayList<SnippetArticleListItem>()); |
+ mGroups = new ArrayList<>(); |
+ mAboveTheFold = new AboveTheFoldListItem(); |
+ mBottomSpacer = new SpacingListItem(); |
+ mSections = new TreeMap<>(); |
+ |
+ // TODO(mvanouwerkerk): Do not hard code ARTICLES. Maybe do not initialize an empty |
+ // section in the constructor. |
+ setSuggestions(ContentSuggestionsCategory.ARTICLES, |
+ Collections.<SnippetArticleListItem>emptyList(), |
+ ContentSuggestionsCategoryStatus.INITIALIZING); |
mSnippetsBridge.setObserver(this); |
} |
@@ -133,54 +141,71 @@ public NewTabPageAdapter(NewTabPageManager manager, NewTabPageLayout newTabPageL |
} |
@Override |
- public void onSnippetsReceived(List<SnippetArticleListItem> snippets) { |
+ public void onSuggestionsReceived( |
+ @SuggestionsCategory 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( |
+ mSnippetsBridge.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, mSnippetsBridge.getCategoryStatus(category)); |
NewTabPageUma.recordSnippetAction(NewTabPageUma.SNIPPETS_ACTION_SHOWN); |
} |
@Override |
- public void onCategoryStatusChanged(int categoryStatus) { |
- // Observers should not be registered for that state |
- assert categoryStatus |
- != ContentSuggestionsCategoryStatus.ALL_SUGGESTIONS_EXPLICITLY_DISABLED; |
- |
- mProviderStatus = categoryStatus; |
- mStatusCard = StatusListItem.create(mProviderStatus, this, mNewTabPageManager); |
- |
- // We had suggestions but we just got notified about the provider being enabled. Nothing to |
- // do then. |
- if (SnippetsBridge.isCategoryStatusAvailable(mProviderStatus) && hasSuggestions()) return; |
- |
- 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>()); |
+ public void onCategoryStatusChanged( |
+ @SuggestionsCategory int category, @SuggestionsCategoryStatus int status) { |
+ // Observers should not be registered for this state. |
+ assert status != ContentSuggestionsCategoryStatus.ALL_SUGGESTIONS_EXPLICITLY_DISABLED; |
+ |
+ // If there is no section for this category there is nothing to do. |
+ if (!mSections.containsKey(category)) return; |
+ |
+ SuggestionsSection section = mSections.get(category); |
+ |
+ // 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; |
+ |
+ if (section.hasSuggestions()) { |
+ // The section has suggestions, this implies that the service was previously enabled |
+ // and just transitioned to a disabled state. Clear it. |
+ setSuggestions(category, Collections.<SnippetArticleListItem>emptyList(), status); |
} 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 and the spacer after it. |
- notifyItemRangeChanged(firstCardPosition, 2); |
+ // If the section has no suggestions, it has an old status card that must be replaced. |
+ section.setSuggestions(Collections.<SnippetArticleListItem>emptyList(), status, this, |
+ mNewTabPageManager); |
+ |
+ // Update both the status card, and the bottom spacer, which may get a different height |
+ // if the status card height changed. |
+ int statusItemPosition = getGroupPositionOffset(section); |
+ List<NewTabPageListItem> sectionItems = section.getItems(); |
+ for (int i = 0; i < sectionItems.size(); i++) { |
+ if (sectionItems.get(i) instanceof StatusListItem) { |
+ statusItemPosition += i; |
+ break; |
+ } |
+ } |
+ notifyItemChanged(statusItemPosition); |
+ notifyItemChanged(getBottomSpacerPosition()); |
} |
} |
@Override |
@NewTabPageListItem.ViewType |
public int getItemViewType(int position) { |
- return mItems.get(position).getType(); |
+ return getItems().get(position).getType(); |
} |
@Override |
@@ -213,20 +238,24 @@ public NewTabPageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
@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 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() { |
@@ -234,7 +263,7 @@ public int getLastCardPosition() { |
} |
public int getBottomSpacerPosition() { |
- return mItems.indexOf(mBottomSpacer); |
+ return getItems().indexOf(mBottomSpacer); |
} |
/** Start a request for new snippets. */ |
@@ -242,36 +271,43 @@ public void reloadSnippets() { |
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; |
- |
- snippet.setThumbnailBitmap( |
- ((SnippetArticleListItem) mItems.get(existingSnippetIdx)).getThumbnailBitmap()); |
- } |
- |
- boolean hasContentToShow = !snippets.isEmpty(); |
+ private void setSuggestions(@SuggestionsCategory int category, |
+ List<SnippetArticleListItem> suggestions, @SuggestionsCategoryStatus int status) { |
+ copyThumbnails(category, suggestions); |
- // 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); |
+ mGroups.clear(); |
+ mGroups.add(mAboveTheFold); |
- 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, mNewTabPageManager)); |
} else { |
- mItems.add(mStatusCard); |
+ mSections.get(category).setSuggestions(suggestions, status, this, mNewTabPageManager); |
} |
- mItems.add(mBottomSpacer); |
+ for (@SuggestionsCategory Integer key : mSections.keySet()) { |
+ mGroups.add(mSections.get(key)); |
+ } |
+ |
+ mGroups.add(mBottomSpacer); |
notifyDataSetChanged(); |
} |
+ private void copyThumbnails( |
+ @SuggestionsCategory int category, List<SnippetArticleListItem> suggestions) { |
+ if (!mSections.containsKey(category)) return; |
+ |
+ List<NewTabPageListItem> items = mSections.get(category).getItems(); |
+ for (SnippetArticleListItem suggestion : suggestions) { |
+ int index = items.indexOf(suggestion); |
+ if (index == -1) continue; |
+ |
+ suggestion.setThumbnailBitmap( |
+ ((SnippetArticleListItem) items.get(index)).getThumbnailBitmap()); |
+ } |
+ } |
+ |
@Override |
public void onAttachedToRecyclerView(RecyclerView recyclerView) { |
super.onAttachedToRecyclerView(recyclerView); |
@@ -290,9 +326,9 @@ private void dismissItem(ViewHolder itemViewHolder) { |
assert itemViewHolder.getItemViewType() == NewTabPageListItem.VIEW_TYPE_SNIPPET; |
int position = itemViewHolder.getAdapterPosition(); |
- SnippetArticleListItem dismissedSnippet = (SnippetArticleListItem) mItems.get(position); |
+ SnippetArticleListItem suggestion = (SnippetArticleListItem) getItems().get(position); |
- mSnippetsBridge.getSnippedVisited(dismissedSnippet, new Callback<Boolean>() { |
+ mSnippetsBridge.getSnippedVisited(suggestion, new Callback<Boolean>() { |
@Override |
public void onResult(Boolean result) { |
NewTabPageUma.recordSnippetAction(result |
@@ -301,30 +337,47 @@ public void onResult(Boolean result) { |
} |
}); |
- mSnippetsBridge.discardSnippet(dismissedSnippet); |
- mItems.remove(position); |
- notifyItemRemoved(position); |
- } |
+ mSnippetsBridge.discardSnippet(suggestion); |
+ SuggestionsSection section = (SuggestionsSection) getGroup(position); |
+ section.dismissSuggestion(suggestion); |
- private void addStatusCardIfNecessary() { |
- if (!hasSuggestions() && !mItems.contains(mStatusCard)) { |
- mItems.add(getFirstCardPosition(), mStatusCard); |
- |
- // 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 (NewTabPageItemGroup group : mGroups) { |
+ items.addAll(group.getItems()); |
} |
- return false; |
+ return Collections.unmodifiableList(items); |
} |
- List<NewTabPageListItem> getItemsForTesting() { |
- return mItems; |
+ private NewTabPageItemGroup getGroup(int itemPosition) { |
+ int itemsSkipped = 0; |
+ for (NewTabPageItemGroup group : mGroups) { |
+ List<NewTabPageListItem> items = group.getItems(); |
+ if (itemPosition < itemsSkipped + items.size()) return group; |
+ } |
+ return null; |
+ } |
+ |
+ private int getGroupPositionOffset(NewTabPageItemGroup group) { |
+ int positionOffset = 0; |
+ for (NewTabPageItemGroup candidateGroup : mGroups) { |
+ if (candidateGroup == group) return positionOffset; |
+ positionOffset += candidateGroup.getItems().size(); |
+ } |
+ return RecyclerView.NO_POSITION; |
} |
} |