Index: chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SectionList.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/SectionList.java |
similarity index 34% |
copy from chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapter.java |
copy to chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SectionList.java |
index aa2fab2040a2760bd113edb1fdf1afa7f78708a6..2143c590c1b5ea768d29bf718f04b28ba84f0b89 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/SectionList.java |
@@ -4,28 +4,12 @@ |
package org.chromium.chrome.browser.ntp.cards; |
-import android.annotation.SuppressLint; |
-import android.graphics.Canvas; |
-import android.support.annotation.StringRes; |
-import android.support.v7.widget.RecyclerView; |
-import android.support.v7.widget.RecyclerView.Adapter; |
-import android.support.v7.widget.RecyclerView.ViewHolder; |
-import android.support.v7.widget.helper.ItemTouchHelper; |
-import android.view.View; |
-import android.view.ViewGroup; |
- |
import org.chromium.base.Log; |
-import org.chromium.base.VisibleForTesting; |
-import org.chromium.chrome.R; |
-import org.chromium.chrome.browser.ntp.NewTabPage.DestructionObserver; |
import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; |
-import org.chromium.chrome.browser.ntp.UiConfig; |
import org.chromium.chrome.browser.ntp.snippets.CategoryInt; |
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus; |
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum; |
-import org.chromium.chrome.browser.ntp.snippets.SectionHeaderViewHolder; |
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; |
-import org.chromium.chrome.browser.ntp.snippets.SnippetArticleViewHolder; |
import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; |
import org.chromium.chrome.browser.ntp.snippets.SnippetsConfig; |
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; |
@@ -37,146 +21,35 @@ import java.util.List; |
import java.util.Map; |
/** |
- * A class that handles merging above the fold elements and below the fold cards into an adapter |
- * that will be used to back the NTP RecyclerView. The first element in the adapter should always be |
- * the above-the-fold view (containing the logo, search box, and most visited tiles) and subsequent |
- * elements will be the cards shown to the user |
+ * A node in the tree containing a list of all suggestions sections. It listens to changes in the |
+ * suggestions source and updates the corresponding sections. |
*/ |
-public class NewTabPageAdapter |
- extends Adapter<NewTabPageViewHolder> implements SuggestionsSource.Observer, NodeParent { |
+public class SectionList extends InnerNode implements SuggestionsSource.Observer { |
private static final String TAG = "Ntp"; |
- private final NewTabPageManager mNewTabPageManager; |
- private final View mAboveTheFoldView; |
- private final UiConfig mUiConfig; |
- private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallbacks(); |
- private final OfflinePageBridge mOfflinePageBridge; |
- private NewTabPageRecyclerView mRecyclerView; |
- |
- /** |
- * List of all child nodes (which can themselves contain multiple child nodes). |
- */ |
- private final List<TreeNode> mChildren = new ArrayList<>(); |
- private final AboveTheFoldItem mAboveTheFold = new AboveTheFoldItem(); |
- private final SignInPromo mSigninPromo; |
- private final AllDismissedItem mAllDismissed; |
- private final Footer mFooter; |
- private final SpacingItem mBottomSpacer = new SpacingItem(); |
- private final InnerNode mRoot; |
- |
/** Maps suggestion categories to sections, with stable iteration ordering. */ |
private final Map<Integer, SuggestionsSection> mSections = new LinkedHashMap<>(); |
+ private final List<TreeNode> mChildren = new ArrayList<>(); |
+ private final NewTabPageManager mNewTabPageManager; |
+ private final OfflinePageBridge mOfflinePageBridge; |
- private class ItemTouchCallbacks extends ItemTouchHelper.Callback { |
- @Override |
- public void onSwiped(ViewHolder viewHolder, int direction) { |
- mRecyclerView.onItemDismissStarted(viewHolder); |
- NewTabPageAdapter.this.dismissItem(viewHolder.getAdapterPosition()); |
- } |
- |
- @Override |
- public void clearView(RecyclerView recyclerView, ViewHolder viewHolder) { |
- // clearView() is called when an interaction with the item is finished, which does |
- // not mean that the user went all the way and dismissed the item before releasing it. |
- // We need to check that the item has been removed. |
- if (viewHolder.getAdapterPosition() == RecyclerView.NO_POSITION) { |
- mRecyclerView.onItemDismissFinished(viewHolder); |
- } |
- |
- super.clearView(recyclerView, viewHolder); |
- } |
- |
- @Override |
- public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) { |
- assert false; // Drag and drop not supported, the method will never be called. |
- return false; |
- } |
- |
- @Override |
- public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) { |
- assert viewHolder instanceof NewTabPageViewHolder; |
- |
- int swipeFlags = 0; |
- if (((NewTabPageViewHolder) viewHolder).isDismissable()) { |
- swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END; |
- } |
- |
- return makeMovementFlags(0 /* dragFlags */, swipeFlags); |
- } |
+ public SectionList(NodeParent parent, NewTabPageManager newTabPageManager, |
+ OfflinePageBridge offlinePageBridge) { |
+ super(parent); |
+ mNewTabPageManager = newTabPageManager; |
+ mNewTabPageManager.getSuggestionsSource().setObserver(this); |
dgn
2016/12/13 14:54:47
setObserver will hook into the native code and mig
|
+ mOfflinePageBridge = offlinePageBridge; |
+ } |
- @Override |
- public void onChildDraw(Canvas c, RecyclerView recyclerView, ViewHolder viewHolder, |
- float dX, float dY, int actionState, boolean isCurrentlyActive) { |
- assert viewHolder instanceof NewTabPageViewHolder; |
- |
- // The item has already been removed. We have nothing more to do. |
- // In some cases a removed children may call this method when unrelated items are |
- // interacted with, but this check also covers the case. |
- // See https://crbug.com/664466, b/32900699 |
- if (viewHolder.getAdapterPosition() == RecyclerView.NO_POSITION) return; |
- |
- // We use our own implementation of the dismissal animation, so we don't call the |
- // parent implementation. (by default it changes the translation-X and elevation) |
- mRecyclerView.updateViewStateForDismiss(dX, (NewTabPageViewHolder) viewHolder); |
- |
- // If there is another item that should be animated at the same time, do the same to it. |
- NewTabPageViewHolder siblingViewHolder = getDismissSibling(viewHolder); |
- if (siblingViewHolder != null) { |
- mRecyclerView.updateViewStateForDismiss(dX, siblingViewHolder); |
- } |
- } |
+ @Override |
+ public void init() { |
+ super.init(); |
+ resetSections(/* alwaysAllowEmptySections = */ false); |
} |
- /** |
- * Creates the adapter that will manage all the cards to display on the NTP. |
- * |
- * @param manager the NewTabPageManager to use to interact with the rest of the system. |
- * @param aboveTheFoldView the layout encapsulating all the above-the-fold elements |
- * (logo, search box, most visited tiles) |
- * @param uiConfig the NTP UI configuration, to be passed to created views. |
- * @param offlinePageBridge the OfflinePageBridge used to determine if articles are available |
- * offline. |
- * |
- */ |
- public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig, |
- OfflinePageBridge offlinePageBridge) { |
- mNewTabPageManager = manager; |
- mAboveTheFoldView = aboveTheFoldView; |
- mUiConfig = uiConfig; |
- mOfflinePageBridge = offlinePageBridge; |
- mRoot = new InnerNode(this) { |
- @Override |
- protected List<TreeNode> getChildren() { |
- return mChildren; |
- } |
- |
- @Override |
- public void onItemRangeChanged(TreeNode child, int index, int count) { |
- if (mChildren.isEmpty()) return; // The sections have not been initialised yet. |
- super.onItemRangeChanged(child, index, count); |
- } |
- |
- @Override |
- public void onItemRangeInserted(TreeNode child, int index, int count) { |
- if (mChildren.isEmpty()) return; // The sections have not been initialised yet. |
- super.onItemRangeInserted(child, index, count); |
- } |
- |
- @Override |
- public void onItemRangeRemoved(TreeNode child, int index, int count) { |
- if (mChildren.isEmpty()) return; // The sections have not been initialised yet. |
- super.onItemRangeRemoved(child, index, count); |
- } |
- }; |
- |
- mSigninPromo = new SignInPromo(mRoot); |
- mAllDismissed = new AllDismissedItem(mRoot); |
- mFooter = new Footer(mRoot); |
- DestructionObserver signInObserver = mSigninPromo.getObserver(); |
- if (signInObserver != null) mNewTabPageManager.addDestructionObserver(signInObserver); |
- |
- resetSections(/*alwaysAllowEmptySections=*/false); |
- mNewTabPageManager.getSuggestionsSource().setObserver(this); |
+ @Override |
+ protected List<TreeNode> getChildren() { |
+ return mChildren; |
} |
/** |
@@ -204,8 +77,6 @@ public class NewTabPageAdapter |
} |
mNewTabPageManager.trackSnippetsPageImpression(categories, suggestionsPerCategory); |
- |
- updateChildren(); |
} |
/** |
@@ -224,17 +95,20 @@ public class NewTabPageAdapter |
List<SnippetArticle> suggestions = suggestionsSource.getSuggestionsForCategory(category); |
SuggestionsCategoryInfo info = suggestionsSource.getCategoryInfo(category); |
+ SuggestionsSection section = mSections.get(category); |
+ |
// Do not show an empty section if not allowed. |
if (suggestions.isEmpty() && !info.showIfEmpty() && !alwaysAllowEmptySections) { |
- mSections.remove(category); |
+ if (section != null) removeSection(section); |
return 0; |
} |
// Create the section if needed. |
- SuggestionsSection section = mSections.get(category); |
if (section == null) { |
- section = new SuggestionsSection(mRoot, info, mNewTabPageManager, mOfflinePageBridge); |
+ section = new SuggestionsSection(this, mNewTabPageManager, mOfflinePageBridge, info); |
mSections.put(category, section); |
+ mChildren.add(section); |
+ didAddChild(section); |
} |
// Add the new suggestions. |
@@ -243,11 +117,6 @@ public class NewTabPageAdapter |
return suggestions.size(); |
} |
- /** Returns callbacks to configure the interactions with the RecyclerView's items. */ |
- public ItemTouchHelper.Callback getItemTouchCallbacks() { |
- return mItemTouchCallbacks; |
- } |
- |
@Override |
public void onNewSuggestions(@CategoryInt int category) { |
@CategoryStatusEnum |
@@ -298,7 +167,9 @@ public class NewTabPageAdapter |
return; |
case CategoryStatus.SIGNED_OUT: |
- // TODO(dgn): We currently can only reach this through an old variation parameter. |
+ resetSection(category, status, /* alwaysAllowEmptySections = */ false); |
+ return; |
+ |
default: |
mSections.get(category).setStatus(status); |
return; |
@@ -313,86 +184,7 @@ public class NewTabPageAdapter |
@Override |
public void onFullRefreshRequired() { |
- resetSections(/*alwaysAllowEmptySections=*/false); |
- } |
- |
- @Override |
- @ItemViewType |
- public int getItemViewType(int position) { |
- return mRoot.getItemViewType(position); |
- } |
- |
- @Override |
- public NewTabPageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
- assert parent == mRecyclerView; |
- |
- switch (viewType) { |
- case ItemViewType.ABOVE_THE_FOLD: |
- return new NewTabPageViewHolder(mAboveTheFoldView); |
- |
- case ItemViewType.HEADER: |
- return new SectionHeaderViewHolder(mRecyclerView, mUiConfig); |
- |
- case ItemViewType.SNIPPET: |
- return new SnippetArticleViewHolder(mRecyclerView, mNewTabPageManager, mUiConfig); |
- |
- case ItemViewType.SPACING: |
- return new NewTabPageViewHolder(SpacingItem.createView(parent)); |
- |
- case ItemViewType.STATUS: |
- return new StatusCardViewHolder(mRecyclerView, mNewTabPageManager, mUiConfig); |
- |
- case ItemViewType.PROGRESS: |
- return new ProgressViewHolder(mRecyclerView); |
- |
- case ItemViewType.ACTION: |
- return new ActionItem.ViewHolder(mRecyclerView, mNewTabPageManager, mUiConfig); |
- |
- case ItemViewType.PROMO: |
- return new SignInPromo.ViewHolder(mRecyclerView, mNewTabPageManager, mUiConfig); |
- |
- case ItemViewType.FOOTER: |
- return new Footer.ViewHolder(mRecyclerView, mNewTabPageManager); |
- |
- case ItemViewType.ALL_DISMISSED: |
- return new AllDismissedItem.ViewHolder(mRecyclerView, mNewTabPageManager, this); |
- } |
- |
- assert false : viewType; |
- return null; |
- } |
- |
- @Override |
- public void onBindViewHolder(NewTabPageViewHolder holder, final int position) { |
- mRoot.onBindViewHolder(holder, position); |
- } |
- |
- @Override |
- public int getItemCount() { |
- return mRoot.getItemCount(); |
- } |
- |
- public int getAboveTheFoldPosition() { |
- return getChildPositionOffset(mAboveTheFold); |
- } |
- |
- public int getFirstHeaderPosition() { |
- return getFirstPositionForType(ItemViewType.HEADER); |
- } |
- |
- public int getFirstCardPosition() { |
- for (int i = 0; i < getItemCount(); ++i) { |
- if (CardViewHolder.isCard(getItemViewType(i))) return i; |
- } |
- return RecyclerView.NO_POSITION; |
- } |
- |
- int getBottomSpacerPosition() { |
- return getChildPositionOffset(mBottomSpacer); |
- } |
- |
- int getLastContentItemPosition() { |
- return getChildPositionOffset(hasAllBeenDismissed() ? mAllDismissed : mFooter); |
+ resetSections(/* alwaysAllowEmptySections = */false); |
} |
private void setSuggestions(@CategoryInt int category, List<SnippetArticle> suggestions, |
@@ -411,155 +203,6 @@ public class NewTabPageAdapter |
mSections.get(category).addSuggestions(suggestions, status); |
} |
- private void updateChildren() { |
- mChildren.clear(); |
- mChildren.add(mAboveTheFold); |
- mChildren.addAll(mSections.values()); |
- mChildren.add(mSigninPromo); |
- mChildren.add(mAllDismissed); |
- mChildren.add(mFooter); |
- mChildren.add(mBottomSpacer); |
- |
- updateAllDismissedVisibility(); |
- |
- // TODO(mvanouwerkerk): Notify about the subset of changed items. At least |mAboveTheFold| |
- // has not changed when refreshing from the all dismissed state. |
- notifyDataSetChanged(); |
- } |
- |
- private void updateAllDismissedVisibility() { |
- boolean showAllDismissed = hasAllBeenDismissed(); |
- mAllDismissed.setVisible(showAllDismissed); |
- mFooter.setVisible(!showAllDismissed); |
- } |
- |
- private void removeSection(SuggestionsSection section) { |
- mSections.remove(section.getCategory()); |
- int startPos = getChildPositionOffset(section); |
- mChildren.remove(section); |
- notifyItemRangeRemoved(startPos, section.getItemCount()); |
- |
- updateAllDismissedVisibility(); |
- |
- notifyItemChanged(getBottomSpacerPosition()); |
- } |
- |
- @Override |
- public void onItemRangeChanged(TreeNode child, int itemPosition, int itemCount) { |
- assert child == mRoot; |
- notifyItemRangeChanged(itemPosition, itemCount); |
- } |
- |
- @Override |
- public void onItemRangeInserted(TreeNode child, int itemPosition, int itemCount) { |
- assert child == mRoot; |
- notifyItemRangeInserted(itemPosition, itemCount); |
- notifyItemChanged(getItemCount() - 1); // Refresh the spacer too. |
- |
- updateAllDismissedVisibility(); |
- } |
- |
- @Override |
- public void onItemRangeRemoved(TreeNode child, int itemPosition, int itemCount) { |
- assert child == mRoot; |
- notifyItemRangeRemoved(itemPosition, itemCount); |
- notifyItemChanged(getItemCount() - 1); // Refresh the spacer too. |
- |
- updateAllDismissedVisibility(); |
- } |
- |
- @Override |
- public void onAttachedToRecyclerView(RecyclerView recyclerView) { |
- super.onAttachedToRecyclerView(recyclerView); |
- |
- // We are assuming for now that the adapter is used with a single RecyclerView. |
- // Getting the reference as we are doing here is going to be broken if that changes. |
- assert mRecyclerView == null; |
- |
- // FindBugs chokes on the cast below when not checked, raising BC_UNCONFIRMED_CAST |
- assert recyclerView instanceof NewTabPageRecyclerView; |
- |
- mRecyclerView = (NewTabPageRecyclerView) recyclerView; |
- } |
- |
- /** |
- * Dismisses the item at the provided adapter position. Can also cause the dismissal of other |
- * items or even entire sections. |
- */ |
- // TODO(crbug.com/635567): Fix this properly. |
- @SuppressLint("SwitchIntDef") |
- public void dismissItem(int position) { |
- int itemViewType = getItemViewType(position); |
- |
- // TODO(dgn): Polymorphism is supposed to allow to avoid that kind of stuff. |
- switch (itemViewType) { |
- case ItemViewType.STATUS: |
- case ItemViewType.ACTION: |
- dismissSection(getSuggestionsSection(position)); |
- return; |
- |
- case ItemViewType.SNIPPET: |
- dismissSuggestion(position); |
- return; |
- |
- case ItemViewType.PROMO: |
- dismissPromo(); |
- return; |
- |
- default: |
- Log.wtf(TAG, "Unsupported dismissal of item of type %d", itemViewType); |
- return; |
- } |
- } |
- |
- private void dismissSection(SuggestionsSection section) { |
- assert SnippetsConfig.isSectionDismissalEnabled(); |
- |
- announceItemRemoved(section.getHeaderText()); |
- |
- mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCategory()); |
- removeSection(section); |
- } |
- |
- private void dismissSuggestion(int position) { |
- SnippetArticle suggestion = mRoot.getSuggestionAt(position); |
- SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsSource(); |
- if (suggestionsSource == null) { |
- // It is possible for this method to be called after the NewTabPage has had destroy() |
- // called. This can happen when NewTabPageRecyclerView.dismissWithAnimation() is called |
- // and the animation ends after the user has navigated away. In this case we cannot |
- // inform the native side that the snippet has been dismissed (http://crbug.com/649299). |
- return; |
- } |
- |
- announceItemRemoved(suggestion.mTitle); |
- |
- suggestionsSource.dismissSuggestion(suggestion); |
- SuggestionsSection section = getSuggestionsSection(position); |
- section.removeSuggestion(suggestion); |
- } |
- |
- private void dismissPromo() { |
- announceItemRemoved(mSigninPromo.getHeader()); |
- mSigninPromo.dismiss(); |
- } |
- |
- /** |
- * Returns another view holder that should be dismissed at the same time as the provided one. |
- */ |
- public NewTabPageViewHolder getDismissSibling(ViewHolder viewHolder) { |
- int swipePos = viewHolder.getAdapterPosition(); |
- int siblingPosDelta = mRoot.getDismissSiblingPosDelta(swipePos); |
- if (siblingPosDelta == 0) return null; |
- |
- return (NewTabPageViewHolder) mRecyclerView.findViewHolderForAdapterPosition( |
- siblingPosDelta + swipePos); |
- } |
- |
- private boolean hasAllBeenDismissed() { |
- return mSections.isEmpty() && !mSigninPromo.isVisible(); |
- } |
- |
private boolean canLoadSuggestions(@CategoryInt int category, @CategoryStatusEnum int status) { |
// We never want to add suggestions from unknown categories. |
if (!mSections.containsKey(category)) return false; |
@@ -576,54 +219,39 @@ public class NewTabPageAdapter |
} |
/** |
- * @param itemPosition The position of an item in the adapter. |
- * @return Returns the {@link SuggestionsSection} that contains the item at |
- * {@code itemPosition}, or null if the item is not part of one. |
+ * Dismisses a section. |
+ * @param section The section to be dismissed. |
*/ |
- private SuggestionsSection getSuggestionsSection(int itemPosition) { |
- TreeNode child = mRoot.getChildForPosition(itemPosition); |
- if (!(child instanceof SuggestionsSection)) return null; |
- return (SuggestionsSection) child; |
- } |
- |
- private int getChildPositionOffset(TreeNode child) { |
- return mRoot.getStartingOffsetForChild(child); |
- } |
- |
- @VisibleForTesting |
- SnippetArticle getSuggestionAt(int position) { |
- return mRoot.getSuggestionAt(position); |
- } |
+ public void dismissSection(SuggestionsSection section) { |
+ assert SnippetsConfig.isSectionDismissalEnabled(); |
- @VisibleForTesting |
- int getFirstPositionForType(@ItemViewType int viewType) { |
- int count = getItemCount(); |
- for (int i = 0; i < count; i++) { |
- if (getItemViewType(i) == viewType) return i; |
- } |
- return RecyclerView.NO_POSITION; |
+ mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCategory()); |
+ removeSection(section); |
} |
- SuggestionsSection getSectionForTesting(@CategoryInt int category) { |
- return mSections.get(category); |
+ private void removeSection(SuggestionsSection section) { |
+ mSections.remove(section.getCategory()); |
+ willRemoveChild(section); |
+ mChildren.remove(section); |
} |
- InnerNode getRootForTesting() { |
- return mRoot; |
+ /** |
+ * Restores any sections that have been dismissed and triggers a new fetch. |
+ */ |
+ public void restoreDismissedSections() { |
+ mNewTabPageManager.getSuggestionsSource().restoreDismissedCategories(); |
+ resetSections(/* allowEmptySections = */ true); |
+ mNewTabPageManager.getSuggestionsSource().fetchRemoteSuggestions(); |
} |
- private void announceItemRemoved(String itemTitle) { |
- // In tests the RecyclerView can be null. |
- if (mRecyclerView == null) return; |
- |
- mRecyclerView.announceForAccessibility(mRecyclerView.getResources().getString( |
- R.string.ntp_accessibility_item_removed, itemTitle)); |
+ /** |
+ * @return Whether the list of sections is empty. |
+ */ |
+ public boolean isEmpty() { |
+ return mSections.isEmpty(); |
} |
- private void announceItemRemoved(@StringRes int stringToAnnounce) { |
- // In tests the RecyclerView can be null. |
- if (mRecyclerView == null) return; |
- |
- announceItemRemoved(mRecyclerView.getResources().getString(stringToAnnounce)); |
+ SuggestionsSection getSectionForTesting(@CategoryInt int categoryId) { |
+ return mSections.get(categoryId); |
} |
} |