blob: 1ccaa6b444923e9ba150de1c812eb6cf85ca08bb [file] [log] [blame]
// 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.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.DiscardableReferencePool;
import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
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.UrlConstants;
import org.chromium.chrome.browser.compositor.layouts.content.InvalidationAwareThumbnailProvider;
import org.chromium.chrome.browser.download.DownloadManagerService;
import org.chromium.chrome.browser.native_page.NativePage;
import org.chromium.chrome.browser.native_page.NativePageHost;
import org.chromium.chrome.browser.ntp.NewTabPageView.NewTabPageManager;
import org.chromium.chrome.browser.ntp.snippets.SuggestionsSource;
import org.chromium.chrome.browser.omnibox.LocationBarVoiceRecognitionHandler;
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.snackbar.SnackbarManager;
import org.chromium.chrome.browser.suggestions.SuggestionsDependencyFactory;
import org.chromium.chrome.browser.suggestions.SuggestionsEventReporter;
import org.chromium.chrome.browser.suggestions.SuggestionsMetrics;
import org.chromium.chrome.browser.suggestions.SuggestionsNavigationDelegate;
import org.chromium.chrome.browser.suggestions.SuggestionsUiDelegateImpl;
import org.chromium.chrome.browser.suggestions.Tile;
import org.chromium.chrome.browser.suggestions.TileGroup;
import org.chromium.chrome.browser.suggestions.TileGroupDelegateImpl;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.Tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tabmodel.TabModel.TabSelectionType;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.util.ColorUtils;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.vr.VrModuleProvider;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationEntry;
import org.chromium.net.NetworkChangeNotifier;
import org.chromium.ui.mojom.WindowOpenDisposition;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Provides functionality when the user interacts with the NTP.
*/
public class NewTabPage
implements NativePage, InvalidationAwareThumbnailProvider, TemplateUrlServiceObserver {
private static final String TAG = "NewTabPage";
// Key for the scroll position data that may be stored in a navigation entry.
private static final String NAVIGATION_ENTRY_SCROLL_POSITION_KEY = "NewTabPageScrollPosition";
public static final String CONTEXT_MENU_USER_ACTION_PREFIX = "Suggestions";
protected final Tab mTab;
private final String mTitle;
private final int mBackgroundColor;
private final int mThemeColor;
protected final NewTabPageManagerImpl mNewTabPageManager;
protected final TileGroup.Delegate mTileGroupDelegate;
private final boolean mIsTablet;
/**
* The {@link NewTabPageView} shown in this NewTabPageLayout. This may be null in sub-classes.
*/
private @Nullable NewTabPageView mNewTabPageView;
protected NewTabPageLayout mNewTabPageLayout;
private TabObserver mTabObserver;
protected boolean mSearchProviderHasLogo;
protected FakeboxDelegate mFakeboxDelegate;
private LocationBarVoiceRecognitionHandler mVoiceRecognitionHandler;
// 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;
/**
* 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 {
/**
* @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 and about:newtab URLs since they will redirect to
// chrome-native://newtab natively.
if (url == null) return false;
try {
// URL().getProtocol() throws MalformedURLException if the scheme is "invalid",
// including common ones like "about:", so it's not usable for isInternalScheme().
URI uri = new URI(url);
if (!UrlUtilities.isInternalScheme(uri)) return false;
String host = uri.getHost();
if (host == null) {
// "about:newtab" would lead to null host.
uri = new URI(uri.getScheme() + "://" + uri.getSchemeSpecificPart());
host = uri.getHost();
}
return UrlConstants.NTP_HOST.equals(host);
} catch (URISyntaxException e) {
return false;
}
}
protected class NewTabPageManagerImpl
extends SuggestionsUiDelegateImpl implements NewTabPageManager {
public NewTabPageManagerImpl(SuggestionsSource suggestionsSource,
SuggestionsEventReporter eventReporter,
SuggestionsNavigationDelegate navigationDelegate, Profile profile,
NativePageHost nativePageHost, DiscardableReferencePool referencePool,
SnackbarManager snackbarManager) {
super(suggestionsSource, eventReporter, navigationDelegate, profile, nativePageHost,
referencePool, snackbarManager);
}
@Override
public boolean isLocationBarShownInNTP() {
if (mIsDestroyed) return false;
return isInSingleUrlBarMode() && !mNewTabPageLayout.urlFocusAnimationsDisabled();
}
@Override
public boolean isVoiceSearchEnabled() {
return mVoiceRecognitionHandler != null
&& mVoiceRecognitionHandler.isVoiceSearchEnabled();
}
@Override
public void focusSearchBox(boolean beginVoiceSearch, String pastedText) {
if (mIsDestroyed) return;
if (VrModuleProvider.getDelegate().isInVr()) return;
if (mVoiceRecognitionHandler != null && beginVoiceSearch) {
mVoiceRecognitionHandler.startVoiceRecognition(
LocationBarVoiceRecognitionHandler.VoiceInteractionSource.NTP);
} else if (mFakeboxDelegate != null) {
mFakeboxDelegate.requestUrlFocusFromFakebox(pastedText);
}
}
@Override
public boolean isCurrentPage() {
if (mIsDestroyed) return false;
if (mFakeboxDelegate == null) return false;
return mFakeboxDelegate.isCurrentPage(NewTabPage.this);
}
@Override
public void onLoadingComplete() {
if (mIsDestroyed) return;
long loadTimeMs = (System.nanoTime() - mConstructedTimeNs) / 1000000;
RecordHistogram.recordTimesHistogram(
"Tab.NewTabOnload", loadTimeMs, TimeUnit.MILLISECONDS);
mIsLoaded = true;
NewTabPageUma.recordNTPImpression(NewTabPageUma.NTP_IMPRESSION_REGULAR);
// If not visible when loading completes, wait until onShown is received.
if (!mTab.isHidden()) recordNTPShown();
}
}
/**
* Extends {@link TileGroupDelegateImpl} to add metrics logging that is specific to
* {@link NewTabPage}.
*/
private class NewTabPageTileGroupDelegate extends TileGroupDelegateImpl {
private NewTabPageTileGroupDelegate(ChromeActivity activity, Profile profile,
SuggestionsNavigationDelegate navigationDelegate) {
super(activity, profile, navigationDelegate, activity.getSnackbarManager());
}
@Override
public void onLoadingComplete(List<Tile> tiles) {
if (mIsDestroyed) return;
super.onLoadingComplete(tiles);
mNewTabPageLayout.onTilesLoaded();
}
@Override
public void openMostVisitedItem(int windowDisposition, Tile tile) {
if (mIsDestroyed) return;
super.openMostVisitedItem(windowDisposition, tile);
if (windowDisposition != WindowOpenDisposition.NEW_WINDOW) {
RecordHistogram.recordMediumTimesHistogram("NewTabPage.MostVisitedTime",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mLastShownTimeNs),
TimeUnit.MILLISECONDS);
}
}
}
/**
* Constructs a NewTabPage.
* @param activity The activity used for context to create the new tab page's View.
* @param nativePageHost The host that is showing this new tab page.
* @param tabModelSelector The TabModelSelector used to open tabs.
*/
public NewTabPage(ChromeActivity activity, NativePageHost nativePageHost,
TabModelSelector tabModelSelector) {
mConstructedTimeNs = System.nanoTime();
TraceEvent.begin(TAG);
mTab = nativePageHost.getActiveTab();
Profile profile = mTab.getProfile();
SuggestionsDependencyFactory depsFactory = SuggestionsDependencyFactory.getInstance();
SuggestionsSource suggestionsSource = depsFactory.createSuggestionSource(profile);
SuggestionsEventReporter eventReporter = depsFactory.createEventReporter();
SuggestionsNavigationDelegate navigationDelegate = new SuggestionsNavigationDelegate(
activity, profile, nativePageHost, tabModelSelector);
mNewTabPageManager = new NewTabPageManagerImpl(suggestionsSource, eventReporter,
navigationDelegate, profile, nativePageHost,
activity.getChromeApplication().getReferencePool(), activity.getSnackbarManager());
mTileGroupDelegate = new NewTabPageTileGroupDelegate(activity, profile, navigationDelegate);
mTitle = activity.getResources().getString(R.string.button_new_tab);
mBackgroundColor = ApiCompatibilityUtils.getColor(
activity.getResources(), R.color.modern_primary_color);
mThemeColor = ColorUtils.getDefaultThemeColor(activity.getResources(), false);
mIsTablet = activity.isTablet();
TemplateUrlService.getInstance().addObserver(this);
mTabObserver = new EmptyTabObserver() {
@Override
public void onShown(Tab tab, @TabSelectionType int type) {
// Showing the NTP is only meaningful when the page has been loaded already.
if (mIsLoaded) recordNTPShown();
mNewTabPageLayout.getTileGroup().onSwitchToForeground(/* trackLoadTask = */ false);
}
@Override
public void onHidden(Tab tab, @TabHidingType int type) {
if (mIsLoaded) recordNTPHidden();
}
@Override
public void onPageLoadStarted(Tab tab, String url) {
saveLastScrollPosition();
}
};
mTab.addObserver(mTabObserver);
updateSearchProviderHasLogo();
initializeMainView(activity);
eventReporter.onSurfaceOpened();
DownloadManagerService.getDownloadManagerService().checkForExternallyRemovedDownloads(
/*isOffTheRecord=*/false);
RecordHistogram.recordBooleanHistogram(
"NewTabPage.MobileIsUserOnline", NetworkChangeNotifier.isOnline());
NewTabPageUma.recordLoadType(activity);
TraceEvent.end(TAG);
}
/**
* Create and initialize the main view contained in this NewTabPage.
* @param context The context used to inflate the view.
*/
protected void initializeMainView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
mNewTabPageView = (NewTabPageView) inflater.inflate(R.layout.new_tab_page_view, null);
mNewTabPageLayout = mNewTabPageView.getNewTabPageLayout();
mNewTabPageView.initialize(mNewTabPageManager, mTab, mTileGroupDelegate,
mSearchProviderHasLogo,
TemplateUrlService.getInstance().isDefaultSearchEngineGoogle(),
getScrollPositionFromNavigationEntry());
}
/**
* Save the last scroll position stored in the navigation entry if necessary.
*/
protected void saveLastScrollPosition() {
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));
}
/** @return The view container for the new tab page. */
@VisibleForTesting
public NewTabPageView getNewTabPageView() {
return mNewTabPageView;
}
/** @return The view container for the new tab layout. */
@VisibleForTesting
public NewTabPageLayout getNewTabPageLayout() {
return mNewTabPageLayout;
}
/**
* Updates whether the NewTabPage should animate on URL focus changes.
* @param disable Whether to disable the animations.
*/
public void setUrlFocusAnimationsDisabled(boolean disable) {
mNewTabPageLayout.setUrlFocusAnimationsDisabled(disable);
}
private boolean isInSingleUrlBarMode() {
return !mIsTablet && mSearchProviderHasLogo;
}
private void updateSearchProviderHasLogo() {
mSearchProviderHasLogo = TemplateUrlService.getInstance().doesDefaultSearchEngineHaveLogo();
}
private void onSearchEngineUpdated() {
updateSearchProviderHasLogo();
setSearchProviderInfoOnView(mSearchProviderHasLogo,
TemplateUrlService.getInstance().isDefaultSearchEngineGoogle());
mNewTabPageLayout.loadSearchProviderLogo();
}
/**
* Set the search provider info on the main child view, so that it can change layouts if
* needed.
* @param hasLogo Whether the search provider has a logo.
* @param isGoogle Whether the search provider is Google.
*/
private void setSearchProviderInfoOnView(boolean hasLogo, boolean isGoogle) {
mNewTabPageLayout.setSearchProviderInfo(hasLogo, isGoogle);
}
/**
* 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) {
mNewTabPageLayout.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) {
mNewTabPageLayout.getSearchBoxBounds(bounds, translation, getView());
}
/**
* Updates the opacity of the search box when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchBoxAlpha(float alpha) {
mNewTabPageLayout.setSearchBoxAlpha(alpha);
}
/**
* Updates the opacity of the search provider logo when scrolling.
*
* @param alpha opacity (alpha) value to use.
*/
public void setSearchProviderLogoAlpha(float alpha) {
mNewTabPageLayout.setSearchProviderLogoAlpha(alpha);
}
/**
* Set the search box background drawable.
*
* @param drawable The search box background.
*/
public void setSearchBoxBackground(Drawable drawable) {
mNewTabPageLayout.setSearchBoxBackground(drawable);
}
/**
* @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) {
mNewTabPageLayout.setSearchBoxScrollListener(listener);
}
/**
* Sets the FakeboxDelegate that this pages interacts with.
*/
public void setFakeboxDelegate(FakeboxDelegate fakeboxDelegate) {
mFakeboxDelegate = fakeboxDelegate;
if (mNewTabPageView != null) mNewTabPageView.setFakeboxDelegate(fakeboxDelegate);
if (mFakeboxDelegate != null) {
// 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.
mNewTabPageLayout.setUrlFocusChangeAnimationPercent(
fakeboxDelegate.isUrlBarFocused() ? 1f : 0f);
}
}
/**
* Sets the {@link LocationBarVoiceRecognitionHandler} this page interacts with.
*/
public void setVoiceRecognitionHandler(
LocationBarVoiceRecognitionHandler voiceRecognitionHandler) {
mVoiceRecognitionHandler = voiceRecognitionHandler;
if (mVoiceRecognitionHandler != null) {
mNewTabPageLayout.updateVoiceSearchButtonVisibility();
}
}
/**
* 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");
SuggestionsMetrics.recordSurfaceVisible();
}
/** Records UMA for the NTP being hidden and the time spent on it. */
private void recordNTPHidden() {
RecordHistogram.recordMediumTimesHistogram("NewTabPage.TimeSpent",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - mLastShownTimeNs),
TimeUnit.MILLISECONDS);
SuggestionsMetrics.recordSurfaceHidden();
}
/**
* 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()) recordNTPHidden();
mNewTabPageManager.onDestroy();
mTileGroupDelegate.destroy();
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() ? mBackgroundColor : 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);
}
@VisibleForTesting
public NewTabPageManager getManagerForTesting() {
return mNewTabPageManager;
}
}