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