// Copyright 2015 The Chromium Authors
// 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.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.chromium.base.CallbackController;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.TimeUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.jank_tracker.JankScenario;
import org.chromium.base.jank_tracker.JankTracker;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.app.feed.FeedActionDelegateImpl;
import org.chromium.chrome.browser.bookmarks.BookmarkModel;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.feature_engagement.TrackerFactory;
import org.chromium.chrome.browser.feed.FeedActionDelegate;
import org.chromium.chrome.browser.feed.FeedReliabilityLogger;
import org.chromium.chrome.browser.feed.FeedSurfaceCoordinator;
import org.chromium.chrome.browser.feed.FeedSurfaceDelegate;
import org.chromium.chrome.browser.feed.FeedSurfaceLifecycleManager;
import org.chromium.chrome.browser.feed.FeedSurfaceProvider;
import org.chromium.chrome.browser.feed.FeedSwipeRefreshLayout;
import org.chromium.chrome.browser.feed.NtpFeedSurfaceLifecycleManager;
import org.chromium.chrome.browser.feed.componentinterfaces.SurfaceCoordinator;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.LifecycleObserver;
import org.chromium.chrome.browser.lifecycle.PauseResumeWithNativeObserver;
import org.chromium.chrome.browser.magic_stack.HomeModulesConfigManager;
import org.chromium.chrome.browser.magic_stack.HomeModulesCoordinator;
import org.chromium.chrome.browser.magic_stack.HomeModulesMetricsUtils;
import org.chromium.chrome.browser.magic_stack.ModuleDelegateHost;
import org.chromium.chrome.browser.magic_stack.ModuleRegistry;
import org.chromium.chrome.browser.native_page.ContextMenuManager;
import org.chromium.chrome.browser.omnibox.OmniboxFocusReason;
import org.chromium.chrome.browser.omnibox.OmniboxStub;
import org.chromium.chrome.browser.omnibox.voice.VoiceRecognitionHandler;
import org.chromium.chrome.browser.privacy.settings.PrivacyPreferencesManagerImpl;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
import org.chromium.chrome.browser.search_resumption.SearchResumptionModuleCoordinator;
import org.chromium.chrome.browser.search_resumption.SearchResumptionModuleUtils;
import org.chromium.chrome.browser.share.ShareDelegate;
import org.chromium.chrome.browser.single_tab.SingleTabSwitcherCoordinator;
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.Tile;
import org.chromium.chrome.browser.suggestions.tile.TileGroup;
import org.chromium.chrome.browser.suggestions.tile.TileGroupDelegateImpl;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.InvalidationAwareThumbnailProvider;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabClosureParams;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.HomeSurfaceTracker;
import org.chromium.chrome.browser.tasks.ReturnToChromeUtil;
import org.chromium.chrome.browser.tasks.tab_management.TabGroupCreationDialogManager;
import org.chromium.chrome.browser.toolbar.top.Toolbar;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.chrome.browser.ui.native_page.BasicSmoothTransitionDelegate;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.chrome.browser.ui.native_page.NativePageHost;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.components.browser_ui.styles.SemanticColorUtils;
import org.chromium.components.browser_ui.widget.displaystyle.UiConfig;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.feature_engagement.EventConstants;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.components.search_engines.TemplateUrlService;
import org.chromium.components.search_engines.TemplateUrlService.TemplateUrlServiceObserver;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.ModalDialogManager;
import org.chromium.url.GURL;
import java.util.List;
/** Provides functionality when the user interacts with the NTP. */
public class NewTabPage
implements NativePage,
InvalidationAwareThumbnailProvider,
TemplateUrlServiceObserver,
BrowserControlsStateProvider.Observer,
FeedSurfaceDelegate,
VoiceRecognitionHandler.Observer,
ModuleDelegateHost {
private static final String TAG = "NewTabPage";
// Key for the scroll position data that may be stored in a navigation entry.
public static final String CONTEXT_MENU_USER_ACTION_PREFIX = "Suggestions";
protected final Tab mTab;
private final Supplier<Tab> mActivityTabProvider;
private final ActivityLifecycleDispatcher mActivityLifecycleDispatcher;
private final String mTitle;
private final JankTracker mJankTracker;
private final Context mContext;
private final int mBackgroundColor;
protected final NewTabPageManagerImpl mNewTabPageManager;
protected final TileGroup.Delegate mTileGroupDelegate;
private final boolean mIsTablet;
private final BrowserControlsStateProvider mBrowserControlsStateProvider;
private final ContextMenuManager mContextMenuManager;
private final ObserverList<MostVisitedTileClickObserver> mMostVisitedTileClickObservers;
private final BottomSheetController mBottomSheetController;
private FeedSurfaceProvider mFeedSurfaceProvider;
private NewTabPageLayout mNewTabPageLayout;
private TabObserver mTabObserver;
private LifecycleObserver mLifecycleObserver;
protected boolean mSearchProviderHasLogo;
protected OmniboxStub mOmniboxStub;
private VoiceRecognitionHandler mVoiceRecognitionHandler;
// The timestamp at which the constructor was called.
protected 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;
private final int mToolbarHeight;
private final Supplier<Toolbar> mToolbarSupplier;
private final TabModelSelector mTabModelSelector;
private final TemplateUrlService mTemplateUrlService;
private final ObservableSupplier<TabContentManager> mTabContentManagerSupplier;
private final ObservableSupplier<Integer> mTabStripHeightSupplier;
private SingleTabSwitcherCoordinator mSingleTabSwitcherCoordinator;
private ViewGroup mSingleTabCardContainer;
@Nullable private HomeModulesCoordinator mHomeModulesCoordinator;
@Nullable private ViewGroup mHomeModulesContainer;
private ObservableSupplierImpl<Tab> mMostRecentTabSupplier = new ObservableSupplierImpl<>();
@Nullable private Point mContextMenuStartPosition;
private final Activity mActivity;
@Nullable private final HomeSurfaceTracker mHomeSurfaceTracker;
private boolean mSnapshotSingleTabCardChanged;
private final boolean mIsInNightMode;
@Nullable private final OneshotSupplier<ModuleRegistry> mModuleRegistrySupplier;
@Nullable private SearchResumptionModuleCoordinator mSearchResumptionModuleCoordinator;
private SmoothTransitionDelegate mSmoothTransitionDelegate;
private CallbackController mCallbackController = new CallbackController();
@Override
public void onControlsOffsetChanged(
int topOffset,
int topControlsMinHeightOffset,
int bottomOffset,
int bottomControlsMinHeightOffset,
boolean needsAnimate,
boolean isVisibilityForced) {
updateMargins();
}
@Override
public void onBottomControlsHeightChanged(
int bottomControlsHeight, int bottomControlsMinHeight) {
updateMargins();
}
/**
* 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);
}
/** An observer for most visited tile clicks. */
public interface MostVisitedTileClickObserver {
/**
* Called when a most visited tile is clicked.
* @param tile The most visited tile that was clicked.
* @param tab The tab hosting the most visited tile section.
*/
void onMostVisitedTileClicked(Tile tile, Tab tab);
}
protected class NewTabPageManagerImpl extends SuggestionsUiDelegateImpl
implements NewTabPageManager {
private final Tracker mTracker;
public NewTabPageManagerImpl(
SuggestionsNavigationDelegate navigationDelegate,
Profile profile,
NativePageHost nativePageHost,
SnackbarManager snackbarManager) {
super(navigationDelegate, profile, nativePageHost, snackbarManager);
mTracker = TrackerFactory.getTrackerForProfile(profile);
}
@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;
FeedReliabilityLogger feedReliabilityLogger =
mFeedSurfaceProvider.getReliabilityLogger();
if (mVoiceRecognitionHandler != null && beginVoiceSearch) {
if (feedReliabilityLogger != null) {
feedReliabilityLogger.onVoiceSearch();
}
mVoiceRecognitionHandler.startVoiceRecognition(
VoiceRecognitionHandler.VoiceInteractionSource.NTP);
mTracker.notifyEvent(EventConstants.NTP_VOICE_SEARCH_BUTTON_CLICKED);
} else if (mOmniboxStub != null) {
if (feedReliabilityLogger != null) {
feedReliabilityLogger.onOmniboxFocused();
}
mOmniboxStub.setUrlBarFocus(
true,
pastedText,
pastedText == null
? OmniboxFocusReason.FAKE_BOX_TAP
: OmniboxFocusReason.FAKE_BOX_LONG_PRESS);
}
}
@Override
public boolean isCurrentPage() {
if (mIsDestroyed) return false;
if (mOmniboxStub == null) return false;
return getNewTabPageForCurrentTab() == NewTabPage.this;
}
private NewTabPage getNewTabPageForCurrentTab() {
Tab currentTab = mActivityTabProvider.get();
NativePage nativePage = currentTab != null ? currentTab.getNativePage() : null;
return (nativePage instanceof NewTabPage) ? (NewTabPage) nativePage : null;
}
@Override
public void onLoadingComplete() {
if (mIsDestroyed) return;
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(
Context context,
Profile profile,
SuggestionsNavigationDelegate navigationDelegate,
SnackbarManager snackbarManager) {
super(context, profile, navigationDelegate, snackbarManager);
}
@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);
for (MostVisitedTileClickObserver observer : mMostVisitedTileClickObservers) {
observer.onMostVisitedTileClicked(tile, mTab);
}
}
}
/**
* Constructs a NewTabPage.
*
* @param activity The activity used for context to create the new tab page's View.
* @param browserControlsStateProvider {@link BrowserControlsStateProvider} to observe for
* offset changes.
* @param activityTabProvider Provides the current active tab.
* @param modalDialogManager {@link ModalDialogManager} for the app.
* @param snackbarManager {@link SnackbarManager} object.
* @param lifecycleDispatcher Activity lifecycle dispatcher.
* @param tabModelSelector {@link TabModelSelector} object.
* @param isTablet {@code true} if running on a Tablet device.
* @param uma {@link NewTabPageUma} object recording user metrics.
* @param isInNightMode {@code true} if the night mode setting is on.
* @param nativePageHost The host that is showing this new tab page.
* @param tab The {@link Tab} that contains this new tab page.
* @param url The URL that launched this new tab page.
* @param bottomSheetController The controller for bottom sheets, used by the feed.
* @param shareDelegateSupplier Supplies the Delegate used to open SharingHub.
* @param windowAndroid The containing window of this page.
* @param jankTracker {@link JankTracker} object to measure jankiness while NTP is visible.
* @param toolbarSupplier Supplies the {@link Toolbar}.
* @param homeSurfaceTracker Used to decide whether we are the home surface.
* @param tabContentManagerSupplier Used to create tab thumbnails.
* @param tabStripHeightSupplier Supplier for the tab strip height.
* @param moduleRegistrySupplier Supplier for the {@link ModuleRegistry}.
*/
public NewTabPage(
Activity activity,
BrowserControlsStateProvider browserControlsStateProvider,
Supplier<Tab> activityTabProvider,
ModalDialogManager modalDialogManager,
SnackbarManager snackbarManager,
ActivityLifecycleDispatcher lifecycleDispatcher,
TabModelSelector tabModelSelector,
boolean isTablet,
NewTabPageUma uma,
boolean isInNightMode,
NativePageHost nativePageHost,
Tab tab,
String url,
BottomSheetController bottomSheetController,
Supplier<ShareDelegate> shareDelegateSupplier,
WindowAndroid windowAndroid,
JankTracker jankTracker,
Supplier<Toolbar> toolbarSupplier,
HomeSurfaceTracker homeSurfaceTracker,
ObservableSupplier<TabContentManager> tabContentManagerSupplier,
ObservableSupplier<Integer> tabStripHeightSupplier,
OneshotSupplier<ModuleRegistry> moduleRegistrySupplier) {
mConstructedTimeNs = System.nanoTime();
TraceEvent.begin(TAG);
mActivity = activity;
mActivityTabProvider = activityTabProvider;
mActivityLifecycleDispatcher = lifecycleDispatcher;
mTab = tab;
mJankTracker = jankTracker;
mToolbarSupplier = toolbarSupplier;
mMostVisitedTileClickObservers = new ObserverList<>();
mBrowserControlsStateProvider = browserControlsStateProvider;
mTabModelSelector = tabModelSelector;
mBottomSheetController = bottomSheetController;
mHomeSurfaceTracker = homeSurfaceTracker;
mTabContentManagerSupplier = tabContentManagerSupplier;
mIsInNightMode = isInNightMode;
mTabStripHeightSupplier = tabStripHeightSupplier;
mModuleRegistrySupplier = moduleRegistrySupplier;
Profile profile = mTab.getProfile();
var tabGroupCreationDialogManager =
new TabGroupCreationDialogManager(
mActivity, modalDialogManager, /* onTabGroupCreation= */ null);
SuggestionsNavigationDelegate navigationDelegate =
new SuggestionsNavigationDelegate(
activity,
profile,
nativePageHost,
tabModelSelector,
tabGroupCreationDialogManager,
mTab);
mNewTabPageManager =
new NewTabPageManagerImpl(
navigationDelegate, profile, nativePageHost, snackbarManager);
mTileGroupDelegate =
new NewTabPageTileGroupDelegate(
activity, profile, navigationDelegate, snackbarManager);
mContext = activity;
mTitle = activity.getResources().getString(R.string.new_tab_title);
mBackgroundColor =
ChromeColors.getSurfaceColor(
mContext, R.dimen.home_surface_background_color_elevation);
mIsTablet = isTablet;
mTemplateUrlService = TemplateUrlServiceFactory.getForProfile(profile);
mTemplateUrlService.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.onSwitchToForeground();
}
@Override
public void onHidden(Tab tab, @TabHidingType int type) {
if (mIsLoaded) recordNtpHidden();
if (mSingleTabSwitcherCoordinator != null
&& (mHomeSurfaceTracker == null
|| !mHomeSurfaceTracker.canShowHomeSurface(mTab))) {
mSingleTabSwitcherCoordinator.hide();
}
}
@Override
public void onInteractabilityChanged(Tab tab, boolean isInteractable) {
// We start/stop tracking based on InteractabilityChanged in addition to
// Shown/Hidden because those events don't trigger for switching to tab
// switcher, we don't rely solely on this event because it doesn't
// trigger when the user navigates to a website.
if (isInteractable) {
mJankTracker.startTrackingScenario(JankScenario.NEW_TAB_PAGE);
} else {
mJankTracker.finishTrackingScenario(JankScenario.NEW_TAB_PAGE);
}
}
};
mTab.addObserver(mTabObserver);
mLifecycleObserver =
new PauseResumeWithNativeObserver() {
@Override
public void onResumeWithNative() {}
@Override
public void onPauseWithNative() {
// Only record when this tab is the current tab.
if (mActivityTabProvider.get() == mTab) {
RecordUserAction.record("MobileNTPPaused");
}
}
};
mActivityLifecycleDispatcher.register(mLifecycleObserver);
updateSearchProviderHasLogo();
initializeMainView(
activity,
windowAndroid,
snackbarManager,
uma,
isInNightMode,
shareDelegateSupplier,
url);
// It is possible that the NewTabPage is created when the Tab model hasn't been initialized.
// For example, the user changes theme when a NTP is showing, which leads to the recreation
// of the ChromeTabbedActivity and showing the NTP as the last visited Tab.
TabModelUtils.runOnTabStateInitialized(
mTabModelSelector,
mCallbackController.makeCancelable(
unusedTabModelSelector -> mayCreateSearchResumptionModule(profile)));
getView()
.addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View view) {
updateMargins();
getView().removeOnAttachStateChangeListener(this);
}
@Override
public void onViewDetachedFromWindow(View view) {}
});
mBrowserControlsStateProvider.addObserver(this);
mToolbarHeight =
activity.getResources().getDimensionPixelSize(R.dimen.toolbar_height_no_shadow);
uma.recordContentSuggestionsDisplayStatus(profile);
// TODO(twellington): Move this somewhere it can be shared with NewTabPageView?
Runnable closeContextMenuCallback = activity::closeContextMenu;
mContextMenuManager =
new ContextMenuManager(
mNewTabPageManager.getNavigationDelegate(),
mFeedSurfaceProvider.getTouchEnabledDelegate(),
closeContextMenuCallback,
NewTabPage.CONTEXT_MENU_USER_ACTION_PREFIX);
windowAndroid.addContextMenuCloseListener(mContextMenuManager);
mNewTabPageLayout.initialize(
mNewTabPageManager,
activity,
mTileGroupDelegate,
mSearchProviderHasLogo,
mTemplateUrlService.isDefaultSearchEngineGoogle(),
mFeedSurfaceProvider.getScrollDelegate(),
mFeedSurfaceProvider.getTouchEnabledDelegate(),
mFeedSurfaceProvider.getUiConfig(),
lifecycleDispatcher,
uma,
mTab.getProfile(),
windowAndroid,
mIsTablet,
mTabStripHeightSupplier);
initializeHomeModules();
TraceEvent.end(TAG);
}
/**
* Create and initialize the main view contained in this NewTabPage.
*
* @param activity The activity used to initialize the view.
* @param windowAndroid Provides the current active tab.
* @param snackbarManager {@link SnackbarManager} object.
* @param uma {@link NewTabPageUma} object recording user metrics.
* @param isInNightMode {@code true} if the night mode setting is on.
* @param shareDelegateSupplier Supplies a delegate used to open SharingHub.
*/
protected void initializeMainView(
Activity activity,
WindowAndroid windowAndroid,
SnackbarManager snackbarManager,
NewTabPageUma uma,
boolean isInNightMode,
Supplier<ShareDelegate> shareDelegateSupplier,
String url) {
Profile profile = mTab.getProfile();
LayoutInflater inflater = LayoutInflater.from(activity);
// TODO(crbug.com/347509698): Remove the log statements after fixing the bug.
Log.i(TAG, "NewTabPageLayout inflate");
mNewTabPageLayout = (NewTabPageLayout) inflater.inflate(R.layout.new_tab_page_layout, null);
FeedActionDelegate actionDelegate =
new FeedActionDelegateImpl(
activity,
snackbarManager,
mNewTabPageManager.getNavigationDelegate(),
BookmarkModel.getForProfile(profile),
mTabModelSelector,
profile,
mBottomSheetController) {
@Override
public void openHelpPage() {
NewTabPageUma.recordAction(NewTabPageUma.ACTION_CLICKED_LEARN_MORE);
super.openHelpPage();
}
};
FeedSurfaceCoordinator feedSurfaceCoordinator =
new FeedSurfaceCoordinator(
activity,
snackbarManager,
windowAndroid,
mJankTracker,
new SnapScrollHelperImpl(mNewTabPageManager, mNewTabPageLayout),
mNewTabPageLayout,
mBrowserControlsStateProvider.getTopControlsHeight(),
isInNightMode,
this,
profile,
mBottomSheetController,
shareDelegateSupplier,
/* externalScrollableContainerDelegate= */ null,
NewTabPageUtils.decodeOriginFromNtpUrl(url),
PrivacyPreferencesManagerImpl.getInstance(),
mToolbarSupplier,
mConstructedTimeNs,
FeedSwipeRefreshLayout.create(activity, R.id.toolbar_container),
/* overScrollDisabled= */ false,
/* viewportView= */ null,
actionDelegate,
mTabStripHeightSupplier);
mFeedSurfaceProvider = feedSurfaceCoordinator;
}
/** Initialize the single tab card on home surface NTP or magic stack. */
private void initializeHomeModules() {
boolean isTrackingTabReady =
mHomeSurfaceTracker != null && mHomeSurfaceTracker.isHomeSurfaceTab(mTab);
// The magic stack is shown on every NTP. There are three cases:
// 1) on any normal NewTabPage. Initialize the magic stack here.
// 2) The home surface NewTabPage which is created via back operations. Initialize the
// magic stack here, and re-show the single Tab card with the previously tracked Tab.
// 3) The home surface NewTabPage which is created at startup. The magic stack will be
// initialized later since its tracking Tab hasn't been available yet.
// The launch type of a home surface NTP is TabLaunchType.FROM_STARTUP.
if (HomeModulesMetricsUtils.useMagicStack()) {
mContextMenuStartPosition =
ReturnToChromeUtil.calculateContextMenuStartPosition(mActivity.getResources());
if (isTrackingTabReady) {
// Case 2) on home surface NTP via back operations.
showMagicStack(mHomeSurfaceTracker.getLastActiveTabToTrack());
} else if (mTab.getLaunchType() != TabLaunchType.FROM_STARTUP) {
// Case 1) on normal NTP.
showMagicStack(null);
}
} else if (isTrackingTabReady) { // On NTP home surface with magic stack disabled.
showHomeSurfaceUi(mHomeSurfaceTracker.getLastActiveTabToTrack());
}
if (isTrackingTabReady) {
ReturnToChromeUtil.recordHomeSurfaceShown();
}
}
/**
* @param isTablet Whether the activity is running in tablet mode.
* @param searchProviderHasLogo Whether the default search engine has logo.
* @return Whether the NTP is in single url bar mode, i.e. the url bar is shown in-line on the
* NTP.
*/
public static boolean isInSingleUrlBarMode(boolean isTablet, boolean searchProviderHasLogo) {
return !isTablet && searchProviderHasLogo;
}
/**
* Update the margins for the content when browser controls constraints or bottom control
* height are changed.
*/
private void updateMargins() {
// TODO(mdjones): can this be merged with BasicNativePage's updateMargins?
View view = getView();
ViewGroup.MarginLayoutParams layoutParams =
((ViewGroup.MarginLayoutParams) view.getLayoutParams());
if (layoutParams == null) return;
// Negative |topControlsDistanceToRest| means the controls Y position is above the rest
// position and the controls height is increasing with animation, while positive
// |topControlsDistanceToRest| means the controls Y position is below the rest position and
// the controls height is decreasing with animation. |getToolbarExtraYOffset()| returns
// the margin when the controls are at rest, so |getToolbarExtraYOffset()
// + topControlsDistanceToRest| will give the margin for the current animation frame.
final int topControlsDistanceToRest =
mBrowserControlsStateProvider.getContentOffset()
- mBrowserControlsStateProvider.getTopControlsHeight();
final int topMargin = getToolbarExtraYOffset() + topControlsDistanceToRest;
final int bottomMargin =
mBrowserControlsStateProvider.getBottomControlsHeight()
- mBrowserControlsStateProvider.getBottomControlOffset();
if (topMargin != layoutParams.topMargin || bottomMargin != layoutParams.bottomMargin) {
layoutParams.topMargin = topMargin;
layoutParams.bottomMargin = bottomMargin;
view.setLayoutParams(layoutParams);
}
}
// TODO(sinansahin): This is the same as {@link ToolbarManager#getToolbarExtraYOffset}. So, we
// should look into sharing the logic.
/**
* @return The height that is included in the top controls but not in the toolbar or the tab
* strip.
*/
private int getToolbarExtraYOffset() {
return mBrowserControlsStateProvider.getTopControlsHeight()
- mToolbarHeight
- mTabStripHeightSupplier.get();
}
/**
* @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 isInSingleUrlBarMode(mIsTablet, mSearchProviderHasLogo);
}
private void updateSearchProviderHasLogo() {
mSearchProviderHasLogo = mTemplateUrlService.doesDefaultSearchEngineHaveLogo();
}
private void onSearchEngineUpdated() {
updateSearchProviderHasLogo();
setSearchProviderInfoOnView(
mSearchProviderHasLogo, mTemplateUrlService.isDefaultSearchEngineGoogle());
// TODO(crbug.com/40226731): Remove this call when the Feed position experiment is
// cleaned up.
updateMargins();
}
/**
* 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();
}
/** @see org.chromium.chrome.browser.omnibox.NewTabPageDelegate#hasCompletedFirstLayout(). */
public boolean hasCompletedFirstLayout() {
return mNewTabPageLayout.getHeight() > 0;
}
/**
* @return Whether the location bar has been scrolled to top in the NTP.
*/
public boolean isLocationBarScrolledToTopInNtp() {
return mNewTabPageLayout.getToolbarTransitionPercentage() == 1;
}
/**
* 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 OmniboxStub that this page interacts with. */
public void setOmniboxStub(OmniboxStub omniboxStub) {
mOmniboxStub = omniboxStub;
if (mOmniboxStub != 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(
omniboxStub.isUrlBarFocused() ? 1f : 0f);
FeedReliabilityLogger feedReliabilityLogger =
mFeedSurfaceProvider.getReliabilityLogger();
if (feedReliabilityLogger != null) {
mOmniboxStub.addUrlFocusChangeListener(feedReliabilityLogger);
}
}
mVoiceRecognitionHandler = mOmniboxStub.getVoiceRecognitionHandler();
if (mVoiceRecognitionHandler != null) {
mVoiceRecognitionHandler.addObserver(this);
mNewTabPageLayout.updateActionButtonVisibility();
}
}
@Override
public void notifyHidingWithBack() {
FeedReliabilityLogger feedReliabilityLogger = mFeedSurfaceProvider.getReliabilityLogger();
if (feedReliabilityLogger != null) {
feedReliabilityLogger.onNavigateBack();
}
}
@Override
public void onVoiceAvailabilityImpacted() {
mNewTabPageLayout.updateActionButtonVisibility();
}
/** Adds an observer to be notified on most visited tile clicks. */
public void addMostVisitedTileClickObserver(MostVisitedTileClickObserver observer) {
mMostVisitedTileClickObservers.addObserver(observer);
}
/** Removes the observer. */
public void removeMostVisitedTileClickObserver(MostVisitedTileClickObserver observer) {
mMostVisitedTileClickObservers.removeObserver(observer);
}
/**
* 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");
mJankTracker.startTrackingScenario(JankScenario.NEW_TAB_PAGE);
SuggestionsMetrics.recordSurfaceVisible();
}
/** Records UMA for the NTP being hidden and the time spent on it. */
private void recordNtpHidden() {
mJankTracker.finishTrackingScenario(JankScenario.NEW_TAB_PAGE);
RecordHistogram.recordMediumTimesHistogram(
"NewTabPage.TimeSpent",
(System.nanoTime() - mLastShownTimeNs) / TimeUtils.NANOSECONDS_PER_MILLISECOND);
SuggestionsMetrics.recordSurfaceHidden();
}
/**
* Returns an arbitrary int value stored in the last committed navigation entry. If some step
* fails then the default is returned instead.
*
* @param key The string previously used to tag this piece of data.
* @param tab A tab that is used to access the NavigationController and the NavigationEntry
* extras.
* @param defaultValue The value to return if lookup or parsing is unsuccessful.
* @return The value for the given key.
* <p>TODO(crbug.com/40618119): Refactor this to be reusable across NativePage components.
*/
private static int getIntFromNavigationEntry(String key, Tab tab, int defaultValue) {
if (tab.getWebContents() == null) return defaultValue;
String stringValue = getStringFromNavigationEntry(tab, key);
if (stringValue == null || stringValue.isEmpty()) {
return RecyclerView.NO_POSITION;
}
try {
return Integer.parseInt(stringValue);
} catch (NumberFormatException e) {
Log.w(TAG, "Bad data found for %s : %s", key, stringValue, e);
return RecyclerView.NO_POSITION;
}
}
/**
* Returns an arbitrary string value stored in the last committed navigation entry. If the look
* up fails, an empty string is returned.
*
* @param tab A tab that is used to access the NavigationController and the NavigationEntry
* extras.
* @param key The string previously used to tag this piece of data.
* @return The value previously stored with the given key.
* <p>TODO(crbug.com/40618119): Refactor this to be reusable across NativePage components.
*/
public static String getStringFromNavigationEntry(Tab tab, String key) {
if (tab.getWebContents() == null) return "";
NavigationController controller = tab.getWebContents().getNavigationController();
int index = controller.getLastCommittedEntryIndex();
return controller.getEntryExtraData(index, key);
}
/**
* @return Whether the NTP has finished loaded.
*/
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();
mCallbackController.destroy();
mNewTabPageManager.onDestroy();
mTileGroupDelegate.destroy();
mTemplateUrlService.removeObserver(this);
mTab.removeObserver(mTabObserver);
mTabObserver = null;
mActivityLifecycleDispatcher.unregister(mLifecycleObserver);
mLifecycleObserver = null;
mBrowserControlsStateProvider.removeObserver(this);
FeedReliabilityLogger feedReliabilityLogger = mFeedSurfaceProvider.getReliabilityLogger();
if (mOmniboxStub != null && feedReliabilityLogger != null) {
mOmniboxStub.removeUrlFocusChangeListener(feedReliabilityLogger);
}
mFeedSurfaceProvider.destroy();
mTab.getWindowAndroid().removeContextMenuCloseListener(mContextMenuManager);
if (mVoiceRecognitionHandler != null) {
mVoiceRecognitionHandler.removeObserver(this);
}
if (mSearchResumptionModuleCoordinator != null) {
mSearchResumptionModuleCoordinator.destroy();
}
if (mSingleTabSwitcherCoordinator != null) {
destroySingleTabCard();
}
if (mHomeModulesCoordinator != null) {
mHomeModulesCoordinator.destroy();
}
mIsDestroyed = true;
}
@Override
public String getUrl() {
return UrlConstants.NTP_URL;
}
@Override
public String getTitle() {
return mTitle;
}
@Override
public int getBackgroundColor() {
return mBackgroundColor;
}
@Override
public @ColorInt int getToolbarTextBoxBackgroundColor(@ColorInt int defaultColor) {
if (isLocationBarShownInNtp()) {
if (!isLocationBarScrolledToTopInNtp()) {
return ChromeColors.getSurfaceColor(
mContext, R.dimen.home_surface_background_color_elevation);
}
if (mIsInNightMode) {
return mContext.getColor(R.color.color_primary_with_alpha_20);
} else {
return SemanticColorUtils.getColorPrimaryContainer(mContext);
}
}
return defaultColor;
}
@Override
public @ColorInt int getToolbarSceneLayerBackground(@ColorInt int defaultColor) {
return isLocationBarShownInNtp() ? getBackgroundColor() : defaultColor;
}
@Override
public boolean needsToolbarShadow() {
return !mSearchProviderHasLogo;
}
@Override
public View getView() {
return mFeedSurfaceProvider.getView();
}
@Override
public String getHost() {
return UrlConstants.NTP_HOST;
}
@Override
public void updateForUrl(String url) {}
@Override
public void reload() {
mFeedSurfaceProvider.reload();
mNewTabPageLayout.reload();
}
// InvalidationAwareThumbnailProvider
@Override
public boolean shouldCaptureThumbnail() {
return mNewTabPageLayout.shouldCaptureThumbnail()
|| mFeedSurfaceProvider.shouldCaptureThumbnail()
|| mSnapshotSingleTabCardChanged;
}
@Override
public void captureThumbnail(Canvas canvas) {
mNewTabPageLayout.onPreCaptureThumbnail();
mFeedSurfaceProvider.captureThumbnail(canvas);
mSnapshotSingleTabCardChanged = false;
}
// Implements FeedSurfaceDelegate
@Override
public FeedSurfaceLifecycleManager createStreamLifecycleManager(
Activity activity, SurfaceCoordinator coordinator, Profile profile) {
return new NtpFeedSurfaceLifecycleManager(
activity, mTab, (FeedSurfaceCoordinator) coordinator);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return !(mTab != null && DeviceFormFactor.isWindowOnTablet(mTab.getWindowAndroid()))
&& (mOmniboxStub != null && mOmniboxStub.isUrlBarFocused());
}
public FeedSurfaceCoordinator getCoordinatorForTesting() {
return (FeedSurfaceCoordinator) mFeedSurfaceProvider;
}
public NewTabPageManager getNewTabPageManagerForTesting() {
return mNewTabPageManager;
}
public TileGroup.Delegate getTileGroupDelegateForTesting() {
return mTileGroupDelegate;
}
public FeedActionDelegate getFeedActionDelegateForTesting() {
return ((FeedSurfaceCoordinator) mFeedSurfaceProvider)
.getActionDelegateForTesting(); // IN-TEST
}
TabObserver getTabObserverForTesting() {
return mTabObserver;
}
private void mayCreateSearchResumptionModule(Profile profile) {
// The module is disabled on tablets.
if (mIsTablet) return;
mSearchResumptionModuleCoordinator =
SearchResumptionModuleUtils.mayCreateSearchResumptionModule(
mNewTabPageLayout,
mTabModelSelector.getCurrentModel(),
mTab,
profile,
R.id.search_resumption_module_container_stub);
}
/**
* Shows the home surface UI on this NTP.
* TODO(crbug.com/40263286): Investigate better solution to show Home surface UI on NTP upon
* creation.
* to show Home surface UI on NTP upon creation.
*/
public void showHomeSurfaceUi(Tab mostRecentTab) {
if (mSingleTabSwitcherCoordinator == null) {
initializeSingleTabCard(mostRecentTab);
} else {
mSingleTabSwitcherCoordinator.show(mostRecentTab);
}
}
/**
* Shows the magic stack on the home surface NTP.
*
* @param mostRecentTab The last shown Tab if exists. It is non null for NTP home surface only.
*/
public void showMagicStack(Tab mostRecentTab) {
if (mModuleRegistrySupplier.get() == null) {
return;
}
if (mostRecentTab != null && !UrlUtilities.isNtpUrl(mostRecentTab.getUrl())) {
mMostRecentTabSupplier.set(mostRecentTab);
}
if (mHomeModulesCoordinator == null) {
initializeMagicStack(mostRecentTab);
}
mHomeModulesCoordinator.show(this::onMagicStackShown);
}
/** Show the module when the current new tab page is been used as the home surface. */
private void initializeSingleTabCard(Tab mostRecentTab) {
if (mostRecentTab == null || UrlUtilities.isNtpUrl(mostRecentTab.getUrl())) {
return;
}
mSingleTabCardContainer =
(FrameLayout)
((ViewStub)
mNewTabPageLayout.findViewById(
R.id.tab_switcher_module_container_stub))
.inflate();
mSingleTabSwitcherCoordinator =
new SingleTabSwitcherCoordinator(
mActivity,
mSingleTabCardContainer,
mTabModelSelector,
mIsTablet,
mostRecentTab,
this::onSingleTabCardClicked,
/* seeMoreLinkClickedCallback= */ null,
() -> mSnapshotSingleTabCardChanged = true,
mTabContentManagerSupplier.get()
/* tabContentManager= */ ,
mIsTablet ? mFeedSurfaceProvider.getUiConfig() : null,
/* moduleDelegate= */ null);
mSingleTabSwitcherCoordinator.showModule();
}
/**
* Initializes the magic stack to show home modules on the current new tab page which is used as
* the home surface.
*/
private void initializeMagicStack(Tab mostRecentTab) {
mHomeModulesContainer =
(ViewGroup)
((ViewStub)
mNewTabPageLayout.findViewById(
R.id.home_modules_recycler_view_stub))
.inflate();
ObservableSupplier<Profile> profileSupplier =
new ObservableSupplierImpl<>(mTab.getProfile());
mHomeModulesCoordinator =
new HomeModulesCoordinator(
mActivity,
this,
mNewTabPageLayout,
HomeModulesConfigManager.getInstance(),
profileSupplier,
mModuleRegistrySupplier.get());
}
private void onMagicStackShown(boolean isVisible) {
mHomeModulesContainer.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
private void onSingleTabCardClicked(int tabId) {
onTabClicked(tabId);
}
/**
* Opens the selected Tab and closes the current NTP. If the single Tab card which tracks the
* last active Tab is selected, updates the mHomeSurfaceTracker too.
*/
private void onTabClicked(int tabId) {
TabModelUtils.selectTabById(mTabModelSelector, tabId, TabSelectionType.FROM_USER);
mTabModelSelector
.getModel(false)
.closeTabs(TabClosureParams.closeTab(mTab).allowUndo(false).build());
if (mHomeSurfaceTracker != null) {
// Updates the mHomeSurfaceTracker since the Tab of the NTP is closed.
mHomeSurfaceTracker.updateHomeSurfaceAndTrackingTabs(null, null);
}
}
public boolean isSingleTabCardVisibleForTesting() {
if (mSingleTabSwitcherCoordinator == null) return false;
return mSingleTabSwitcherCoordinator.isVisible();
}
public boolean isMagicStackVisibleForTesting() {
if (mHomeModulesContainer == null) return false;
return mHomeModulesContainer.getVisibility() == View.VISIBLE;
}
/* Destroy the single tab card on the {@link NewTabPageLayout}. */
@VisibleForTesting
void destroySingleTabCard() {
mSingleTabCardContainer.removeAllViews();
mSingleTabSwitcherCoordinator.hide();
mSingleTabSwitcherCoordinator.destroy();
mSingleTabSwitcherCoordinator = null;
}
public boolean getSnapshotSingleTabCardChangedForTesting() {
return mSnapshotSingleTabCardChanged;
}
@Override
public Point getContextMenuStartPoint() {
return mContextMenuStartPosition;
}
@Override
public UiConfig getUiConfig() {
return mIsTablet ? mFeedSurfaceProvider.getUiConfig() : null;
}
@Override
public void onUrlClicked(GURL gurl) {
mTab.loadUrl(new LoadUrlParams(gurl));
}
@Override
public void onTabSelected(int tabId) {
onTabClicked(tabId);
}
@Override
public void onCaptureThumbnailStatusChanged() {
mSnapshotSingleTabCardChanged = true;
}
@Override
public void customizeSettings() {
HomeModulesConfigManager.getInstance().onMenuClick(mContext);
}
@Override
public int getStartMargin() {
boolean isInNarrowWindowOnTablet =
mIsTablet
&& NewTabPageLayout.isInNarrowWindowOnTablet(
mIsTablet, mFeedSurfaceProvider.getUiConfig());
int marginResourceId =
isInNarrowWindowOnTablet
? R.dimen.ntp_search_box_lateral_margin_narrow_window_tablet
: R.dimen.mvt_container_lateral_margin;
return mContext.getResources().getDimensionPixelSize(marginResourceId);
}
@Nullable
@Override
public Tab getTrackingTab() {
if (!mMostRecentTabSupplier.hasValue()) {
return null;
}
return mMostRecentTabSupplier.get();
}
@Override
public boolean isHomeSurface() {
// Can only show a local tab to resume if we we have a tracked tab. The presence of the
// local tab to resume module is effectively what being a home surface is.
return mMostRecentTabSupplier.hasValue();
}
@Override
public SmoothTransitionDelegate enableSmoothTransition() {
if (mSmoothTransitionDelegate == null) {
mSmoothTransitionDelegate = new BasicSmoothTransitionDelegate(getView());
}
return mSmoothTransitionDelegate;
}
public SmoothTransitionDelegate getSmoothTransitionDelegateForTesting() {
return mSmoothTransitionDelegate;
}
}