Chromium Code Reviews| 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 791ff025f3385521cf87bc6d040a86a0dab29f1f..83941effa5701751644aa31bb0382b0e02c3c0ea 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.NewTabPageLayout; |
| 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.SnippetHeaderListItem; |
| 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 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| private final NewTabPageManager mNewTabPageManager; |
| private final NewTabPageLayout mNewTabPageLayout; |
| - private final AboveTheFoldListItem mAboveTheFold; |
| - private final SnippetHeaderListItem mHeader; |
| + private final SnippetsBridge mSnippetsBridge; |
|
PEConn
2016/08/01 16:26:51
Just a heads up, when you merge this will be chang
Bernhard Bauer
2016/08/02 13:03:26
Acknowledged.
|
| 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 |
| @@ -112,17 +118,20 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| 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); |
| + mItemTouchCallbacks = new ItemTouchCallbacks(); |
| - loadSnippets(new ArrayList<SnippetArticleListItem>()); |
| + mGroups = new ArrayList<>(); |
|
PEConn
2016/08/01 16:26:50
Why not initialize these inline where they are dec
Bernhard Bauer
2016/08/02 13:03:26
Done.
|
| + 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); |
| } |
| @@ -132,54 +141,70 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| } |
| @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); |
| - |
| - // 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); |
|
PEConn
2016/08/01 16:26:51
Do we want to pull the rug out from under the user
Marc Treib
2016/08/01 16:34:22
Drive-by: In some cases, yes, namely when the sugg
|
| } 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); |
| + |
| + // 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) { |
|
PEConn
2016/08/01 16:26:51
Won't the StatusListItem only ever be the first it
Bernhard Bauer
2016/08/02 13:03:26
Done.
|
| + 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 |
| @@ -212,24 +237,28 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| @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 getItems().indexOf(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() { |
| @@ -237,7 +266,7 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| } |
| public int getBottomSpacerPosition() { |
| - return mItems.indexOf(mBottomSpacer); |
| + return getItems().indexOf(mBottomSpacer); |
| } |
| /** Start a request for new snippets. */ |
| @@ -245,36 +274,43 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| 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(@SuggestionsCategory int category, |
| + List<SnippetArticleListItem> suggestions, @SuggestionsCategoryStatus int status) { |
| + copyThumbnails(category, suggestions); |
| - snippet.setThumbnailBitmap( |
| - ((SnippetArticleListItem) mItems.get(existingSnippetIdx)).getThumbnailBitmap()); |
| - } |
| + mGroups.clear(); |
| + mGroups.add(mAboveTheFold); |
| - 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); |
| + mSections.get(category).setSuggestions(suggestions, status, this); |
| } |
| - mItems.add(mBottomSpacer); |
| + for (@SuggestionsCategory Integer key : mSections.keySet()) { |
|
PEConn
2016/08/01 16:26:51
Integer -> int ?
Bernhard Bauer
2016/08/02 13:03:26
Hm, the key type for the Map is a boxed Integer, s
PEConn
2016/08/02 15:45:14
Given we're probably going to have a handful of ca
|
| + 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); |
| @@ -293,9 +329,9 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| 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 |
| @@ -304,32 +340,48 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements |
| } |
| }); |
| - mSnippetsBridge.discardSnippet(dismissedSnippet); |
| - mItems.remove(position); |
| - notifyItemRemoved(position); |
| + mSnippetsBridge.discardSnippet(suggestion); |
| + SuggestionsSection section = (SuggestionsSection) getGroup(position); |
| + section.dismissSuggestion(suggestion); |
| - addStatusCardIfNecessary(); |
| + 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(); |
| + } |
| } |
| - 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); |
| - notifyDataSetChanged(); |
| + /** |
| + * Returns an unmodifiable list containing all items in the adapter. |
| + */ |
| + List<NewTabPageListItem> getItems() { |
|
Philipp Keck
2016/08/01 15:28:57
Not sure if this is relevant, but this method is c
Bernhard Bauer
2016/08/01 15:37:24
Acknowledged. This is the area where I'm planning
|
| + List<NewTabPageListItem> items = new ArrayList<>(); |
| + for (NewTabPageItemGroup group : mGroups) { |
| + items.addAll(group.getItems()); |
| } |
| + return Collections.unmodifiableList(items); |
| } |
| - /** Returns whether we have some suggested content to display. */ |
| - private boolean hasSuggestions() { |
| - for (NewTabPageListItem item : mItems) { |
| - if (item instanceof SnippetArticleListItem) return true; |
| + private NewTabPageItemGroup getGroup(int itemPosition) { |
| + int itemsSkipped = 0; |
| + for (NewTabPageItemGroup group : mGroups) { |
| + List<NewTabPageListItem> items = group.getItems(); |
| + itemsSkipped += items.size(); |
| + if (itemPosition < itemsSkipped) return group; |
| } |
| - return false; |
| + return null; |
| } |
| - List<NewTabPageListItem> getItemsForTesting() { |
| - return mItems; |
| + 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; |
| } |
| } |