Chromium Code Reviews| 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/SectionList.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SectionList.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..2143c590c1b5ea768d29bf718f04b28ba84f0b89 |
| --- /dev/null |
| +++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SectionList.java |
| @@ -0,0 +1,257 @@ |
| +// Copyright 2016 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +package org.chromium.chrome.browser.ntp.cards; |
|
Michael van Ouwerkerk
2016/12/12 16:09:57
Fwiw, I'm not convinced having packages 'cards' an
Bernhard Bauer
2016/12/12 17:39:19
Yeah, I agree, the current split feels kind of arb
Michael van Ouwerkerk
2016/12/13 11:08:21
Yes I considered that split, but we have things li
|
| + |
| +import org.chromium.base.Log; |
| +import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; |
| +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.SnippetArticle; |
| +import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; |
| +import org.chromium.chrome.browser.ntp.snippets.SnippetsConfig; |
| +import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; |
| +import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
| + |
| +import java.util.ArrayList; |
| +import java.util.LinkedHashMap; |
| +import java.util.List; |
| +import java.util.Map; |
| + |
| +/** |
| + * 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 SectionList extends InnerNode implements SuggestionsSource.Observer { |
|
Michael van Ouwerkerk
2016/12/12 16:09:57
It seems it would be nice to see a diff between th
Bernhard Bauer
2016/12/12 17:39:19
Done!
|
| + private static final String TAG = "Ntp"; |
| + |
| + /** 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; |
| + |
| + public SectionList(NodeParent parent, NewTabPageManager newTabPageManager, |
| + OfflinePageBridge offlinePageBridge) { |
| + super(parent); |
| + mNewTabPageManager = newTabPageManager; |
| + mNewTabPageManager.getSuggestionsSource().setObserver(this); |
| + mOfflinePageBridge = offlinePageBridge; |
| + } |
| + |
| + @Override |
| + public void init() { |
| + super.init(); |
| + resetSections(/* alwaysAllowEmptySections = */ false); |
| + } |
| + |
| + @Override |
| + protected List<TreeNode> getChildren() { |
| + return mChildren; |
| + } |
| + |
| + /** |
| + * Resets the sections, reloading the whole new tab page content. |
| + * @param alwaysAllowEmptySections Whether sections are always allowed to be displayed when |
| + * they are empty, even when they are normally not. |
| + */ |
| + public void resetSections(boolean alwaysAllowEmptySections) { |
| + mSections.clear(); |
| + mChildren.clear(); |
| + |
| + SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsSource(); |
| + int[] categories = suggestionsSource.getCategories(); |
| + int[] suggestionsPerCategory = new int[categories.length]; |
| + int i = 0; |
| + for (int category : categories) { |
| + int categoryStatus = suggestionsSource.getCategoryStatus(category); |
| + if (categoryStatus == CategoryStatus.LOADING_ERROR |
| + || categoryStatus == CategoryStatus.NOT_PROVIDED |
| + || categoryStatus == CategoryStatus.CATEGORY_EXPLICITLY_DISABLED) |
| + continue; |
| + |
| + suggestionsPerCategory[i++] = |
| + resetSection(category, categoryStatus, alwaysAllowEmptySections); |
| + } |
| + |
| + mNewTabPageManager.trackSnippetsPageImpression(categories, suggestionsPerCategory); |
| + } |
| + |
| + /** |
| + * Resets the section for {@code category}. Removes the section if there are no suggestions for |
| + * it and it is not allowed to be empty. Otherwise, creates the section if it is not present |
| + * yet. Sets the available suggestions on the section. |
| + * @param category The category for which the section must be reset. |
| + * @param categoryStatus The category status. |
| + * @param alwaysAllowEmptySections Whether sections are always allowed to be displayed when |
| + * they are empty, even when they are normally not. |
| + * @return The number of suggestions for the section. |
| + */ |
| + private int resetSection(@CategoryInt int category, @CategoryStatusEnum int categoryStatus, |
| + boolean alwaysAllowEmptySections) { |
| + SuggestionsSource suggestionsSource = mNewTabPageManager.getSuggestionsSource(); |
| + 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) { |
| + if (section != null) removeSection(section); |
| + return 0; |
| + } |
| + |
| + // Create the section if needed. |
| + if (section == null) { |
| + section = new SuggestionsSection(this, mNewTabPageManager, mOfflinePageBridge, info); |
| + mSections.put(category, section); |
| + mChildren.add(section); |
| + didAddChild(section); |
| + } |
| + |
| + // Add the new suggestions. |
| + setSuggestions(category, suggestions, categoryStatus); |
| + |
| + return suggestions.size(); |
| + } |
| + |
| + @Override |
| + public void onNewSuggestions(@CategoryInt int category) { |
| + @CategoryStatusEnum |
| + int status = mNewTabPageManager.getSuggestionsSource().getCategoryStatus(category); |
| + |
| + if (!canLoadSuggestions(category, status)) return; |
| + |
| + // We never want to refresh the suggestions if we already have some content. |
| + if (mSections.get(category).hasSuggestions()) return; |
| + |
| + List<SnippetArticle> suggestions = |
| + mNewTabPageManager.getSuggestionsSource().getSuggestionsForCategory(category); |
| + |
| + Log.d(TAG, "Received %d new suggestions for category %d.", suggestions.size(), category); |
| + |
| + // At first, there might be no suggestions available, we wait until they have been fetched. |
| + if (suggestions.isEmpty()) return; |
| + |
| + setSuggestions(category, suggestions, status); |
| + } |
| + |
| + @Override |
| + public void onMoreSuggestions(@CategoryInt int category, List<SnippetArticle> suggestions) { |
| + @CategoryStatusEnum |
| + int status = mNewTabPageManager.getSuggestionsSource().getCategoryStatus(category); |
| + if (!canLoadSuggestions(category, status)) return; |
| + |
| + setSuggestions(category, suggestions, status); |
| + } |
| + |
| + @Override |
| + public void onCategoryStatusChanged(@CategoryInt int category, @CategoryStatusEnum int status) { |
| + // Observers should not be registered for this state. |
| + assert status != CategoryStatus.ALL_SUGGESTIONS_EXPLICITLY_DISABLED; |
| + |
| + // If there is no section for this category there is nothing to do. |
| + if (!mSections.containsKey(category)) return; |
| + |
| + switch (status) { |
| + case CategoryStatus.NOT_PROVIDED: |
| + // The section provider has gone away. Keep open UIs as they are. |
| + return; |
| + |
| + case CategoryStatus.CATEGORY_EXPLICITLY_DISABLED: |
| + case CategoryStatus.LOADING_ERROR: |
| + // Need to remove the entire section from the UI immediately. |
| + removeSection(mSections.get(category)); |
| + return; |
| + |
| + case CategoryStatus.SIGNED_OUT: |
| + resetSection(category, status, /* alwaysAllowEmptySections = */ false); |
| + return; |
| + |
| + default: |
| + mSections.get(category).setStatus(status); |
| + return; |
| + } |
| + } |
| + |
| + @Override |
| + public void onSuggestionInvalidated(@CategoryInt int category, String idWithinCategory) { |
| + if (!mSections.containsKey(category)) return; |
| + mSections.get(category).removeSuggestionById(idWithinCategory); |
| + } |
| + |
| + @Override |
| + public void onFullRefreshRequired() { |
| + resetSections(/* alwaysAllowEmptySections = */false); |
| + } |
| + |
| + private void setSuggestions(@CategoryInt int category, List<SnippetArticle> suggestions, |
| + @CategoryStatusEnum int status) { |
| + // Count the number of suggestions before this category. |
| + int globalPositionOffset = 0; |
| + for (Map.Entry<Integer, SuggestionsSection> entry : mSections.entrySet()) { |
| + if (entry.getKey() == category) break; |
| + globalPositionOffset += entry.getValue().getSuggestionsCount(); |
| + } |
| + // Assign global indices to the new suggestions. |
| + for (SnippetArticle suggestion : suggestions) { |
| + suggestion.mGlobalPosition = globalPositionOffset + suggestion.mPosition; |
| + } |
| + |
| + mSections.get(category).addSuggestions(suggestions, status); |
| + } |
| + |
| + private boolean canLoadSuggestions(@CategoryInt int category, @CategoryStatusEnum int status) { |
| + // We never want to add suggestions from unknown categories. |
| + if (!mSections.containsKey(category)) return false; |
| + |
| + // The status may have changed while the suggestions were loading, perhaps they should not |
| + // be displayed any more. |
| + if (!SnippetsBridge.isCategoryEnabled(status)) { |
| + Log.w(TAG, "Received suggestions for a disabled category (id=%d, status=%d)", category, |
| + status); |
| + return false; |
| + } |
| + |
| + return true; |
| + } |
| + |
| + /** |
| + * Dismisses a section. |
| + * @param section The section to be dismissed. |
| + */ |
| + public void dismissSection(SuggestionsSection section) { |
| + assert SnippetsConfig.isSectionDismissalEnabled(); |
| + |
| + mNewTabPageManager.getSuggestionsSource().dismissCategory(section.getCategory()); |
| + removeSection(section); |
| + } |
| + |
| + private void removeSection(SuggestionsSection section) { |
| + mSections.remove(section.getCategory()); |
| + willRemoveChild(section); |
| + mChildren.remove(section); |
| + } |
| + |
| + /** |
| + * Restores any sections that have been dismissed and triggers a new fetch. |
| + */ |
| + public void restoreDismissedSections() { |
| + mNewTabPageManager.getSuggestionsSource().restoreDismissedCategories(); |
| + resetSections(/* allowEmptySections = */ true); |
| + mNewTabPageManager.getSuggestionsSource().fetchRemoteSuggestions(); |
| + } |
| + |
| + /** |
| + * @return Whether the list of sections is empty. |
| + */ |
| + public boolean isEmpty() { |
| + return mSections.isEmpty(); |
| + } |
| + |
| + SuggestionsSection getSectionForTesting(@CategoryInt int categoryId) { |
| + return mSections.get(categoryId); |
| + } |
| +} |