| 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;
|
| }
|
| }
|
|
|