| 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 17904cab8b5ac4bf57bd1ae0a2d6b707bd9cc58f..c63b9479794e47e0628c3d08b65ffa90c6512705 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.NewTabPageUma;
|
| 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;
|
| 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,17 +46,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 SnippetsSource mSnippetsSource;
|
| private final UiConfig mUiConfig;
|
| - private StatusListItem mStatusCard;
|
| - private final SpacingListItem mBottomSpacer;
|
| - private final List<NewTabPageListItem> mItems;
|
| - private final ItemTouchCallbacks mItemTouchCallbacks;
|
| + private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallbacks();
|
| private NewTabPageRecyclerView mRecyclerView;
|
| - private int mProviderStatus;
|
|
|
| - private SnippetsSource mSnippetsSource;
|
| + private final List<NewTabPageItemGroup> 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
|
| @@ -113,17 +119,14 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
|
| SnippetsSource snippetsSource, UiConfig uiConfig) {
|
| mNewTabPageManager = manager;
|
| mNewTabPageLayout = newTabPageLayout;
|
| - mAboveTheFold = new AboveTheFoldListItem();
|
| - mHeader = new SnippetHeaderListItem();
|
| - mBottomSpacer = new SpacingListItem();
|
| - mItemTouchCallbacks = new ItemTouchCallbacks();
|
| - mItems = new ArrayList<>();
|
| - mProviderStatus = CategoryStatus.INITIALIZING;
|
| mSnippetsSource = snippetsSource;
|
| mUiConfig = uiConfig;
|
| - mStatusCard = StatusListItem.create(snippetsSource.getCategoryStatus(), 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(),
|
| + CategoryStatus.INITIALIZING);
|
| snippetsSource.setObserver(this);
|
| }
|
|
|
| @@ -133,53 +136,65 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
|
| }
|
|
|
| @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);
|
| + // 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>());
|
| + // 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);
|
| +
|
| + // Update both the status card, and the bottom spacer, which may get a different height
|
| + // if the status card height changed.
|
| + List<NewTabPageListItem> sectionItems = section.getItems();
|
| + if (!sectionItems.isEmpty() && sectionItems.get(0) instanceof StatusListItem) {
|
| + notifyItemChanged(getGroupPositionOffset(section));
|
| + }
|
| + notifyItemChanged(getBottomSpacerPosition());
|
| }
|
| }
|
|
|
| @Override
|
| @NewTabPageListItem.ViewType
|
| public int getItemViewType(int position) {
|
| - return mItems.get(position).getType();
|
| + return getItems().get(position).getType();
|
| }
|
|
|
| @Override
|
| @@ -212,24 +227,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 +256,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 +264,41 @@ 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(@KnownCategoriesEnum int category,
|
| + List<SnippetArticleListItem> suggestions, @CategoryStatusEnum int status) {
|
| + copyThumbnails(category, suggestions);
|
|
|
| - 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);
|
| + 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));
|
| } else {
|
| - mItems.add(mStatusCard);
|
| + mSections.get(category).setSuggestions(suggestions, status, this);
|
| }
|
|
|
| - mItems.add(mBottomSpacer);
|
| + mGroups.addAll(mSections.values());
|
| +
|
| + mGroups.add(mBottomSpacer);
|
|
|
| notifyDataSetChanged();
|
| }
|
|
|
| + private void copyThumbnails(
|
| + @KnownCategoriesEnum 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 +317,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);
|
|
|
| - mSnippetsSource.getSnippedVisited(dismissedSnippet, new Callback<Boolean>() {
|
| + mSnippetsSource.getSnippedVisited(suggestion, new Callback<Boolean>() {
|
| @Override
|
| public void onResult(Boolean result) {
|
| NewTabPageUma.recordSnippetAction(result
|
| @@ -304,32 +328,48 @@ public class NewTabPageAdapter extends Adapter<NewTabPageViewHolder> implements
|
| }
|
| });
|
|
|
| - mSnippetsSource.discardSnippet(dismissedSnippet);
|
| - mItems.remove(position);
|
| - notifyItemRemoved(position);
|
| + mSnippetsSource.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() {
|
| + 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;
|
| }
|
| }
|
|
|