[NTP] Show Offline Badge when Article is available offline.
This is hidden behind the NTPOfflineBadge feature.
BUG=657358
Review-Url: https://codereview.chromium.org/2443183002
Cr-Commit-Position: refs/heads/master@{#427823}
diff --git a/chrome/android/java/res/layout/new_tab_page_snippets_card.xml b/chrome/android/java/res/layout/new_tab_page_snippets_card.xml
index 8938814..353974f 100644
--- a/chrome/android/java/res/layout/new_tab_page_snippets_card.xml
+++ b/chrome/android/java/res/layout/new_tab_page_snippets_card.xml
@@ -68,11 +68,11 @@
android:id="@+id/offline_icon"
android:layout_width="@dimen/snippets_offline_icon_size"
android:layout_height="@dimen/snippets_offline_icon_size"
+ android:layout_marginStart="6dp"
android:alpha="0.46"
- android:paddingStart="6dp"
android:src="@drawable/offline_pin_black"
android:contentDescription="@string/accessibility_ntp_offline_badge"
- android:visibility="invisible" />
+ android:visibility="gone" />
</LinearLayout>
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
index 2b87293..03adcd241 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ChromeFeatureList.java
@@ -60,6 +60,7 @@
public static final String NTP_FAKE_OMNIBOX_TEXT = "NTPFakeOmniboxText";
public static final String NTP_SNIPPETS = "NTPSnippets";
public static final String NTP_SNIPPETS_SAVE_TO_OFFLINE = "NTPSaveToOffline";
+ public static final String NTP_SNIPPETS_OFFLINE_BADGE = "NTPOfflineBadge";
public static final String NTP_SUGGESTIONS_SECTION_DISMISSAL = "NTPSuggestionsSectionDismissal";
public static final String SCAN_CARDS_IN_WEB_PAYMENTS = "ScanCardsInWebPayments";
public static final String TAB_REPARENTING = "TabReparenting";
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
index bd8a4278..ac283629 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPage.java
@@ -23,6 +23,7 @@
import org.chromium.base.Callback;
import org.chromium.base.CommandLine;
import org.chromium.base.Log;
+import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
@@ -143,7 +144,7 @@
// Whether destroy() has been called.
private boolean mIsDestroyed;
- private DestructionObserver mDestructionObserver;
+ private final ObserverList<DestructionObserver> mDestructionObservers = new ObserverList<>();
/**
* Allows clients to listen for updates to the scroll changes of the search box on the
@@ -623,10 +624,9 @@
}
@Override
- public void setDestructionObserver(DestructionObserver destructionObserver) {
+ public void addDestructionObserver(DestructionObserver destructionObserver) {
if (mIsDestroyed) return;
- assert mDestructionObserver == null;
- mDestructionObserver = destructionObserver;
+ mDestructionObservers.addObserver(destructionObserver);
}
@Override
@@ -933,9 +933,10 @@
if (mMostVisitedItemRemovedController != null) {
mTab.getSnackbarManager().dismissSnackbars(mMostVisitedItemRemovedController);
}
- if (mDestructionObserver != null) {
- mDestructionObserver.onDestroy();
+ for (DestructionObserver observer : mDestructionObservers) {
+ observer.onDestroy();
}
+ mDestructionObservers.clear();
TemplateUrlService.getInstance().removeObserver(this);
mTab.removeObserver(mTabObserver);
mTabObserver = null;
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
index 88400a2..5a4d51df4 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/NewTabPageView.java
@@ -57,7 +57,9 @@
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
import org.chromium.chrome.browser.ntp.snippets.SnippetsConfig;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver;
+import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.chrome.browser.util.ViewUtils;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
@@ -299,7 +301,7 @@
/**
* Registers a {@link DestructionObserver}, notified when the New Tab Page goes away.
*/
- void setDestructionObserver(DestructionObserver destructionObserver);
+ void addDestructionObserver(DestructionObserver destructionObserver);
/**
* @return whether the {@link NewTabPage} associated with this manager is the current page
@@ -389,7 +391,8 @@
// Set up snippets
if (mUseCardsUi) {
- mNewTabPageAdapter = new NewTabPageAdapter(mManager, mNewTabPageLayout, mUiConfig);
+ mNewTabPageAdapter = new NewTabPageAdapter(mManager, mNewTabPageLayout, mUiConfig,
+ OfflinePageBridge.getForProfile(Profile.getLastUsedProfile()));
mRecyclerView.setAdapter(mNewTabPageAdapter);
int scrollOffset;
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 4ba9f41..ba0890c 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
@@ -28,6 +28,7 @@
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.Collections;
@@ -49,6 +50,7 @@
private final View mAboveTheFoldView;
private final UiConfig mUiConfig;
private final ItemTouchCallbacks mItemTouchCallbacks = new ItemTouchCallbacks();
+ private final OfflinePageBridge mOfflineBridge;
private NewTabPageRecyclerView mRecyclerView;
/**
@@ -129,11 +131,16 @@
* @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 offlineBridge the OfflinePageBridge used to determine if articles are available
+ * offline.
+ *
*/
- public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig) {
+ public NewTabPageAdapter(NewTabPageManager manager, View aboveTheFoldView, UiConfig uiConfig,
+ OfflinePageBridge offlineBridge) {
mNewTabPageManager = manager;
mAboveTheFoldView = aboveTheFoldView;
mUiConfig = uiConfig;
+ mOfflineBridge = offlineBridge;
mRoot = new InnerNode(this) {
@Override
protected List<TreeNode> getChildren() {
@@ -161,7 +168,7 @@
mSigninPromo = new SignInPromo(mRoot, this);
DestructionObserver signInObserver = mSigninPromo.getObserver();
- if (signInObserver != null) mNewTabPageManager.setDestructionObserver(signInObserver);
+ if (signInObserver != null) mNewTabPageManager.addDestructionObserver(signInObserver);
resetSections(/*alwaysAllowEmptySections=*/false);
mNewTabPageManager.getSuggestionsSource().setObserver(this);
@@ -221,7 +228,7 @@
// Create the section if needed.
SuggestionsSection section = mSections.get(category);
if (section == null) {
- section = new SuggestionsSection(mRoot, info);
+ section = new SuggestionsSection(mRoot, info, mNewTabPageManager, mOfflineBridge);
mSections.put(category, section);
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java
index 433905d..d6876af7 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSection.java
@@ -4,19 +4,28 @@
package org.chromium.chrome.browser.ntp.cards;
+import org.chromium.base.Callback;
+import org.chromium.base.Promise;
import org.chromium.base.VisibleForTesting;
+import org.chromium.chrome.browser.ntp.NewTabPage.DestructionObserver;
+import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus.CategoryStatusEnum;
import org.chromium.chrome.browser.ntp.snippets.SectionHeader;
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.offlinepages.OfflinePageBridge;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge.OfflinePageModelObserver;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
- * A group of suggestions, with a header, a status card, and a progress indicator.
+ * A group of suggestions, with a header, a status card, and a progress indicator. This is
+ * responsible for tracking whether its suggestions have been saved offline.
*/
public class SuggestionsSection extends InnerNode {
private final List<TreeNode> mChildren = new ArrayList<>();
@@ -27,14 +36,32 @@
private final ProgressItem mProgressIndicator = new ProgressItem();
private final ActionItem mMoreButton;
private final SuggestionsCategoryInfo mCategoryInfo;
+ private final OfflinePageBridge mOfflinePageBridge;
- public SuggestionsSection(NodeParent parent, SuggestionsCategoryInfo info) {
+ public SuggestionsSection(NodeParent parent, SuggestionsCategoryInfo info,
+ NewTabPageManager manager, OfflinePageBridge offlineBridge) {
super(parent);
mHeader = new SectionHeader(info.getTitle());
mCategoryInfo = info;
mMoreButton = new ActionItem(info);
mStatus = StatusItem.createNoSuggestionsItem(info);
resetChildren();
+
+ mOfflinePageBridge = offlineBridge;
+ final OfflinePageModelObserver offlinePageObserver = new OfflinePageModelObserver() {
+ @Override
+ public void offlinePageModelChanged() {
+ markSnippetsAvailableOffline();
+ }
+ };
+
+ mOfflinePageBridge.addObserver(offlinePageObserver);
+ manager.addDestructionObserver(new DestructionObserver() {
+ @Override
+ public void onDestroy() {
+ mOfflinePageBridge.removeObserver(offlinePageObserver);
+ }
+ });
}
private class SuggestionsList extends ChildNode {
@@ -130,6 +157,8 @@
mSuggestions.clear();
mSuggestions.addAll(suggestions);
+ markSnippetsAvailableOffline();
+
if (mMoreButton != null) {
mMoreButton.setPosition(mSuggestions.size());
mMoreButton.setDismissable(mSuggestions.isEmpty());
@@ -138,6 +167,28 @@
notifySectionChanged(itemCountBefore);
}
+
+ /** Checks which SnippetArticles are available offline, and updates them accordingly. */
+ private void markSnippetsAvailableOffline() {
+ final Set<String> urls = new HashSet<>();
+ Promise<Set<String>> promise = new Promise<>();
+
+ for (final SnippetArticle article : mSuggestions) {
+ urls.add(article.mUrl);
+ urls.add(article.mAmpUrl);
+
+ promise.then(new Callback<Set<String>>() {
+ @Override
+ public void onResult(Set<String> offlineUrls) {
+ if (offlineUrls.contains(article.mUrl)) article.setAvailableOffline(true);
+ if (offlineUrls.contains(article.mAmpUrl)) article.setAmpAvailableOffline(true);
+ }
+ });
+ }
+
+ mOfflinePageBridge.checkPagesExistOffline(urls, promise.fulfillmentCallback());
+ }
+
/** Sets the status for the section. Some statuses can cause the suggestions to be cleared. */
public void setStatus(@CategoryStatusEnum int status) {
int itemCountBefore = getItemCount();
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticle.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticle.java
index 5f79c18..1b353aa 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticle.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticle.java
@@ -54,6 +54,15 @@
/** Stores whether impression of this article has been tracked already. */
private boolean mImpressionTracked;
+ /** Whether the linked article (normal URL) is available offline. */
+ private boolean mAvailableOffline;
+
+ /** Whether the linked AMP article is available offline. */
+ private boolean mAmpAvailableOffline;
+
+ /** To be run when the offline status of the article or AMP article changes. */
+ private Runnable mOfflineStatusChangeRunnable;
+
/**
* Creates a SnippetArticleListItem object that will hold the data.
*/
@@ -106,6 +115,44 @@
return true;
}
+ /** Sets whether the non-AMP URL is available offline. */
+ public void setAvailableOffline(boolean available) {
+ boolean previous = mAvailableOffline;
+ mAvailableOffline = available;
+
+ if (mOfflineStatusChangeRunnable != null && available != previous) {
+ mOfflineStatusChangeRunnable.run();
+ }
+ }
+
+ /** Sets whether the AMP URL is available offline. */
+ public void setAmpAvailableOffline(boolean available) {
+ boolean previous = mAmpAvailableOffline;
+ mAmpAvailableOffline = available;
+
+ if (mOfflineStatusChangeRunnable != null && available != previous) {
+ mOfflineStatusChangeRunnable.run();
+ }
+ }
+
+ /** Whether the non-AMP URL is available offline. */
+ public boolean isAvailableOffline() {
+ return mAvailableOffline;
+ }
+
+ /** Whether the AMP URL is available offline. */
+ public boolean isAmpAvailableOffline() {
+ return mAmpAvailableOffline;
+ }
+
+ /**
+ * Sets the {@link Runnable} to be run when the article's offline status changes.
+ * Pass null to wipe.
+ */
+ public void setOfflineStatusChangeRunnable(Runnable runnable) {
+ mOfflineStatusChangeRunnable = runnable;
+ }
+
@Override
public String toString() {
// For debugging purposes. Displays the first 42 characters of the title.
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleViewHolder.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleViewHolder.java
index cf2012b..9db836b 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleViewHolder.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetArticleViewHolder.java
@@ -60,6 +60,7 @@
private final TextView mPublisherTextView;
private final TextView mArticleSnippetTextView;
private final ImageView mThumbnailView;
+ private final ImageView mOfflineBadge;
private final View mPublisherBar;
private FetchImageCallback mImageCallback;
@@ -86,6 +87,7 @@
mPublisherTextView = (TextView) itemView.findViewById(R.id.article_publisher);
mArticleSnippetTextView = (TextView) itemView.findViewById(R.id.article_snippet);
mPublisherBar = itemView.findViewById(R.id.publisher_bar);
+ mOfflineBadge = (ImageView) itemView.findViewById(R.id.offline_icon);
new ImpressionTracker(itemView, this);
@@ -176,6 +178,9 @@
public void onBindViewHolder(SnippetArticle article) {
super.onBindViewHolder();
+ // No longer listen for offline status changes to the old article.
+ if (mArticle != null) mArticle.setOfflineStatusChangeRunnable(null);
+
mArticle = article;
updateLayout();
@@ -233,6 +238,20 @@
} catch (URISyntaxException e) {
setDefaultFaviconOnView();
}
+
+ mOfflineBadge.setVisibility(View.GONE);
+ if (SnippetsConfig.isOfflineBadgeEnabled()) {
+ Runnable offlineChecker = new Runnable() {
+ @Override
+ public void run() {
+ if (mArticle.isAvailableOffline() || mArticle.isAmpAvailableOffline()) {
+ mOfflineBadge.setVisibility(View.VISIBLE);
+ }
+ }
+ };
+ mArticle.setOfflineStatusChangeRunnable(offlineChecker);
+ offlineChecker.run();
+ }
}
private static class FetchImageCallback extends Callback<Bitmap> {
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsConfig.java b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsConfig.java
index c2e95e5..a2749b29 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsConfig.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/ntp/snippets/SnippetsConfig.java
@@ -23,6 +23,10 @@
&& OfflinePageBridge.isBackgroundLoadingEnabled();
}
+ public static boolean isOfflineBadgeEnabled() {
+ return ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_SNIPPETS_OFFLINE_BADGE);
+ }
+
public static boolean isSectionDismissalEnabled() {
return ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_SUGGESTIONS_SECTION_DISMISSAL);
}
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/snippets/ArticleSnippetsTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/snippets/ArticleSnippetsTest.java
index 25dbd899..65cc0b4 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/snippets/ArticleSnippetsTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/ntp/snippets/ArticleSnippetsTest.java
@@ -29,7 +29,9 @@
import org.chromium.chrome.browser.ntp.cards.NewTabPageAdapter;
import org.chromium.chrome.browser.ntp.cards.NewTabPageRecyclerView;
import org.chromium.chrome.browser.ntp.cards.SuggestionsCategoryInfo;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver;
+import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.test.ChromeActivityTestCaseBase;
import org.chromium.chrome.test.util.RenderUtils.ViewRenderer;
@@ -76,7 +78,8 @@
View aboveTheFold = new View(getActivity());
mRecyclerView.setAboveTheFoldView(aboveTheFold);
- mAdapter = new NewTabPageAdapter(mNtpManager, aboveTheFold, mUiConfig);
+ mAdapter = new NewTabPageAdapter(mNtpManager, aboveTheFold, mUiConfig,
+ OfflinePageBridge.getForProfile(Profile.getLastUsedProfile()));
mRecyclerView.setAdapter(mAdapter);
}
});
@@ -346,7 +349,7 @@
}
@Override
- public void setDestructionObserver(DestructionObserver destructionObserver) {}
+ public void addDestructionObserver(DestructionObserver destructionObserver) {}
@Override
public void closeContextMenu() {
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/ContentSuggestionsTestUtils.java b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/ContentSuggestionsTestUtils.java
index acb95386..85206799 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/ContentSuggestionsTestUtils.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/ContentSuggestionsTestUtils.java
@@ -4,9 +4,11 @@
package org.chromium.chrome.browser.ntp.cards;
+import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.ntp.snippets.CategoryInt;
import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsCardLayout;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import java.util.ArrayList;
import java.util.List;
@@ -32,9 +34,9 @@
category, "", ContentSuggestionsCardLayout.FULL_CARD, moreButton, showIfEmpty, "");
}
- public static SuggestionsSection createSection(
- boolean moreButton, boolean showIfEmpty, NodeParent parent) {
+ public static SuggestionsSection createSection(boolean moreButton, boolean showIfEmpty,
+ NodeParent parent, NewTabPageManager manager, OfflinePageBridge bridge) {
SuggestionsCategoryInfo info = createInfo(42, moreButton, showIfEmpty);
- return new SuggestionsSection(parent, info);
+ return new SuggestionsSection(parent, info, manager, bridge);
}
}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
index bb7d0bb..3fdba1f 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/NewTabPageAdapterTest.java
@@ -10,6 +10,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
@@ -19,16 +20,17 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.AdapterDataObserver;
-import android.view.Menu;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
@@ -36,17 +38,11 @@
import static org.chromium.chrome.browser.ntp.cards.ContentSuggestionsTestUtils.createDummySuggestions;
import static org.chromium.chrome.browser.ntp.cards.ContentSuggestionsTestUtils.createInfo;
-import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.EnableFeatures;
-import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback;
-import org.chromium.chrome.browser.favicon.FaviconHelper.IconAvailabilityCallback;
-import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback;
-import org.chromium.chrome.browser.ntp.LogoBridge.LogoObserver;
-import org.chromium.chrome.browser.ntp.MostVisitedItem;
import org.chromium.chrome.browser.ntp.NewTabPage.DestructionObserver;
import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.ntp.cards.SignInPromo.SigninObserver;
@@ -56,9 +52,8 @@
import org.chromium.chrome.browser.ntp.snippets.FakeSuggestionsSource;
import org.chromium.chrome.browser.ntp.snippets.KnownCategories;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
-import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
import org.chromium.chrome.browser.preferences.ChromePreferenceManager;
-import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver;
import org.chromium.chrome.browser.signin.SigninManager;
import org.chromium.chrome.browser.signin.SigninManager.SignInAllowedObserver;
import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver;
@@ -68,7 +63,6 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Set;
/**
* Unit tests for {@link NewTabPageAdapter}.
@@ -82,7 +76,8 @@
private FakeSuggestionsSource mSource;
private NewTabPageAdapter mAdapter;
private SigninManager mMockSigninManager;
- private MockNewTabPageManager mNtpManager;
+ @Mock private OfflinePageBridge mOfflinePageBridge;
+ @Mock private NewTabPageManager mNewTabPageManager;
/**
* Stores information about a section that should be present in the adapter.
@@ -225,6 +220,8 @@
@Before
public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
// Initialise the sign in state. We will be signed in by default in the tests.
assertFalse(ChromePreferenceManager.getInstance(RuntimeEnvironment.application)
.getNewTabPageSigninPromoDismissed());
@@ -241,8 +238,11 @@
mSource = new FakeSuggestionsSource();
mSource.setStatusForCategory(category, CategoryStatus.INITIALIZING);
mSource.setInfoForCategory(category, createInfo(category, false, true));
- mNtpManager = new MockNewTabPageManager(mSource);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(mSource);
+ when(mNewTabPageManager.isCurrentPage()).thenReturn(true);
+
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
}
@After
@@ -408,20 +408,20 @@
assertItemsFor();
// Same when loading a new NTP.
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor();
// Same for CATEGORY_EXPLICITLY_DISABLED.
mSource.setStatusForCategory(KnownCategories.ARTICLES, CategoryStatus.AVAILABLE);
mSource.setSuggestionsForCategory(KnownCategories.ARTICLES, snippets);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor(section(5));
mSource.setStatusForCategory(
KnownCategories.ARTICLES, CategoryStatus.CATEGORY_EXPLICITLY_DISABLED);
assertItemsFor();
// Same when loading a new NTP.
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor();
}
@@ -442,7 +442,7 @@
assertItemsFor(section(4));
// But it disappears when loading a new NTP.
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor();
}
@@ -463,8 +463,8 @@
suggestionsSource.setInfoForCategory(category, createInfo(category, false, true));
// 1.1 - Initial state
- mNtpManager = new MockNewTabPageManager(suggestionsSource);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor(sectionWithStatusCard());
// 1.2 - With suggestions
@@ -487,8 +487,8 @@
suggestionsSource.setInfoForCategory(category, createInfo(category, false, false));
// 2.1 - Initial state
- mNtpManager = new MockNewTabPageManager(suggestionsSource);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor();
// 2.2 - With suggestions
@@ -519,8 +519,8 @@
suggestionsSource.setInfoForCategory(category, createInfo(category, true, true));
// 1.1 - Initial state.
- mNtpManager = new MockNewTabPageManager(suggestionsSource);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor(sectionWithStatusCardAndMoreButton());
// 1.2 - With suggestions.
@@ -543,8 +543,8 @@
suggestionsSource.setInfoForCategory(category, createInfo(category, false, true));
// 2.1 - Initial state.
- mNtpManager = new MockNewTabPageManager(suggestionsSource);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor(sectionWithStatusCard());
// 2.2 - With suggestions.
@@ -602,7 +602,9 @@
mSource.setInfoForCategory(dynamicCategory1, createInfo(dynamicCategory1, true, false));
mSource.setStatusForCategory(dynamicCategory1, CategoryStatus.AVAILABLE);
mSource.setSuggestionsForCategory(dynamicCategory1, dynamics1);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null); // Reload
+ // Reload
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
+
assertItemsFor(section(3), sectionWithMoreButton(5));
int dynamicCategory2 = 1011;
@@ -610,7 +612,8 @@
mSource.setInfoForCategory(dynamicCategory2, createInfo(dynamicCategory1, false, false));
mSource.setStatusForCategory(dynamicCategory2, CategoryStatus.AVAILABLE);
mSource.setSuggestionsForCategory(dynamicCategory2, dynamics2);
- mAdapter = new NewTabPageAdapter(mNtpManager, null, null); // Reload
+ // Reload
+ mAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertItemsFor(section(3), sectionWithMoreButton(5), section(11));
}
@@ -627,8 +630,9 @@
registerCategory(suggestionsSource, KnownCategories.PHYSICAL_WEB_PAGES, 0);
registerCategory(suggestionsSource, KnownCategories.DOWNLOADS, 0);
- NewTabPageAdapter ntpAdapter =
- new NewTabPageAdapter(new MockNewTabPageManager(suggestionsSource), null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ NewTabPageAdapter ntpAdapter = new NewTabPageAdapter(
+ mNewTabPageManager, null, null, mOfflinePageBridge);
List<TreeNode> children = ntpAdapter.getChildren();
assertEquals(basicChildCount + 4, children.size());
@@ -649,8 +653,9 @@
registerCategory(suggestionsSource, KnownCategories.DOWNLOADS, 0);
registerCategory(suggestionsSource, KnownCategories.BOOKMARKS, 0);
- ntpAdapter =
- new NewTabPageAdapter(new MockNewTabPageManager(suggestionsSource), null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ ntpAdapter = new NewTabPageAdapter(
+ mNewTabPageManager, null, null, mOfflinePageBridge);
children = ntpAdapter.getChildren();
assertEquals(basicChildCount + 4, children.size());
@@ -670,8 +675,8 @@
registerCategory(suggestionsSource, KnownCategories.PHYSICAL_WEB_PAGES, 0);
registerCategory(suggestionsSource, KnownCategories.DOWNLOADS, 0);
- ntpAdapter =
- new NewTabPageAdapter(new MockNewTabPageManager(suggestionsSource), null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ ntpAdapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
// The adapter is already initialised, it will not accept new categories anymore.
registerCategory(suggestionsSource, 42, 1);
@@ -697,8 +702,9 @@
doNothing().when(suggestionsSource).dismissSuggestion(any(SnippetArticle.class));
registerCategory(suggestionsSource, KnownCategories.ARTICLES, 3);
- NewTabPageAdapter adapter =
- new NewTabPageAdapter(new MockNewTabPageManager(suggestionsSource), null, null);
+ when(mNewTabPageManager.getSuggestionsSource()).thenReturn(suggestionsSource);
+ NewTabPageAdapter adapter = new NewTabPageAdapter(
+ mNewTabPageManager, null, null, mOfflinePageBridge);
AdapterDataObserver dataObserver = mock(AdapterDataObserver.class);
adapter.registerAdapterDataObserver(dataObserver);
@@ -772,8 +778,13 @@
public void testSigninPromo() {
when(mMockSigninManager.isSignInAllowed()).thenReturn(true);
when(mMockSigninManager.isSignedInOnNative()).thenReturn(false);
- MockNewTabPageManager ntpManager = new MockNewTabPageManager(mSource);
- NewTabPageAdapter adapter = new NewTabPageAdapter(ntpManager, null, null);
+ ArgumentCaptor<DestructionObserver> observers =
+ ArgumentCaptor.forClass(DestructionObserver.class);
+
+ doNothing().when(mNewTabPageManager).addDestructionObserver(observers.capture());
+
+ NewTabPageAdapter adapter =
+ new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
TreeNode signinPromo = adapter.getChildren().get(2);
@@ -792,18 +803,33 @@
assertEquals(1, signinPromo.getItemCount());
assertEquals(ItemViewType.PROMO, signinPromo.getItemViewType(0));
- ((SignInStateObserver) ntpManager.mDestructionObserver).onSignedIn();
+ // verify(mNewTabPageManager).addDestructionObserver(observers.capture());
+
+ // Note: As currently implemented, these two variables should point to the same object, a
+ // SignInPromo.SigninObserver
+ SignInStateObserver signInStateObserver = null;
+ SignInAllowedObserver signInAllowedObserver = null;
+ for (DestructionObserver observer : observers.getAllValues()) {
+ if (observer instanceof SignInStateObserver) {
+ signInStateObserver = (SignInStateObserver) observer;
+ }
+ if (observer instanceof SignInAllowedObserver) {
+ signInAllowedObserver = (SignInAllowedObserver) observer;
+ }
+ }
+
+ signInStateObserver.onSignedIn();
assertEquals(0, signinPromo.getItemCount());
- ((SignInStateObserver) ntpManager.mDestructionObserver).onSignedOut();
+ signInStateObserver.onSignedOut();
assertEquals(1, signinPromo.getItemCount());
when(mMockSigninManager.isSignInAllowed()).thenReturn(false);
- ((SignInAllowedObserver) ntpManager.mDestructionObserver).onSignInAllowedChanged();
+ signInAllowedObserver.onSignInAllowedChanged();
assertEquals(0, signinPromo.getItemCount());
when(mMockSigninManager.isSignInAllowed()).thenReturn(true);
- ((SignInAllowedObserver) ntpManager.mDestructionObserver).onSignInAllowedChanged();
+ signInAllowedObserver.onSignInAllowedChanged();
assertEquals(1, signinPromo.getItemCount());
}
@@ -814,8 +840,10 @@
when(mMockSigninManager.isSignedInOnNative()).thenReturn(false);
ChromePreferenceManager.getInstance(RuntimeEnvironment.application)
.setNewTabPageSigninPromoDismissed(false);
- MockNewTabPageManager ntpManager = new MockNewTabPageManager(mSource);
- NewTabPageAdapter adapter = new NewTabPageAdapter(ntpManager, null, null);
+
+ // TODO(peconn)
+ NewTabPageAdapter adapter =
+ new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
final int signInPromoIndex = 5;
assertEquals(5, adapter.getChildren().size());
@@ -840,7 +868,7 @@
assertTrue(ChromePreferenceManager.getInstance(RuntimeEnvironment.application)
.getNewTabPageSigninPromoDismissed());
- adapter = new NewTabPageAdapter(ntpManager, null, null);
+ adapter = new NewTabPageAdapter(mNewTabPageManager, null, null, mOfflinePageBridge);
assertEquals(5, adapter.getChildren().size());
// The items below the signin promo move up, footer is now at the position of the promo.
assertEquals(ItemViewType.FOOTER, adapter.getItemViewType(signInPromoIndex));
@@ -850,7 +878,17 @@
@Feature({"Ntp"})
@EnableFeatures(ChromeFeatureList.NTP_SUGGESTIONS_SECTION_DISMISSAL)
public void testAllDismissedVisibility() {
- SigninObserver signinObserver = (SigninObserver) mNtpManager.mDestructionObserver;
+ ArgumentCaptor<DestructionObserver> observers =
+ ArgumentCaptor.forClass(DestructionObserver.class);
+
+ verify(mNewTabPageManager, atLeastOnce()).addDestructionObserver(observers.capture());
+
+ SigninObserver signinObserver = null;
+ for (DestructionObserver observer : observers.getAllValues()) {
+ if (observer instanceof SigninObserver) {
+ signinObserver = (SigninObserver) observer;
+ }
+ }
// By default, there is no All Dismissed item.
// Adapter content:
@@ -960,174 +998,4 @@
private int getCategory(TreeNode item) {
return ((SuggestionsSection) item).getCategory();
}
-
- private static class MockNewTabPageManager implements NewTabPageManager {
- SuggestionsSource mSuggestionsSource;
- DestructionObserver mDestructionObserver;
-
- public MockNewTabPageManager(SuggestionsSource suggestionsSource) {
- mSuggestionsSource = suggestionsSource;
- }
-
- @Override
- public void removeMostVisitedItem(MostVisitedItem item) {
- throw new UnsupportedOperationException();
-
- }
-
- @Override
- public void openMostVisitedItem(int windowDisposition, MostVisitedItem item) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isLocationBarShownInNTP() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isVoiceSearchEnabled() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isFakeOmniboxTextEnabledTablet() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isOpenInNewWindowEnabled() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isOpenInIncognitoEnabled() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void navigateToBookmarks() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void navigateToRecentTabs() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void navigateToDownloadManager() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void trackSnippetsPageImpression(int[] categories, int[] suggestionsPerCategory) {
- }
-
- @Override
- public void trackSnippetImpression(SnippetArticle article) {
- }
-
- @Override
- public void trackSnippetMenuOpened(SnippetArticle article) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void trackSnippetCategoryActionImpression(int category, int position) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void trackSnippetCategoryActionClick(int category, int position) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void openSnippet(int windowOpenDisposition, SnippetArticle article) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void focusSearchBox(boolean beginVoiceSearch, String pastedText) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void setMostVisitedURLsObserver(MostVisitedURLsObserver observer, int numResults) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void getLocalFaviconImageForURL(String url, int size,
- FaviconImageCallback faviconCallback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void getLargeIconForUrl(String url, int size, LargeIconCallback callback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void ensureIconIsAvailable(String pageUrl, String iconUrl, boolean isLargeIcon,
- boolean isTemporary, IconAvailabilityCallback callback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void getUrlsAvailableOffline(Set<String> pageUrls, Callback<Set<String>> callback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void onLogoClicked(boolean isAnimatedLogoShowing) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void getSearchProviderLogo(LogoObserver logoObserver) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void onLoadingComplete(MostVisitedItem[] mostVisitedItems) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void addContextMenuCloseCallback(Callback<Menu> callback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void removeContextMenuCloseCallback(Callback<Menu> callback) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void onLearnMoreClicked() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- @Nullable public SuggestionsSource getSuggestionsSource() {
- return mSuggestionsSource;
- }
-
- @Override
- public void closeContextMenu() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void setDestructionObserver(DestructionObserver destructionObserver) {
- mDestructionObserver = destructionObserver;
- }
-
- @Override
- public boolean isCurrentPage() {
- return true;
- }
- }
}
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSectionTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSectionTest.java
index def83db3..ac39cb5 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSectionTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/ntp/cards/SuggestionsSectionTest.java
@@ -5,9 +5,12 @@
package org.chromium.chrome.browser.ntp.cards;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anySetOf;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -18,16 +21,26 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config;
+import org.chromium.base.Callback;
import org.chromium.base.test.util.Feature;
+import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.ntp.snippets.CategoryStatus;
import org.chromium.chrome.browser.ntp.snippets.SnippetArticle;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge;
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge.OfflinePageModelObserver;
import org.chromium.testing.local.LocalRobolectricTestRunner;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
/**
* Unit tests for {@link SuggestionsSection}.
@@ -40,8 +53,12 @@
*/
private static final int EMPTY_SECTION_COUNT = 4;
- @Mock
- private NodeParent mParent;
+ @Mock private NodeParent mParent;
+ @Mock private OfflinePageBridge mBridge;
+ @Mock private NewTabPageManager mManager;
+
+ // This is a member so we can initialize it with the annotation and capture the generic type.
+ @Captor ArgumentCaptor<Callback<Set<String>>> mCallbacks;
@Before
public void setUp() {
@@ -54,7 +71,7 @@
List<SnippetArticle> snippets = createDummySuggestions(3);
SuggestionsSection section;
- section = ContentSuggestionsTestUtils.createSection(true, true, mParent);
+ section = ContentSuggestionsTestUtils.createSection(true, true, mParent, mManager, mBridge);
section.setStatus(CategoryStatus.AVAILABLE);
assertNotNull(section.getActionItem());
@@ -76,7 +93,7 @@
final int suggestionCount = 5;
List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
- SuggestionsSection section = createSection(false, true, mParent);
+ SuggestionsSection section = createSection(false, true, mParent, mManager, mBridge);
// Note: when status is not initialised, we insert an item for the status card, but it's
// null!
assertEquals(EMPTY_SECTION_COUNT, section.getItemCount());
@@ -93,7 +110,7 @@
final int suggestionCount = 5;
List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
- SuggestionsSection section = createSection(false, true, mParent);
+ SuggestionsSection section = createSection(false, true, mParent, mManager, mBridge);
section.setStatus(CategoryStatus.AVAILABLE);
verify(mParent).onItemRangeChanged(section, 1, EMPTY_SECTION_COUNT - 1);
@@ -119,7 +136,7 @@
final int suggestionCount = 2;
List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
- SuggestionsSection section = createSection(false, true, mParent);
+ SuggestionsSection section = createSection(false, true, mParent, mManager, mBridge);
section.removeSuggestion(snippets.get(0));
verify(mParent, never())
@@ -145,7 +162,7 @@
final int suggestionCount = 2;
List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
- SuggestionsSection section = createSection(true, true, mParent);
+ SuggestionsSection section = createSection(true, true, mParent, mManager, mBridge);
section.removeSuggestion(snippets.get(0));
verify(mParent, never())
@@ -164,4 +181,64 @@
verify(mParent, times(2)).onItemRangeRemoved(section, 1, 1);
verify(mParent).onItemRangeInserted(section, 1, 3);
}
+
+ @Test
+ @Feature({"Ntp"})
+ public void testOfflineStatus() {
+ final int suggestionCount = 2;
+ List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
+
+ assertFalse(snippets.get(0).isAvailableOffline());
+ assertFalse(snippets.get(0).isAmpAvailableOffline());
+ assertFalse(snippets.get(1).isAvailableOffline());
+ assertFalse(snippets.get(1).isAmpAvailableOffline());
+
+ SuggestionsSection section = createSection(true, true, mParent, mManager, mBridge);
+ section.setSuggestions(snippets, CategoryStatus.AVAILABLE);
+ verify(mBridge).checkPagesExistOffline(anySetOf(String.class), mCallbacks.capture());
+
+ // The callback is asynchronous.
+ mCallbacks.getValue().onResult(new HashSet<>(Arrays.asList(
+ snippets.get(0).mUrl,
+ snippets.get(1).mAmpUrl)));
+
+ assertTrue(snippets.get(0).isAvailableOffline());
+ assertFalse(snippets.get(0).isAmpAvailableOffline());
+ assertFalse(snippets.get(1).isAvailableOffline());
+ assertTrue(snippets.get(1).isAmpAvailableOffline());
+ }
+
+ @Test
+ @Feature({"Ntp"})
+ public void testOfflineStatusUpdate() {
+ ArgumentCaptor<OfflinePageModelObserver> observer =
+ ArgumentCaptor.forClass(OfflinePageModelObserver.class);
+
+ final int suggestionCount = 1;
+ List<SnippetArticle> snippets = createDummySuggestions(suggestionCount);
+
+ assertFalse(snippets.get(0).isAvailableOffline());
+ assertFalse(snippets.get(0).isAmpAvailableOffline());
+
+ SuggestionsSection section = createSection(true, true, mParent, mManager, mBridge);
+ section.setSuggestions(snippets, CategoryStatus.AVAILABLE);
+
+ // The first callback, triggered when we called setSuggestions.
+ verify(mBridge).checkPagesExistOffline(anySetOf(String.class), mCallbacks.capture());
+ mCallbacks.getAllValues().get(0).onResult(Collections.<String>emptySet());
+
+ verify(mBridge).addObserver(observer.capture());
+ observer.getValue().offlinePageModelChanged();
+
+ // The second callback, triggered by the offlinePageModelChanged notification.
+ verify(mBridge, times(2))
+ .checkPagesExistOffline(anySetOf(String.class), mCallbacks.capture());
+
+ mCallbacks.getAllValues().get(1).onResult(new HashSet<>(Arrays.asList(
+ snippets.get(0).mUrl,
+ snippets.get(0).mAmpUrl)));
+
+ assertTrue(snippets.get(0).isAvailableOffline());
+ assertTrue(snippets.get(0).isAmpAvailableOffline());
+ }
}
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index d6555dd..ed0cd72 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -15269,6 +15269,12 @@
<message name="IDS_FLAGS_ENABLE_NTP_SAVE_TO_OFFLINE_DESCRIPTION" desc="Description for the flag to enable offline page suggestions on the New Tab page." translateable="false">
If enabled, the Snippets context menu (see #enable-ntp-snippets) will contain the option to save linked page for offline viewing.
</message>
+ <message name="IDS_FLAGS_ENABLE_NTP_OFFLINE_BADGE_NAME" desc="Name for the flag to enable offline badges for snippets on the New Tab page." translateable="false">
+ Show offline badge for offline available snippets on the ntp.
+ </message>
+ <message name="IDS_FLAGS_ENABLE_NTP_OFFLINE_BADGE_DESCRIPTION" desc="Description for the flag to enable offline badges for snippets on the New Tab page." translateable="false">
+ If enabled, Snippets that are available offline will have an offline badge.
+ </message>
<message name="IDS_FLAGS_ENABLE_NTP_DOWNLOAD_SUGGESTIONS_NAME" desc="Name for the flag to enable downloads suggestions on the New Tab page." translateable="false">
Show downloads on the New Tab page
</message>
diff --git a/chrome/browser/about_flags.cc b/chrome/browser/about_flags.cc
index b04f658..a1c4291 100644
--- a/chrome/browser/about_flags.cc
+++ b/chrome/browser/about_flags.cc
@@ -1841,6 +1841,9 @@
{"enable-ntp-save-to-offline", IDS_FLAGS_ENABLE_NTP_SAVE_TO_OFFLINE_NAME,
IDS_FLAGS_ENABLE_NTP_SAVE_TO_OFFLINE_DESCRIPTION, kOsAndroid,
FEATURE_VALUE_TYPE(ntp_snippets::kSaveToOfflineFeature)},
+ {"enable-ntp-offline-badge", IDS_FLAGS_ENABLE_NTP_OFFLINE_BADGE_NAME,
+ IDS_FLAGS_ENABLE_NTP_OFFLINE_BADGE_DESCRIPTION, kOsAndroid,
+ FEATURE_VALUE_TYPE(ntp_snippets::kOfflineBadgeFeature)},
{"enable-ntp-recent-offline-tab-suggestions",
IDS_FLAGS_ENABLE_NTP_RECENT_OFFLINE_TAB_SUGGESTIONS_NAME,
IDS_FLAGS_ENABLE_NTP_RECENT_OFFLINE_TAB_SUGGESTIONS_DESCRIPTION,
diff --git a/chrome/browser/android/chrome_feature_list.cc b/chrome/browser/android/chrome_feature_list.cc
index 3359f17..07e5ef85 100644
--- a/chrome/browser/android/chrome_feature_list.cc
+++ b/chrome/browser/android/chrome_feature_list.cc
@@ -53,6 +53,7 @@
&kTabReparenting,
&kWebApks,
&ntp_snippets::kContentSuggestionsFeature,
+ &ntp_snippets::kOfflineBadgeFeature,
&ntp_snippets::kSaveToOfflineFeature,
&ntp_snippets::kSectionDismissalFeature,
&offline_pages::kBackgroundLoaderForDownloadsFeature,
diff --git a/components/ntp_snippets/features.cc b/components/ntp_snippets/features.cc
index b499f0e..f440c23 100644
--- a/components/ntp_snippets/features.cc
+++ b/components/ntp_snippets/features.cc
@@ -21,6 +21,9 @@
const base::Feature kSaveToOfflineFeature{
"NTPSaveToOffline", base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kOfflineBadgeFeature{
+ "NTPOfflineBadge", base::FEATURE_DISABLED_BY_DEFAULT};
+
const base::Feature kDownloadSuggestionsFeature{
"NTPDownloadSuggestions", base::FEATURE_DISABLED_BY_DEFAULT};
diff --git a/components/ntp_snippets/features.h b/components/ntp_snippets/features.h
index 1449d78..bf83e17 100644
--- a/components/ntp_snippets/features.h
+++ b/components/ntp_snippets/features.h
@@ -23,6 +23,9 @@
// context menu.
extern const base::Feature kSaveToOfflineFeature;
+// Feature to allow offline badges to appear on snippets.
+extern const base::Feature kOfflineBadgeFeature;
+
// Feature to allow dismissing sections.
extern const base::Feature kSectionDismissalFeature;
diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml
index e97c34d..9e8ac86 100644
--- a/tools/metrics/histograms/histograms.xml
+++ b/tools/metrics/histograms/histograms.xml
@@ -90071,6 +90071,7 @@
<int value="346711293" label="enable-save-password-bubble"/>
<int value="348854923" label="v8-cache-strategies-for-cache-storage"/>
<int value="358399482" label="enable-high-dpi-fixed-position-compositing"/>
+ <int value="360391863" label="NTPOfflineBadge:enabled"/>
<int value="360599302" label="enable-gpu-rasterization"/>
<int value="365467768" label="prefetch-search-results"/>
<int value="368854020" label="ash-screen-rotation-animation"/>
@@ -90196,6 +90197,7 @@
<int value="943319566" label="enable-intent-picker"/>
<int value="952558794" label="enable-remote-assistance"/>
<int value="980396200" label="enable-new-korean-ime"/>
+ <int value="982032277" label="NTPOfflineBadge:disabled"/>
<int value="983311394" label="tab-management-experiment-type"/>
<int value="1000706989" label="AutomaticTabDiscarding:disabled"/>
<int value="1002585107" label="emphasize-titles-in-omnibox-dropdown"/>