Index: chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsManagerImpl.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsManagerImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsManagerImpl.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..39492cbcebe7aeb5d1dc4490588c631b64d5d093 |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/suggestions/SuggestionsManagerImpl.java |
@@ -0,0 +1,366 @@ |
+// Copyright 2017 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.suggestions; |
+ |
+import android.app.Activity; |
+import android.net.Uri; |
+import android.os.SystemClock; |
+ |
+import org.chromium.base.Callback; |
+import org.chromium.base.ObserverList; |
+import org.chromium.base.ThreadUtils; |
+import org.chromium.base.metrics.RecordHistogram; |
+import org.chromium.base.metrics.RecordUserAction; |
+import org.chromium.chrome.browser.ChromeFeatureList; |
+import org.chromium.chrome.browser.UrlConstants; |
+import org.chromium.chrome.browser.bookmarks.BookmarkUtils; |
+import org.chromium.chrome.browser.download.DownloadUtils; |
+import org.chromium.chrome.browser.favicon.FaviconHelper; |
+import org.chromium.chrome.browser.favicon.FaviconHelper.FaviconImageCallback; |
+import org.chromium.chrome.browser.favicon.FaviconHelper.IconAvailabilityCallback; |
+import org.chromium.chrome.browser.favicon.LargeIconBridge; |
+import org.chromium.chrome.browser.favicon.LargeIconBridge.LargeIconCallback; |
+import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
+import org.chromium.chrome.browser.ntp.NewTabPage.DestructionObserver; |
+import org.chromium.chrome.browser.ntp.NewTabPageUma; |
+import org.chromium.chrome.browser.ntp.snippets.KnownCategories; |
+import org.chromium.chrome.browser.ntp.snippets.SnippetArticle; |
+import org.chromium.chrome.browser.ntp.snippets.SnippetsBridge; |
+import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; |
+import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; |
+import org.chromium.chrome.browser.offlinepages.OfflinePageUtils; |
+import org.chromium.chrome.browser.preferences.PrefServiceBridge; |
+import org.chromium.chrome.browser.profiles.Profile; |
+import org.chromium.chrome.browser.tab.Tab; |
+import org.chromium.chrome.browser.tabmodel.TabModel; |
+import org.chromium.chrome.browser.tabmodel.TabModel.TabLaunchType; |
+import org.chromium.chrome.browser.tabmodel.TabModelSelector; |
+import org.chromium.chrome.browser.tabmodel.TabModelUtils; |
+import org.chromium.chrome.browser.tabmodel.document.TabDelegate; |
+import org.chromium.content_public.browser.LoadUrlParams; |
+import org.chromium.content_public.common.Referrer; |
+import org.chromium.ui.base.PageTransition; |
+import org.chromium.ui.mojom.WindowOpenDisposition; |
+ |
+import java.util.HashSet; |
+import java.util.Set; |
+import java.util.concurrent.TimeUnit; |
+ |
+/** |
+ * {@link ContentSuggestionsManager} implementation. |
+ */ |
+public class SuggestionsManagerImpl implements ContentSuggestionsManager { |
+ private static final String CHROME_CONTENT_SUGGESTIONS_REFERRER = |
+ "https://www.googleapis.com/auth/chrome-content-suggestions"; |
+ |
+ private final ObserverList<DestructionObserver> mDestructionObservers = new ObserverList<>(); |
+ private final SuggestionsSource mSuggestionsSource; |
+ private SnippetsBridge mSnippetsBridge; |
+ private final Activity mActivity; |
+ private final Profile mProfile; |
+ |
+ private final Tab mTab; |
+ private final TabModelSelector mTabModelSelector; |
+ |
+ private FaviconHelper mFaviconHelper; |
+ private LargeIconBridge mLargeIconBridge; |
+ |
+ private boolean mIsDestroyed; |
+ |
+ public SuggestionsManagerImpl(SuggestionsSource suggestionsSource, Activity activity, |
+ Profile profile, Tab currentTab, TabModelSelector tabModelSelector) { |
+ mSuggestionsSource = suggestionsSource; |
+ mActivity = activity; |
+ mProfile = profile; |
+ mTab = currentTab; |
+ mTabModelSelector = tabModelSelector; |
+ mSnippetsBridge = (SnippetsBridge) suggestionsSource; |
+ } |
+ |
+ @Override |
+ public boolean isOpenInNewWindowEnabled() { |
+ return MultiWindowUtils.getInstance().isOpenInOtherWindowSupported(mActivity); |
+ } |
+ |
+ @Override |
+ public boolean isOpenInIncognitoEnabled() { |
+ return PrefServiceBridge.getInstance().isIncognitoModeEnabled(); |
+ } |
+ |
+ @Override |
+ public void navigateToBookmarks() { |
+ if (mIsDestroyed) return; |
+ RecordUserAction.record("MobileNTPSwitchToBookmarks"); |
+ BookmarkUtils.showBookmarkManager(mActivity); |
+ } |
+ |
+ @Override |
+ public void navigateToRecentTabs() { |
+ if (mIsDestroyed) return; |
+ RecordUserAction.record("MobileNTPSwitchToOpenTabs"); |
+ mTab.loadUrl(new LoadUrlParams(UrlConstants.RECENT_TABS_URL)); |
+ } |
+ |
+ @Override |
+ public void navigateToDownloadManager() { |
+ if (mIsDestroyed) return; |
+ assert DownloadUtils.isDownloadHomeEnabled(); |
+ |
+ RecordUserAction.record("MobileNTPSwitchToDownloadManager"); |
+ DownloadUtils.showDownloadManager(mActivity, mTab); |
+ } |
+ |
+ @Override |
+ public void onLearnMoreClicked() { |
+ if (mIsDestroyed) return; |
+ NewTabPageUma.recordAction(NewTabPageUma.ACTION_CLICKED_LEARN_MORE); |
+ String url = "https://support.google.com/chrome/?p=new_tab"; |
+ // TODO(mastiz): Change this to LINK? |
+ openUrl(WindowOpenDisposition.CURRENT_TAB, |
+ new LoadUrlParams(url, PageTransition.AUTO_BOOKMARK)); |
+ } |
+ |
+ @Override |
+ public void openSnippet(int windowOpenDisposition, SnippetArticle article) { |
+ mSnippetsBridge.onSuggestionOpened(article, windowOpenDisposition); |
+ NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_SNIPPET); |
+ |
+ if (article.mIsAssetDownload) { |
+ assert windowOpenDisposition == WindowOpenDisposition.CURRENT_TAB |
+ || windowOpenDisposition == WindowOpenDisposition.NEW_WINDOW |
+ || windowOpenDisposition == WindowOpenDisposition.NEW_FOREGROUND_TAB; |
+ DownloadUtils.openFile( |
+ article.getAssetDownloadFile(), article.getAssetDownloadMimeType(), false); |
+ return; |
+ } |
+ |
+ if (article.isRecentTab()) { |
+ assert windowOpenDisposition == WindowOpenDisposition.CURRENT_TAB; |
+ // TODO(vitaliii): Add a debug check that the result is true after crbug.com/662924 |
+ // is resolved. |
+ openRecentTabSnippet(article); |
+ return; |
+ } |
+ |
+ // TODO(treib): Also track other dispositions. crbug.com/665915 |
+ if (windowOpenDisposition == WindowOpenDisposition.CURRENT_TAB) { |
+ NewTabPageUma.monitorContentSuggestionVisit(mTab, article.mCategory); |
+ } |
+ |
+ LoadUrlParams loadUrlParams; |
+ // We explicitly open an offline page only for offline page downloads. For all other |
+ // sections the URL is opened and it is up to Offline Pages whether to open its offline |
+ // page (e.g. when offline). |
+ if (article.isDownload() && !article.mIsAssetDownload) { |
+ assert article.getOfflinePageOfflineId() != null; |
+ assert windowOpenDisposition == WindowOpenDisposition.CURRENT_TAB |
+ || windowOpenDisposition == WindowOpenDisposition.NEW_WINDOW |
+ || windowOpenDisposition == WindowOpenDisposition.NEW_FOREGROUND_TAB; |
+ loadUrlParams = OfflinePageUtils.getLoadUrlParamsForOpeningOfflineVersion( |
+ article.mUrl, article.getOfflinePageOfflineId()); |
+ // Extra headers are not read in loadUrl, but verbatim headers are. |
+ loadUrlParams.setVerbatimHeaders(loadUrlParams.getExtraHeadersString()); |
+ } else { |
+ loadUrlParams = new LoadUrlParams(article.mUrl, PageTransition.AUTO_BOOKMARK); |
+ } |
+ |
+ // For article suggestions, we set the referrer. This is exploited |
+ // to filter out these history entries for NTP tiles. |
+ // TODO(mastiz): Extend this with support for other categories. |
+ if (article.mCategory == KnownCategories.ARTICLES) { |
+ loadUrlParams.setReferrer(new Referrer( |
+ CHROME_CONTENT_SUGGESTIONS_REFERRER, Referrer.REFERRER_POLICY_ALWAYS)); |
+ } |
+ |
+ openUrl(windowOpenDisposition, loadUrlParams); |
+ } |
+ |
+ @Override |
+ public void openUrl(int windowOpenDisposition, LoadUrlParams loadUrlParams) { |
+ assert !mIsDestroyed; |
+ switch (windowOpenDisposition) { |
+ case WindowOpenDisposition.CURRENT_TAB: |
+ mTab.loadUrl(loadUrlParams); |
+ break; |
+ case WindowOpenDisposition.NEW_FOREGROUND_TAB: |
+ openUrlInNewTab(loadUrlParams, false); |
+ break; |
+ case WindowOpenDisposition.OFF_THE_RECORD: |
+ openUrlInNewTab(loadUrlParams, true); |
+ break; |
+ case WindowOpenDisposition.NEW_WINDOW: |
+ openUrlInNewWindow(loadUrlParams); |
+ break; |
+ case WindowOpenDisposition.SAVE_TO_DISK: |
+ saveUrlForOffline(loadUrlParams.getUrl()); |
+ break; |
+ default: |
+ assert false; |
+ } |
+ } |
+ |
+ @Override |
+ public void trackSnippetsPageImpression(int[] categories, int[] suggestionsPerCategory) { |
+ mSnippetsBridge.onPageShown(categories, suggestionsPerCategory); |
+ } |
+ |
+ @Override |
+ public void trackSnippetImpression(SnippetArticle article) { |
+ mSnippetsBridge.onSuggestionShown(article); |
+ } |
+ |
+ @Override |
+ public void trackSnippetMenuOpened(SnippetArticle article) { |
+ mSnippetsBridge.onSuggestionMenuOpened(article); |
+ } |
+ |
+ @Override |
+ public void trackSnippetCategoryActionImpression(int category, int position) { |
+ mSnippetsBridge.onMoreButtonShown(category, position); |
+ } |
+ |
+ @Override |
+ public void trackSnippetCategoryActionClick(int category, int position) { |
+ mSnippetsBridge.onMoreButtonClicked(category, position); |
+ switch (category) { |
+ case KnownCategories.BOOKMARKS: |
+ NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_BOOKMARKS_MANAGER); |
+ break; |
+ // MORE button in both categories leads to the recent tabs manager |
+ case KnownCategories.FOREIGN_TABS: |
+ case KnownCategories.RECENT_TABS: |
+ NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_RECENT_TABS_MANAGER); |
+ break; |
+ case KnownCategories.DOWNLOADS: |
+ NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_DOWNLOADS_MANAGER); |
+ break; |
+ default: |
+ // No action associated |
+ break; |
+ } |
+ } |
+ |
+ @Override |
+ public void getLocalFaviconImageForURL( |
+ String url, int size, FaviconImageCallback faviconCallback) { |
+ if (mIsDestroyed) return; |
+ if (mFaviconHelper == null) mFaviconHelper = new FaviconHelper(); |
+ mFaviconHelper.getLocalFaviconImageForURL(mProfile, url, size, faviconCallback); |
+ } |
+ |
+ @Override |
+ public void getLargeIconForUrl(String url, int size, LargeIconCallback callback) { |
+ if (mIsDestroyed) return; |
+ if (mLargeIconBridge == null) mLargeIconBridge = new LargeIconBridge(mProfile); |
+ mLargeIconBridge.getLargeIconForUrl(url, size, callback); |
+ } |
+ |
+ @Override |
+ public void ensureIconIsAvailable(String pageUrl, String iconUrl, boolean isLargeIcon, |
+ boolean isTemporary, IconAvailabilityCallback callback) { |
+ if (mIsDestroyed) return; |
+ if (mFaviconHelper == null) mFaviconHelper = new FaviconHelper(); |
+ mFaviconHelper.ensureIconIsAvailable(mProfile, mTab.getWebContents(), pageUrl, iconUrl, |
+ isLargeIcon, isTemporary, callback); |
+ } |
+ |
+ @Override |
+ public void getUrlsAvailableOffline( |
+ Set<String> pageUrls, final Callback<Set<String>> callback) { |
+ final Set<String> urlsAvailableOffline = new HashSet<>(); |
+ if (mIsDestroyed || !isNtpOfflinePagesEnabled()) { |
+ callback.onResult(urlsAvailableOffline); |
+ return; |
+ } |
+ |
+ HashSet<String> urlsToCheckForOfflinePage = new HashSet<>(); |
+ |
+ for (String pageUrl : pageUrls) { |
+ if (isLocalUrl(pageUrl)) { |
+ urlsAvailableOffline.add(pageUrl); |
+ } else { |
+ urlsToCheckForOfflinePage.add(pageUrl); |
+ } |
+ } |
+ |
+ final long offlineQueryStartTime = SystemClock.elapsedRealtime(); |
+ |
+ OfflinePageBridge offlinePageBridge = OfflinePageBridge.getForProfile(mProfile); |
+ |
+ // TODO(dewittj): Remove this code by making the NTP badging available after the NTP is |
+ // fully loaded. |
+ if (offlinePageBridge == null || !offlinePageBridge.isOfflinePageModelLoaded()) { |
+ // Posting a task to avoid potential re-entrancy issues. |
+ ThreadUtils.postOnUiThread(new Runnable() { |
+ @Override |
+ public void run() { |
+ callback.onResult(urlsAvailableOffline); |
+ } |
+ }); |
+ return; |
+ } |
+ |
+ offlinePageBridge.checkPagesExistOffline( |
+ urlsToCheckForOfflinePage, new Callback<Set<String>>() { |
+ @Override |
+ public void onResult(Set<String> urlsWithOfflinePages) { |
+ urlsAvailableOffline.addAll(urlsWithOfflinePages); |
+ callback.onResult(urlsAvailableOffline); |
+ RecordHistogram.recordTimesHistogram("NewTabPage.OfflineUrlsLoadTime", |
+ SystemClock.elapsedRealtime() - offlineQueryStartTime, |
+ TimeUnit.MILLISECONDS); |
+ } |
+ }); |
+ } |
+ |
+ @Override |
+ public SuggestionsSource getSuggestionsSource() { |
+ return mSuggestionsSource; |
+ } |
+ |
+ @Override |
+ public void addDestructionObserver(DestructionObserver destructionObserver) { |
+ mDestructionObservers.addObserver(destructionObserver); |
+ } |
+ |
+ public void onDestroy() { |
+ for (DestructionObserver observer : mDestructionObservers) { |
+ observer.onDestroy(); |
+ } |
+ mIsDestroyed = true; |
+ } |
+ |
+ private boolean openRecentTabSnippet(SnippetArticle article) { |
+ TabModel tabModel = mTabModelSelector.getModel(false); |
+ int tabId = Integer.parseInt(article.getRecentTabId()); |
+ int tabIndex = TabModelUtils.getTabIndexById(tabModel, tabId); |
+ if (tabIndex == TabModel.INVALID_TAB_INDEX) return false; |
+ TabModelUtils.setIndex(tabModel, tabIndex); |
+ return true; |
+ } |
+ |
+ private void openUrlInNewWindow(LoadUrlParams loadUrlParams) { |
+ TabDelegate tabDelegate = new TabDelegate(false); |
+ tabDelegate.createTabInOtherWindow(loadUrlParams, mActivity, mTab.getParentId()); |
+ } |
+ |
+ private void openUrlInNewTab(LoadUrlParams loadUrlParams, boolean incognito) { |
+ mTabModelSelector.openNewTab( |
+ loadUrlParams, TabLaunchType.FROM_LONGPRESS_BACKGROUND, mTab, incognito); |
+ } |
+ |
+ private void saveUrlForOffline(String url) { |
+ OfflinePageBridge.getForProfile(mProfile).savePageLater( |
+ url, "ntp_suggestions", true /* userRequested */); |
+ } |
+ |
+ private boolean isNtpOfflinePagesEnabled() { |
+ return ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_OFFLINE_PAGES_FEATURE_NAME); |
+ } |
+ |
+ private boolean isLocalUrl(String url) { |
+ return "file".equals(Uri.parse(url).getScheme()); |
+ } |
+} |