// Copyright 2015 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; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.SystemClock; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import org.chromium.base.ApiCompatibilityUtils; import org.chromium.base.Callback; import org.chromium.base.CommandLine; import org.chromium.base.Log; import org.chromium.base.ThreadUtils; import org.chromium.base.VisibleForTesting; import org.chromium.base.metrics.RecordHistogram; import org.chromium.base.metrics.RecordUserAction; import org.chromium.chrome.R; import org.chromium.chrome.browser.ChromeActivity; import org.chromium.chrome.browser.ChromeFeatureList; import org.chromium.chrome.browser.ChromeSwitches; import org.chromium.chrome.browser.NativePage; import org.chromium.chrome.browser.UrlConstants; import org.chromium.chrome.browser.bookmarks.BookmarkUtils; import org.chromium.chrome.browser.compositor.layouts.content.InvalidationAwareThumbnailProvider; 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.metrics.StartupMetrics; import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; import org.chromium.chrome.browser.ntp.LogoBridge.Logo; import org.chromium.chrome.browser.ntp.LogoBridge.LogoObserver; import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager; 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.SnippetsConfig; import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource; import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; import org.chromium.chrome.browser.preferences.PrefServiceBridge; import org.chromium.chrome.browser.profiles.MostVisitedSites; import org.chromium.chrome.browser.profiles.MostVisitedSites.MostVisitedURLsObserver; import org.chromium.chrome.browser.profiles.Profile; import org.chromium.chrome.browser.search_engines.TemplateUrlService; import org.chromium.chrome.browser.search_engines.TemplateUrlService.TemplateUrlServiceObserver; import org.chromium.chrome.browser.signin.SigninManager; import org.chromium.chrome.browser.signin.SigninManager.SignInStateObserver; import org.chromium.chrome.browser.snackbar.Snackbar; import org.chromium.chrome.browser.snackbar.SnackbarManager.SnackbarController; import org.chromium.chrome.browser.sync.SyncSessionsMetrics; import org.chromium.chrome.browser.tab.EmptyTabObserver; import org.chromium.chrome.browser.tab.Tab; import org.chromium.chrome.browser.tab.TabObserver; 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.chrome.browser.util.UrlUtilities; import org.chromium.content_public.browser.LoadUrlParams; import org.chromium.content_public.browser.NavigationController; import org.chromium.content_public.browser.NavigationEntry; import org.chromium.content_public.common.Referrer; import org.chromium.net.NetworkChangeNotifier; import org.chromium.ui.base.DeviceFormFactor; 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; import jp.tomorrowkey.android.gifplayer.BaseGifImage; /** * Provides functionality when the user interacts with the NTP. */ public class NewTabPage implements NativePage, InvalidationAwareThumbnailProvider, TemplateUrlServiceObserver { private static final String TAG = "NewTabPage"; // MostVisitedItem Context menu item IDs. static final int ID_OPEN_IN_NEW_WINDOW = 0; static final int ID_OPEN_IN_NEW_TAB = 1; static final int ID_OPEN_IN_INCOGNITO_TAB = 2; static final int ID_REMOVE = 3; // UMA enum constants. CTA means the "click-to-action" icon. private static final String LOGO_SHOWN_UMA_NAME = "NewTabPage.LogoShown"; private static final int STATIC_LOGO_SHOWN = 0; private static final int CTA_IMAGE_SHOWN = 1; private static final String LOGO_CLICK_UMA_NAME = "NewTabPage.LogoClick"; private static final int STATIC_LOGO_CLICKED = 0; private static final int CTA_IMAGE_CLICKED = 1; private static final int ANIMATED_LOGO_CLICKED = 2; // Key for the scroll position data that may be stored in a navigation entry. private static final String NAVIGATION_ENTRY_SCROLL_POSITION_KEY = "NewTabPageScrollPosition"; private static final String CHROME_CONTENT_SUGGESTIONS_REFERRER = "https://www.googleapis.com/auth/chrome-content-suggestions"; private static MostVisitedSites sMostVisitedSitesForTests; private final Tab mTab; private final TabModelSelector mTabModelSelector; private final ChromeActivity mActivity; private final Profile mProfile; private final String mTitle; private final int mBackgroundColor; private final int mThemeColor; private final NewTabPageView mNewTabPageView; private TabObserver mTabObserver; private MostVisitedSites mMostVisitedSites; private SnackbarController mMostVisitedItemRemovedController; private FaviconHelper mFaviconHelper; private LargeIconBridge mLargeIconBridge; private LogoBridge mLogoBridge; private boolean mSearchProviderHasLogo; private String mOnLogoClickUrl; private String mAnimatedLogoUrl; private FakeboxDelegate mFakeboxDelegate; private SnippetsBridge mSnippetsBridge; // The timestamp at which the constructor was called. private final long mConstructedTimeNs; // The timestamp at which this NTP was last shown to the user. private long mLastShownTimeNs; private boolean mIsLoaded; // Whether destroy() has been called. private boolean mIsDestroyed; /** Used by {@link #mNewTabPageManager}. Observer tracked for de-registration purposes. */ private SignInStateObserver mSignInStateObserver; /** * Allows clients to listen for updates to the scroll changes of the search box on the * NTP. */ public interface OnSearchBoxScrollListener { /** * Callback to be notified when the scroll position of the search box on the NTP has * changed. A scroll percentage of 0, means the search box has no scroll applied and * is in it's natural resting position. A value of 1 means the search box is scrolled * entirely to the top of the screen viewport. * * @param scrollPercentage The percentage the search box has been scrolled off the page. */ void onNtpScrollChanged(float scrollPercentage); } /** * Handles user interaction with the fakebox (the URL bar in the NTP). */ public interface FakeboxDelegate { /** * Shows the voice recognition dialog. Called when the user taps the microphone icon. */ void startVoiceRecognition(); /** * @return Whether voice search is currently enabled. */ boolean isVoiceSearchEnabled(); /** * @return Whether the URL bar is currently focused. */ boolean isUrlBarFocused(); /** * Focuses the URL bar when the user taps the fakebox, types in the fakebox, or pastes text * into the fakebox. * * @param pastedText The text that was pasted or typed into the fakebox, or null if the user * just tapped the fakebox. */ void requestUrlFocusFromFakebox(String pastedText); /** * @return whether the provided native page is the one currently displayed to the user. */ boolean isCurrentPage(NativePage nativePage); } /** * @param url The URL to check whether it is for the NTP. * @return Whether the passed in URL is used to render the NTP. */ public static boolean isNTPUrl(String url) { // Also handle the legacy chrome://newtab URL since that will redirect to // chrome-native://newtab natively. return url != null && (url.startsWith(UrlConstants.NTP_URL) || url.startsWith("chrome://newtab")); } @VisibleForTesting static void setMostVisitedSitesForTests(MostVisitedSites mostVisitedSitesForTests) { sMostVisitedSitesForTests = mostVisitedSitesForTests; } private final NewTabPageManager mNewTabPageManager = new NewTabPageManager() { private static final String NTP_OFFLINE_PAGES_FEATURE_NAME = "NTPOfflinePages"; private boolean isNtpOfflinePagesEnabled() { return ChromeFeatureList.isEnabled(NTP_OFFLINE_PAGES_FEATURE_NAME); } @Override public boolean isLocationBarShownInNTP() { if (mIsDestroyed) return false; Context context = mNewTabPageView.getContext(); return isInSingleUrlBarMode(context) && !mNewTabPageView.urlFocusAnimationsDisabled(); } @Override public boolean isVoiceSearchEnabled() { return mFakeboxDelegate != null && mFakeboxDelegate.isVoiceSearchEnabled(); } @Override public boolean isFakeOmniboxTextEnabledTablet() { return ChromeFeatureList.isEnabled(ChromeFeatureList.NTP_FAKE_OMNIBOX_TEXT); } private void recordOpenedMostVisitedItem(MostVisitedItem item) { if (mIsDestroyed) return; NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_MOST_VISITED_ENTRY); NewTabPageUma.recordExplicitUserNavigation( item.getUrl(), NewTabPageUma.RAPPOR_ACTION_VISITED_SUGGESTED_TILE); RecordHistogram.recordMediumTimesHistogram("NewTabPage.MostVisitedTime", System.nanoTime() - mLastShownTimeNs, TimeUnit.NANOSECONDS); mMostVisitedSites.recordOpenedMostVisitedItem( item.getIndex(), item.getTileType(), item.getSource()); } @Override public void openMostVisitedItem(MostVisitedItem item) { if (mIsDestroyed) return; recordOpenedMostVisitedItem(item); String url = item.getUrl(); if (!switchToExistingTab(url)) { openUrlMostVisited(WindowOpenDisposition.CURRENT_TAB, url); } } @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)); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) private boolean switchToExistingTab(String url) { String matchPattern = CommandLine.getInstance().getSwitchValue( ChromeSwitches.NTP_SWITCH_TO_EXISTING_TAB); boolean matchByHost; if ("url".equals(matchPattern)) { matchByHost = false; } else if ("host".equals(matchPattern)) { matchByHost = true; } else { return false; } TabModel tabModel = mTabModelSelector.getModel(false); for (int i = tabModel.getCount() - 1; i >= 0; --i) { if (matchURLs(tabModel.getTabAt(i).getUrl(), url, matchByHost)) { TabModelUtils.setIndex(tabModel, i); return true; } } return false; } private boolean matchURLs(String url1, String url2, boolean matchByHost) { if (url1 == null || url2 == null) return false; return matchByHost ? UrlUtilities.sameHost(url1, url2) : url1.equals(url2); } @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); } @Override public void openSnippet(int windowOpenDisposition, SnippetArticle article) { mSnippetsBridge.onSuggestionOpened(article, windowOpenDisposition); NewTabPageUma.monitorContentSuggestionVisit(mTab, article.mCategory); LoadUrlParams 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); } // TODO(mastiz): Merge with openMostVisitedItem(). private void openUrlMostVisited(int windowOpenDisposition, String url) { openUrl(windowOpenDisposition, new LoadUrlParams(url, PageTransition.AUTO_BOOKMARK)); } private 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 onCreateContextMenu(ContextMenu menu, OnMenuItemClickListener listener) { if (mIsDestroyed) return; if (isOpenInNewWindowEnabled()) { menu.add(Menu.NONE, ID_OPEN_IN_NEW_WINDOW, Menu.NONE, R.string.contextmenu_open_in_other_window) .setOnMenuItemClickListener(listener); } menu.add(Menu.NONE, ID_OPEN_IN_NEW_TAB, Menu.NONE, R.string.contextmenu_open_in_new_tab) .setOnMenuItemClickListener(listener); if (isOpenInIncognitoEnabled()) { menu.add(Menu.NONE, ID_OPEN_IN_INCOGNITO_TAB, Menu.NONE, R.string.contextmenu_open_in_incognito_tab).setOnMenuItemClickListener( listener); } menu.add(Menu.NONE, ID_REMOVE, Menu.NONE, R.string.remove) .setOnMenuItemClickListener(listener); } @Override public boolean onMenuItemClick(int menuId, MostVisitedItem item) { if (mIsDestroyed) return false; switch (menuId) { case ID_OPEN_IN_NEW_WINDOW: // TODO(treib): Should we call recordOpenedMostVisitedItem here? openUrlMostVisited(WindowOpenDisposition.NEW_WINDOW, item.getUrl()); return true; case ID_OPEN_IN_NEW_TAB: recordOpenedMostVisitedItem(item); openUrlMostVisited(WindowOpenDisposition.NEW_FOREGROUND_TAB, item.getUrl()); return true; case ID_OPEN_IN_INCOGNITO_TAB: recordOpenedMostVisitedItem(item); openUrlMostVisited(WindowOpenDisposition.OFF_THE_RECORD, item.getUrl()); return true; case ID_REMOVE: mMostVisitedSites.addBlacklistedUrl(item.getUrl()); showMostVisitedItemRemovedSnackbar(item.getUrl()); return true; default: return false; } } @Override public boolean isOpenInNewWindowEnabled() { return MultiWindowUtils.getInstance().isOpenInOtherWindowSupported(mActivity); } @Override public boolean isOpenInIncognitoEnabled() { return PrefServiceBridge.getInstance().isIncognitoModeEnabled(); } 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 */); } @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 focusSearchBox(boolean beginVoiceSearch, String pastedText) { if (mIsDestroyed) return; if (mFakeboxDelegate != null) { if (beginVoiceSearch) { mFakeboxDelegate.startVoiceRecognition(); } else { mFakeboxDelegate.requestUrlFocusFromFakebox(pastedText); } } } @Override public void setMostVisitedURLsObserver(MostVisitedURLsObserver observer, int numResults) { if (mIsDestroyed) return; mMostVisitedSites.setMostVisitedURLsObserver(observer, numResults); } @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); } private boolean isLocalUrl(String url) { return "file".equals(Uri.parse(url).getScheme()); } @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 void onLogoClicked(boolean isAnimatedLogoShowing) { if (mIsDestroyed) return; if (!isAnimatedLogoShowing && mAnimatedLogoUrl != null) { RecordHistogram.recordSparseSlowlyHistogram(LOGO_CLICK_UMA_NAME, CTA_IMAGE_CLICKED); mNewTabPageView.showLogoLoadingView(); mLogoBridge.getAnimatedLogo(new LogoBridge.AnimatedLogoCallback() { @Override public void onAnimatedLogoAvailable(BaseGifImage animatedLogoImage) { if (mIsDestroyed) return; mNewTabPageView.playAnimatedLogo(animatedLogoImage); } }, mAnimatedLogoUrl); } else if (mOnLogoClickUrl != null) { RecordHistogram.recordSparseSlowlyHistogram(LOGO_CLICK_UMA_NAME, isAnimatedLogoShowing ? ANIMATED_LOGO_CLICKED : STATIC_LOGO_CLICKED); mTab.loadUrl(new LoadUrlParams(mOnLogoClickUrl, PageTransition.LINK)); } } @Override public void getSearchProviderLogo(final LogoObserver logoObserver) { if (mIsDestroyed) return; LogoObserver wrapperCallback = new LogoObserver() { @Override public void onLogoAvailable(Logo logo, boolean fromCache) { if (mIsDestroyed) return; mOnLogoClickUrl = logo != null ? logo.onClickUrl : null; mAnimatedLogoUrl = logo != null ? logo.animatedLogoUrl : null; if (logo != null) { RecordHistogram.recordSparseSlowlyHistogram(LOGO_SHOWN_UMA_NAME, logo.animatedLogoUrl == null ? STATIC_LOGO_SHOWN : CTA_IMAGE_SHOWN); } logoObserver.onLogoAvailable(logo, fromCache); } }; mLogoBridge.getCurrentLogo(wrapperCallback); } @Override public void onLoadingComplete(MostVisitedItem[] items) { if (mIsDestroyed) return; long loadTimeMs = (System.nanoTime() - mConstructedTimeNs) / 1000000; RecordHistogram.recordTimesHistogram( "Tab.NewTabOnload", loadTimeMs, TimeUnit.MILLISECONDS); mIsLoaded = true; StartupMetrics.getInstance().recordOpenedNTP(); NewTabPageUma.recordNTPImpression(NewTabPageUma.NTP_IMPRESSION_REGULAR); // If not visible when loading completes, wait until onShown is received. if (!mTab.isHidden()) recordNTPShown(); int tileTypes[] = new int[items.length]; int sources[] = new int[items.length]; for (int i = 0; i < items.length; i++) { tileTypes[i] = items[i].getTileType(); sources[i] = items[i].getSource(); } mMostVisitedSites.recordTileTypeMetrics(tileTypes, sources); if (isNtpOfflinePagesEnabled()) { final int maxNumTiles = 12; for (int i = 0; i < items.length; i++) { if (items[i].isOfflineAvailable()) { RecordHistogram.recordEnumeratedHistogram( "NewTabPage.TileOfflineAvailable", i, maxNumTiles); } } } SyncSessionsMetrics.recordYoungestForeignTabAgeOnNTP(); } @Override public void addContextMenuCloseCallback(Callback<Menu> callback) { mActivity.addContextMenuCloseCallback(callback); } @Override public void removeContextMenuCloseCallback(Callback<Menu> callback) { mActivity.removeContextMenuCloseCallback(callback); } @Override public void closeContextMenu() { mActivity.closeContextMenu(); } @Override public SuggestionsSource getSuggestionsSource() { return mSnippetsBridge; } @Override public void registerSignInStateObserver(SignInStateObserver signInStateObserver) { if (mIsDestroyed) return; assert mSignInStateObserver == null; mSignInStateObserver = signInStateObserver; SigninManager.get(mActivity).addSignInStateObserver(mSignInStateObserver); } @Override public boolean isCurrentPage() { if (mIsDestroyed) return false; if (mFakeboxDelegate == null) return false; return mFakeboxDelegate.isCurrentPage(NewTabPage.this); } }; /** * Constructs a NewTabPage. * @param activity The activity used for context to create the new tab page's View. * @param tab The Tab that is showing this new tab page. * @param tabModelSelector The TabModelSelector used to open tabs. */ public NewTabPage(ChromeActivity activity, Tab tab, TabModelSelector tabModelSelector) { mConstructedTimeNs = System.nanoTime(); mTab = tab; mActivity = activity; mTabModelSelector = tabModelSelector; mProfile = tab.getProfile(); mTitle = activity.getResources().getString(R.string.button_new_tab); mBackgroundColor = NtpStyleUtils.getBackgroundColorResource(activity.getResources(), false); mThemeColor = ApiCompatibilityUtils.getColor( activity.getResources(), R.color.default_primary_color); TemplateUrlService.getInstance().addObserver(this); mTabObserver = new EmptyTabObserver() { @Override public void onShown(Tab tab) { // Showing the NTP is only meaningful when the page has been loaded already. if (mIsLoaded) recordNTPShown(); } @Override public void onHidden(Tab tab) { if (mIsLoaded) recordNTPInteractionTime(); } @Override public void onPageLoadStarted(Tab tab, String url) { int scrollPosition = mNewTabPageView.getScrollPosition(); if (scrollPosition == RecyclerView.NO_POSITION) return; if (mTab.getWebContents() == null) return; NavigationController controller = mTab.getWebContents().getNavigationController(); int index = controller.getLastCommittedEntryIndex(); NavigationEntry entry = controller.getEntryAtIndex(index); if (entry == null) return; // At least under test conditions this method may be called initially for the load // of the NTP itself, at which point the last committed entry is not for the NTP // yet. This method will then be called a second time when the user navigates away, // at which point the last committed entry is for the NTP. The extra data must only // be set in the latter case. if (!isNTPUrl(entry.getUrl())) return; controller.setEntryExtraData(index, NAVIGATION_ENTRY_SCROLL_POSITION_KEY, Integer.toString(scrollPosition)); } }; mTab.addObserver(mTabObserver); mMostVisitedSites = buildMostVisitedSites(mProfile); mLogoBridge = new LogoBridge(mProfile); updateSearchProviderHasLogo(); if (SnippetsConfig.isEnabled()) { mSnippetsBridge = new SnippetsBridge(mProfile); } LayoutInflater inflater = LayoutInflater.from(activity); mNewTabPageView = (NewTabPageView) inflater.inflate(R.layout.new_tab_page_view, null); mNewTabPageView.initialize( mNewTabPageManager, mSearchProviderHasLogo, getScrollPositionFromNavigationEntry()); RecordHistogram.recordBooleanHistogram( "NewTabPage.MobileIsUserOnline", NetworkChangeNotifier.isOnline()); } private static MostVisitedSites buildMostVisitedSites(Profile profile) { if (sMostVisitedSitesForTests != null) { return sMostVisitedSitesForTests; } else { return new MostVisitedSites(profile); } } private void showMostVisitedItemRemovedSnackbar(String url) { if (mMostVisitedItemRemovedController == null) { mMostVisitedItemRemovedController = new SnackbarController() { @Override public void onDismissNoAction(Object actionData) {} /** Undoes the most visited item removal. */ @Override public void onAction(Object actionData) { if (mIsDestroyed) return; String url = (String) actionData; mMostVisitedSites.removeBlacklistedUrl(url); } }; } Context context = mNewTabPageView.getContext(); Snackbar snackbar = Snackbar .make(context.getString(R.string.most_visited_item_removed), mMostVisitedItemRemovedController, Snackbar.TYPE_ACTION, Snackbar.UMA_NTP_MOST_VISITED_DELETE_UNDO) .setAction(context.getString(R.string.undo), url); mTab.getSnackbarManager().showSnackbar(snackbar); } /** @return The view container for the new tab page. */ @VisibleForTesting NewTabPageView getNewTabPageView() { return mNewTabPageView; } /** @return whether the NTP is using the cards UI. */ public boolean isCardsUiEnabled() { return SnippetsConfig.isEnabled(); } /** * Updates whether the NewTabPage should animate on URL focus changes. * @param disable Whether to disable the animations. */ public void setUrlFocusAnimationsDisabled(boolean disable) { mNewTabPageView.setUrlFocusAnimationsDisabled(disable); } private boolean isInSingleUrlBarMode(Context context) { if (DeviceFormFactor.isTablet(context)) return false; return mSearchProviderHasLogo; } private void updateSearchProviderHasLogo() { mSearchProviderHasLogo = TemplateUrlService.getInstance().isDefaultSearchEngineGoogle(); } private void onSearchEngineUpdated() { // TODO(newt): update this if other search providers provide logos. updateSearchProviderHasLogo(); mNewTabPageView.setSearchProviderHasLogo(mSearchProviderHasLogo); } /** * Specifies the percentage the URL is focused during an animation. 1.0 specifies that the URL * bar has focus and has completed the focus animation. 0 is when the URL bar is does not have * any focus. * * @param percent The percentage of the URL bar focus animation. */ public void setUrlFocusChangeAnimationPercent(float percent) { mNewTabPageView.setUrlFocusChangeAnimationPercent(percent); } /** * Get the bounds of the search box in relation to the top level NewTabPage view. * * @param bounds The current drawing location of the search box. * @param translation The translation applied to the search box by the parent view hierarchy up * to the NewTabPage view. */ public void getSearchBoxBounds(Rect bounds, Point translation) { mNewTabPageView.getSearchBoxBounds(bounds, translation); } /** * Updates the opacity of the search box when scrolling. * * @param alpha opacity (alpha) value to use. */ public void setSearchBoxAlpha(float alpha) { mNewTabPageView.setSearchBoxAlpha(alpha); } /** * Updates the opacity of the search provider logo when scrolling. * * @param alpha opacity (alpha) value to use. */ public void setSearchProviderLogoAlpha(float alpha) { mNewTabPageView.setSearchProviderLogoAlpha(alpha); } /** * @return Whether the location bar is shown in the NTP. */ public boolean isLocationBarShownInNTP() { return mNewTabPageManager.isLocationBarShownInNTP(); } /** * Sets the listener for search box scroll changes. * @param listener The listener to be notified on changes. */ public void setSearchBoxScrollListener(OnSearchBoxScrollListener listener) { mNewTabPageView.setSearchBoxScrollListener(listener); } /** * Sets the FakeboxDelegate that this pages interacts with. */ public void setFakeboxDelegate(FakeboxDelegate fakeboxDelegate) { mFakeboxDelegate = fakeboxDelegate; if (mFakeboxDelegate != null) { mNewTabPageView.updateVoiceSearchButtonVisibility(); // The toolbar can't get the reference to the native page until its initialization is // finished, so we can't cache it here and transfer it to the view later. We pull that // state from the location bar when we get a reference to it as a workaround. if (fakeboxDelegate.isUrlBarFocused()) { mNewTabPageView.setUrlFocusChangeAnimationPercent(1f); } } } /** * Records UMA for the NTP being shown. This includes a fresh page load or being brought to the * foreground. */ private void recordNTPShown() { mLastShownTimeNs = System.nanoTime(); RecordUserAction.record("MobileNTPShown"); } private void recordNTPInteractionTime() { RecordHistogram.recordMediumTimesHistogram( "NewTabPage.TimeSpent", System.nanoTime() - mLastShownTimeNs, TimeUnit.NANOSECONDS); } /** * Returns the value of the adapter scroll position that was stored in the last committed * navigation entry. Returns {@code RecyclerView.NO_POSITION} if there is no last committed * navigation entry, or if no data is found. * @return The adapter scroll position. */ private int getScrollPositionFromNavigationEntry() { if (mTab.getWebContents() == null) return RecyclerView.NO_POSITION; NavigationController controller = mTab.getWebContents().getNavigationController(); int index = controller.getLastCommittedEntryIndex(); String scrollPositionData = controller.getEntryExtraData(index, NAVIGATION_ENTRY_SCROLL_POSITION_KEY); if (TextUtils.isEmpty(scrollPositionData)) return RecyclerView.NO_POSITION; try { return Integer.parseInt(scrollPositionData); } catch (NumberFormatException e) { Log.w(TAG, "Bad data found for scroll position: %s", scrollPositionData, e); return RecyclerView.NO_POSITION; } } /** * @return Whether the NTP has finished loaded. */ @VisibleForTesting public boolean isLoadedForTests() { return mIsLoaded; } // TemplateUrlServiceObserver overrides @Override public void onTemplateURLServiceChanged() { onSearchEngineUpdated(); } // NativePage overrides @Override public void destroy() { assert !mIsDestroyed; assert !ViewCompat .isAttachedToWindow(getView()) : "Destroy called before removed from window"; if (mIsLoaded && !mTab.isHidden()) recordNTPInteractionTime(); if (mFaviconHelper != null) { mFaviconHelper.destroy(); mFaviconHelper = null; } if (mLargeIconBridge != null) { mLargeIconBridge.destroy(); mLargeIconBridge = null; } if (mMostVisitedSites != null) { mMostVisitedSites.destroy(); mMostVisitedSites = null; } if (mLogoBridge != null) { mLogoBridge.destroy(); mLogoBridge = null; } if (mSnippetsBridge != null) { mSnippetsBridge.destroy(); mSnippetsBridge = null; } if (mMostVisitedItemRemovedController != null) { mTab.getSnackbarManager().dismissSnackbars(mMostVisitedItemRemovedController); } if (mSignInStateObserver != null) { SigninManager.get(mActivity).removeSignInStateObserver(mSignInStateObserver); } TemplateUrlService.getInstance().removeObserver(this); mTab.removeObserver(mTabObserver); mTabObserver = null; mIsDestroyed = true; } @Override public String getUrl() { return UrlConstants.NTP_URL; } @Override public String getTitle() { return mTitle; } @Override public int getBackgroundColor() { return mBackgroundColor; } @Override public int getThemeColor() { return isLocationBarShownInNTP() ? NtpStyleUtils.getBackgroundColorResource(mActivity.getResources(), false) : mThemeColor; } @Override public boolean needsToolbarShadow() { return !mSearchProviderHasLogo; } @Override public View getView() { return mNewTabPageView; } @Override public String getHost() { return UrlConstants.NTP_HOST; } @Override public void updateForUrl(String url) { } // InvalidationAwareThumbnailProvider @Override public boolean shouldCaptureThumbnail() { return mNewTabPageView.shouldCaptureThumbnail(); } @Override public void captureThumbnail(Canvas canvas) { mNewTabPageView.captureThumbnail(canvas); } }