blob: f97259875998b30d6ffc0873d7976abffb3512cc [file] [log] [blame]
// Copyright 2019 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.tasks.tab_management;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.ANIMATE_VISIBILITY_CHANGES;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BOTTOM_CONTROLS_HEIGHT;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.BOTTOM_PADDING;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.INITIAL_SCROLL_INDEX;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_INCOGNITO;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.IS_VISIBLE;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.SHADOW_TOP_OFFSET;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.TOP_MARGIN;
import static org.chromium.chrome.browser.tasks.tab_management.TabListContainerProperties.VISIBILITY_LISTENER;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.SystemClock;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.Log;
import org.chromium.base.ObserverList;
import org.chromium.base.StrictModeContext;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.compositor.layouts.content.TabContentManager;
import org.chromium.chrome.browser.flags.CachedFeatureFlags;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.init.FirstDrawDetector;
import org.chromium.chrome.browser.multiwindow.MultiWindowModeStateDispatcher;
import org.chromium.chrome.browser.preferences.ChromePreferenceKeys;
import org.chromium.chrome.browser.preferences.SharedPreferencesManager;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData;
import org.chromium.chrome.browser.tabmodel.TabList;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelFilter;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.tasks.ReturnToChromeExperimentsUtil;
import org.chromium.chrome.browser.tasks.pseudotab.PseudoTab;
import org.chromium.chrome.browser.tasks.tab_groups.TabGroupModelFilter;
import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode;
import org.chromium.chrome.features.start_surface.StartSurfaceUserData;
import org.chromium.chrome.tab_ui.R;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.List;
/**
* The Mediator that is responsible for resetting the tab grid or carousel based on visibility and
* model changes.
*/
class TabSwitcherMediator implements TabSwitcher.Controller, TabListRecyclerView.VisibilityListener,
TabListMediator.GridCardOnClickListenerProvider,
PriceMessageService.PriceWelcomeMessageReviewActionProvider {
private static final String TAG = "TabSwitcherMediator";
// This should be the same as TabListCoordinator.GRID_LAYOUT_SPAN_COUNT for the selected tab
// to be on the 2nd row.
static final int INITIAL_SCROLL_INDEX_OFFSET_GTS = 2;
static final int INITIAL_SCROLL_INDEX_OFFSET_CAROUSEL = 1;
private static final int DEFAULT_TOP_PADDING = 0;
// Count histograms for tab counts when showing switcher.
static final String TAB_COUNT_HISTOGRAM = "Tabs.TabCountInSwitcher";
static final String TAB_ENTRIES_HISTOGRAM = "Tabs.IndependentTabCountInSwitcher";
/** Field trial parameter for the {@link TabListRecyclerView} cleanup delay. */
private static final String SOFT_CLEANUP_DELAY_PARAM = "soft-cleanup-delay";
private static final int DEFAULT_SOFT_CLEANUP_DELAY_MS = 3_000;
private static final String CLEANUP_DELAY_PARAM = "cleanup-delay";
private static final int DEFAULT_CLEANUP_DELAY_MS = 30_000;
private final Handler mHandler;
private final Runnable mSoftClearTabListRunnable;
private final Runnable mClearTabListRunnable;
private final ResetHandler mResetHandler;
private final PropertyModel mContainerViewModel;
private final TabModelSelector mTabModelSelector;
private final TabModelObserver mTabModelObserver;
private final TabModelSelectorObserver mTabModelSelectorObserver;
private final ObserverList<TabSwitcher.OverviewModeObserver> mObservers = new ObserverList<>();
private final BrowserControlsStateProvider mBrowserControlsStateProvider;
private final BrowserControlsStateProvider.Observer mBrowserControlsObserver;
private final ViewGroup mContainerView;
private final TabContentManager mTabContentManager;
private final MultiWindowModeStateDispatcher mMultiWindowModeStateDispatcher;
private final MultiWindowModeStateDispatcher.MultiWindowModeObserver mMultiWindowModeObserver;
private Integer mSoftCleanupDelayMsForTesting;
private Integer mCleanupDelayMsForTesting;
private TabGridDialogMediator.DialogController mTabGridDialogController;
private TabSelectionEditorCoordinator
.TabSelectionEditorController mTabSelectionEditorController;
private TabSwitcher.OnTabSelectingListener mOnTabSelectingListener;
private PriceMessageService mPriceMessageService;
/**
* In cases where a didSelectTab was due to switching models with a toggle,
* we don't change tab grid visibility.
*/
private boolean mShouldIgnoreNextSelect;
private int mModelIndexWhenShown;
private int mTabIdWhenShown;
private int mIndexInNewModelWhenSwitched;
private boolean mIsSelectingInTabSwitcher;
private boolean mShowTabsInMruOrder;
private static class FirstMeaningfulPaintRecorder {
private final long mActivityCreationTimeMs;
private FirstMeaningfulPaintRecorder(long activityCreationTimeMs) {
mActivityCreationTimeMs = activityCreationTimeMs;
}
private void record(int numOfThumbnails) {
assert numOfThumbnails >= 0;
long elapsed = SystemClock.elapsedRealtime() - mActivityCreationTimeMs;
PostTask.postTask(UiThreadTaskTraits.BEST_EFFORT,
()
-> ReturnToChromeExperimentsUtil.recordTimeToGTSFirstMeaningfulPaint(
elapsed, numOfThumbnails));
}
}
private FirstMeaningfulPaintRecorder mFirstMeaningfulPaintRecorder;
private boolean mRegisteredFirstMeaningfulPaintRecorder;
private @TabListCoordinator.TabListMode int mMode;
private Context mContext;
/**
* Interface to delegate resetting the tab grid.
*/
interface ResetHandler {
/**
* Reset the tab grid with the given {@link TabList}, which can be null.
* @param tabList The {@link TabList} to show the tabs for in the grid.
* @param quickMode Whether to skip capturing the selected live tab for the thumbnail.
* @param mruMode Whether order the Tabs by MRU.
* @return Whether the {@link TabListRecyclerView} can be shown quickly.
*/
boolean resetWithTabList(@Nullable TabList tabList, boolean quickMode, boolean mruMode);
/**
* Reset the tab grid with the given {@link List<PseudoTab>}, which can be null.
* @param tabs The {@link List<PseudoTab>} to show the tabs for in the grid.
* @param quickMode Whether to skip capturing the selected live tab for the thumbnail.
* @param mruMode Whether order the Tabs by MRU.
* @return Whether the {@link TabListRecyclerView} can be shown quickly.
*/
boolean resetWithTabs(@Nullable List<PseudoTab> tabs, boolean quickMode, boolean mruMode);
/**
* Release the thumbnail {@link Bitmap} but keep the {@link TabGridView}.
*/
void softCleanup();
}
/**
* Interface to control message items in grid tab switcher.
*/
interface MessageItemsController {
/**
* Remove all the message items in the model list. Right now this is used when all tabs are
* closed in the grid tab switcher.
*/
void removeAllAppendedMessage();
/**
* Restore all the message items that should show. Right now this is only used to restore
* message items when the closure of the last tab in tab switcher is undone.
*/
void restoreAllAppendedMessage();
}
/**
* An interface to control price welcome message in grid tab switcher.
*/
interface PriceWelcomeMessageController {
/**
* Remove the price welcome message item in the model list. Right now this is used when
* its binding tab is closed in the grid tab switcher.
*/
void removePriceWelcomeMessage();
/**
* Restore the price welcome message item that should show. Right now this is only used
* when the closure of the binding tab in tab switcher is undone.
*/
void restorePriceWelcomeMessage();
/**
* Show the price welcome message in tab switcher. This is used when any open tab in tab
* switcher has a price drop.
*/
void showPriceWelcomeMessage(PriceMessageService.PriceTabData priceTabData);
}
/**
* Basic constructor for the Mediator.
* @param context The context to use for accessing {@link android.content.res.Resources}.
* @param resetHandler The {@link ResetHandler} that handles reset for this Mediator.
* @param containerViewModel The {@link PropertyModel} to keep state on the View containing the
* grid or carousel.
* @param tabModelSelector {@link TabModelSelector} to observer for model and selection changes.
* @param browserControlsStateProvider {@link BrowserControlsStateProvider} to use.
* @param containerView The container {@link ViewGroup} to use.
* @param tabContentManager The {@link TabContentManager} for first meaningful paint event.
* @param multiWindowModeStateDispatcher The {@link MultiWindowModeStateDispatcher} to observe
* for multi-window related changes.
* @param mode One of the {@link TabListCoordinator.TabListMode}.
*/
TabSwitcherMediator(Context context, ResetHandler resetHandler,
PropertyModel containerViewModel, TabModelSelector tabModelSelector,
BrowserControlsStateProvider browserControlsStateProvider, ViewGroup containerView,
TabContentManager tabContentManager, MessageItemsController messageItemsController,
PriceWelcomeMessageController priceWelcomeMessageController,
MultiWindowModeStateDispatcher multiWindowModeStateDispatcher,
@TabListCoordinator.TabListMode int mode) {
mResetHandler = resetHandler;
mContainerViewModel = containerViewModel;
mTabModelSelector = tabModelSelector;
mBrowserControlsStateProvider = browserControlsStateProvider;
mMultiWindowModeStateDispatcher = multiWindowModeStateDispatcher;
mMode = mode;
mContext = context;
mTabModelSelectorObserver = new TabModelSelectorObserver() {
@Override
public void onTabModelSelected(TabModel newModel, TabModel oldModel) {
mShouldIgnoreNextSelect = true;
mIndexInNewModelWhenSwitched = newModel.index();
TabList currentTabModelFilter =
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter();
mContainerViewModel.set(IS_INCOGNITO, currentTabModelFilter.isIncognito());
if (mTabGridDialogController != null) {
mTabGridDialogController.hideDialog(false);
}
if (!mContainerViewModel.get(IS_VISIBLE)) return;
mResetHandler.resetWithTabList(currentTabModelFilter, false, mShowTabsInMruOrder);
// Hide the currently selected tab when moving to an empty incognito model.
// TODO(crbug.com/1082612): If the need arises, generalize this solution in the
// IncognitoTabModel.
if (newModel.isIncognito() && newModel.getCount() == 0 && oldModel.getCount() > 0) {
oldModel.getTabAt(oldModel.index()).hide(TabHidingType.CHANGED_TABS);
}
setInitialScrollIndexOffset();
}
};
mTabModelSelector.addObserver(mTabModelSelectorObserver);
mTabModelObserver = new TabModelObserver() {
@Override
public void didAddTab(Tab tab, int type, @TabCreationState int creationState) {
// TODO(wychen): move didAddTab and didSelectTab to another observer and inject
// after restoreCompleted.
if (!mTabModelSelector.isTabStateInitialized()) {
return;
}
mShouldIgnoreNextSelect = false;
}
@Override
public void didSelectTab(Tab tab, int type, int lastId) {
if (!mTabModelSelector.isTabStateInitialized()) {
return;
}
if (type == TabSelectionType.FROM_CLOSE || mShouldIgnoreNextSelect) {
mShouldIgnoreNextSelect = false;
return;
}
if (mIsSelectingInTabSwitcher) {
mIsSelectingInTabSwitcher = false;
TabModelFilter modelFilter = mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter();
if (modelFilter instanceof TabGroupModelFilter) {
((TabGroupModelFilter) modelFilter).recordSessionsCount(tab);
}
// Use TabSelectionType.From_USER to filter the new tab creation case.
if (type == TabSelectionType.FROM_USER) recordUserSwitchedTab(tab, lastId);
}
if (mContainerViewModel.get(IS_VISIBLE)) {
onTabSelecting(tab.getId(), false);
}
}
@Override
public void restoreCompleted() {
if (!mContainerViewModel.get(IS_VISIBLE)) return;
mResetHandler.resetWithTabList(
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(),
false, mShowTabsInMruOrder);
recordTabCounts();
setInitialScrollIndexOffset();
}
@Override
public void willCloseTab(Tab tab, boolean animate) {
if (mTabModelSelector.getCurrentModel().getCount() == 1) {
messageItemsController.removeAllAppendedMessage();
} else if (mPriceMessageService != null
&& mPriceMessageService.getBindingTabId() == tab.getId()) {
priceWelcomeMessageController.removePriceWelcomeMessage();
}
}
@Override
public void tabClosureUndone(Tab tab) {
if (mTabModelSelector.getCurrentModel().getCount() == 1) {
messageItemsController.restoreAllAppendedMessage();
}
if (mPriceMessageService != null
&& mPriceMessageService.getBindingTabId() == tab.getId()) {
priceWelcomeMessageController.restorePriceWelcomeMessage();
}
}
@Override
public void tabClosureCommitted(Tab tab) {
// TODO(crbug.com/1157578): Auto update the PriceMessageService instead of
// updating it based on the client caller.
if (mPriceMessageService != null
&& mPriceMessageService.getBindingTabId() == tab.getId()) {
mPriceMessageService.invalidateMessage();
}
}
};
mBrowserControlsObserver = new BrowserControlsStateProvider.Observer() {
@Override
public void onControlsOffsetChanged(int topOffset, int topControlsMinHeightOffset,
int bottomOffset, int bottomControlsMinHeightOffset, boolean needsAnimate) {
if (mMode == TabListCoordinator.TabListMode.CAROUSEL) return;
updateTopControlsProperties();
}
@Override
public void onTopControlsHeightChanged(
int topControlsHeight, int topControlsMinHeight) {
if (mMode == TabListCoordinator.TabListMode.CAROUSEL) return;
updateTopControlsProperties();
}
@Override
public void onBottomControlsHeightChanged(
int bottomControlsHeight, int bottomControlsMinHeight) {
mContainerViewModel.set(BOTTOM_CONTROLS_HEIGHT, bottomControlsHeight);
}
};
mBrowserControlsStateProvider.addObserver(mBrowserControlsObserver);
if (mTabModelSelector.getModels().isEmpty()) {
TabModelSelectorObserver selectorObserver = new TabModelSelectorObserver() {
@Override
public void onChange() {
assert !mTabModelSelector.getModels().isEmpty();
assert mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(false)
!= null;
assert mTabModelSelector.getTabModelFilterProvider().getTabModelFilter(true)
!= null;
mTabModelSelector.removeObserver(this);
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(
mTabModelObserver);
}
};
mTabModelSelector.addObserver(selectorObserver);
} else {
mTabModelSelector.getTabModelFilterProvider().addTabModelFilterObserver(
mTabModelObserver);
}
mContainerViewModel.set(VISIBILITY_LISTENER, this);
TabModelFilter tabModelFilter =
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter();
mContainerViewModel.set(
IS_INCOGNITO, tabModelFilter == null ? false : tabModelFilter.isIncognito());
mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true);
// Container view takes care of padding and margin in start surface.
if (mMode != TabListCoordinator.TabListMode.CAROUSEL) {
updateTopControlsProperties();
mContainerViewModel.set(
BOTTOM_CONTROLS_HEIGHT, browserControlsStateProvider.getBottomControlsHeight());
}
if (mMode == TabListMode.GRID) {
mContainerViewModel.set(BOTTOM_PADDING,
(int) context.getResources().getDimension(R.dimen.tab_grid_bottom_padding));
}
mContainerView = containerView;
mSoftClearTabListRunnable = mResetHandler::softCleanup;
mClearTabListRunnable =
() -> mResetHandler.resetWithTabList(null, false, mShowTabsInMruOrder);
mHandler = new Handler();
mTabContentManager = tabContentManager;
mShowTabsInMruOrder = TabSwitcherCoordinator.isShowingTabsInMRUOrder(mMode);
mMultiWindowModeObserver = isInMultiWindowMode -> {
if (isInMultiWindowMode) {
messageItemsController.removeAllAppendedMessage();
} else {
messageItemsController.restoreAllAppendedMessage();
}
};
mMultiWindowModeStateDispatcher.addObserver(mMultiWindowModeObserver);
}
/**
* Called after native initialization is completed.
* @param tabSelectionEditorController The controller that can control the visibility of the
* TabSelectionEditor.
*/
public void initWithNative(TabSelectionEditorCoordinator
.TabSelectionEditorController tabSelectionEditorController) {
mTabSelectionEditorController = tabSelectionEditorController;
}
/**
* Set the controller of the TabGridDialog so that it can be directly controlled.
* @param tabGridDialogController The handler of the Grid Dialog
*/
void setTabGridDialogController(
TabGridDialogMediator.DialogController tabGridDialogController) {
mTabGridDialogController = tabGridDialogController;
}
@VisibleForTesting
int getSoftCleanupDelayForTesting() {
return getSoftCleanupDelay();
}
private int getSoftCleanupDelay() {
if (mSoftCleanupDelayMsForTesting != null) return mSoftCleanupDelayMsForTesting;
if (!LibraryLoader.getInstance().isInitialized()) {
return 0;
}
return ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, SOFT_CLEANUP_DELAY_PARAM,
DEFAULT_SOFT_CLEANUP_DELAY_MS);
}
@VisibleForTesting
int getCleanupDelayForTesting() {
return getCleanupDelay();
}
private int getCleanupDelay() {
if (mCleanupDelayMsForTesting != null) return mCleanupDelayMsForTesting;
if (!LibraryLoader.getInstance().isInitialized()) {
return 0;
}
return ChromeFeatureList.getFieldTrialParamByFeatureAsInt(
ChromeFeatureList.TAB_GRID_LAYOUT_ANDROID, CLEANUP_DELAY_PARAM,
DEFAULT_CLEANUP_DELAY_MS);
}
private void setVisibility(boolean isVisible) {
if (isVisible) {
RecordUserAction.record("MobileToolbarShowStackView");
}
mContainerViewModel.set(IS_VISIBLE, isVisible);
}
private void updateTopControlsProperties() {
// If the Start surface is enabled, it will handle the margins and positioning of the tab
// switcher. So, we shouldn't do it here.
if (ReturnToChromeExperimentsUtil.isStartSurfaceEnabled(mContext)) {
mContainerViewModel.set(TOP_MARGIN, 0);
mContainerViewModel.set(SHADOW_TOP_OFFSET, 0);
return;
}
final int contentOffset = mBrowserControlsStateProvider.getContentOffset();
mContainerViewModel.set(TOP_MARGIN, contentOffset);
mContainerViewModel.set(SHADOW_TOP_OFFSET, contentOffset);
}
/**
* Record tab switch related metric for GTS.
* @param tab The new selected tab.
* @param lastId The id of the previous selected tab, and that tab is still a valid tab
* in TabModel.
*/
private void recordUserSwitchedTab(Tab tab, int lastId) {
if (tab == null) {
assert false : "New selected tab cannot be null when recording tab switch.";
return;
}
Tab fromTab = TabModelUtils.getTabById(mTabModelSelector.getCurrentModel(), lastId);
assert fromTab != null;
if (mModelIndexWhenShown == mTabModelSelector.getCurrentModelIndex()) {
if (tab.getId() == mTabIdWhenShown) {
if (mMode == TabListCoordinator.TabListMode.CAROUSEL) {
RecordUserAction.record("MobileTabReturnedToCurrentTab.TabCarousel");
} else if (mMode == TabListCoordinator.TabListMode.GRID) {
RecordUserAction.record("MobileTabReturnedToCurrentTab.TabGrid");
} else {
// TODO(crbug.com/1085246): Differentiate others.
}
RecordUserAction.record("MobileTabReturnedToCurrentTab");
RecordHistogram.recordSparseHistogram(
"Tabs.TabOffsetOfSwitch." + TabSwitcherCoordinator.COMPONENT_NAME, 0);
} else {
int fromIndex = mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.indexOf(fromTab);
int toIndex = mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.indexOf(tab);
if (fromIndex != toIndex) {
// Only log when you switch a tab page directly from tab switcher.
if (getRelatedTabs(tab.getId()).size() == 1) {
RecordUserAction.record(
"MobileTabSwitched." + TabSwitcherCoordinator.COMPONENT_NAME);
}
RecordHistogram.recordSparseHistogram(
"Tabs.TabOffsetOfSwitch." + TabSwitcherCoordinator.COMPONENT_NAME,
fromIndex - toIndex);
}
}
} else {
int newSelectedTabIndex =
TabModelUtils.getTabIndexById(mTabModelSelector.getCurrentModel(), tab.getId());
if (newSelectedTabIndex == mIndexInNewModelWhenSwitched) {
// TabModelImpl logs this action only when a different index is set within a
// TabModelImpl. If we switch between normal tab model and incognito tab model and
// leave the index the same (i.e. after switched tab model and select the
// highlighted tab), TabModelImpl doesn't catch this case. Therefore, we record it
// here.
RecordUserAction.record("MobileTabSwitched");
}
// Only log when you switch a tab page directly from tab switcher.
if (!TabUiFeatureUtilities.isTabGroupsAndroidEnabled(mContext)
|| getRelatedTabs(tab.getId()).size() == 1) {
RecordUserAction.record(
"MobileTabSwitched." + TabSwitcherCoordinator.COMPONENT_NAME);
}
}
if (mMode == TabListCoordinator.TabListMode.GRID
&& PriceTrackingUtilities.isTabModelPriceTrackingEligible(
mTabModelSelector.getCurrentModel())
&& PriceTrackingUtilities.isTrackPricesOnTabsEnabled()) {
RecordUserAction.record("Commerce.TabGridSwitched."
+ (ShoppingPersistedTabData.hasPriceDrop(tab) ? "HasPriceDrop"
: "NoPriceDrop"));
}
}
@Override
public boolean overviewVisible() {
return mContainerViewModel.get(IS_VISIBLE);
}
@Override
public void addOverviewModeObserver(TabSwitcher.OverviewModeObserver observer) {
mObservers.addObserver(observer);
}
@Override
public void removeOverviewModeObserver(TabSwitcher.OverviewModeObserver observer) {
mObservers.removeObserver(observer);
}
@Override
public void hideOverview(boolean animate) {
if (!animate) mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false);
setVisibility(false);
mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true);
if (mTabGridDialogController != null) {
// Don't wait until didSelectTab(), which is after the GTS animation.
// We need to hide the dialog immediately.
mTabGridDialogController.hideDialog(false);
}
}
void registerFirstMeaningfulPaintRecorder() {
ThreadUtils.assertOnUiThread();
if (mFirstMeaningfulPaintRecorder == null) return;
if (mRegisteredFirstMeaningfulPaintRecorder) return;
mRegisteredFirstMeaningfulPaintRecorder = true;
boolean hasTabs = getTabCount() > 0;
if (!hasTabs) {
FirstDrawDetector.waitForFirstDraw(
mContainerView, this::notifyOnFirstMeaningfulPaintNoTab);
} else {
mTabContentManager.addOnLastThumbnailListener(this::notifyOnFirstMeaningfulPaint);
}
}
private void notifyOnFirstMeaningfulPaintNoTab() {
notifyOnFirstMeaningfulPaint(0);
}
private void notifyOnFirstMeaningfulPaint(int numOfThumbnails) {
ThreadUtils.assertOnUiThread();
mFirstMeaningfulPaintRecorder.record(numOfThumbnails);
mFirstMeaningfulPaintRecorder = null;
}
boolean prepareOverview() {
mHandler.removeCallbacks(mSoftClearTabListRunnable);
mHandler.removeCallbacks(mClearTabListRunnable);
boolean quick = false;
if (!mTabModelSelector.isTabStateInitialized()) return quick;
if (TabUiFeatureUtilities.isTabToGtsAnimationEnabled()) {
quick = mResetHandler.resetWithTabList(
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), false,
mShowTabsInMruOrder);
}
setInitialScrollIndexOffset();
return quick;
}
private void setInitialScrollIndexOffset() {
int offset = mMode == TabListMode.CAROUSEL ? INITIAL_SCROLL_INDEX_OFFSET_CAROUSEL
: INITIAL_SCROLL_INDEX_OFFSET_GTS;
int initialPosition = Math.max(
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter().index()
- offset,
0);
// In MRU order, selected Tab is always at the first position.
if (mShowTabsInMruOrder) initialPosition = 0;
mContainerViewModel.set(INITIAL_SCROLL_INDEX, initialPosition);
}
@Override
public void showOverview(boolean animate) {
mHandler.removeCallbacks(mSoftClearTabListRunnable);
mHandler.removeCallbacks(mClearTabListRunnable);
if (mTabModelSelector.isTabStateInitialized()) {
mResetHandler.resetWithTabList(
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(),
TabUiFeatureUtilities.isTabToGtsAnimationEnabled(), mShowTabsInMruOrder);
recordTabCounts();
// When |mTabModelSelector.isTabStateInitialized| is false and INSTANT_START is enabled,
// the scrolling request is already processed in TabModelObserver#restoreCompleted.
// Therefore, we only need to handle the case with isTabStateInitialized() here.
setInitialScrollIndexOffset();
} else if (CachedFeatureFlags.isEnabled(ChromeFeatureList.INSTANT_START)) {
List<PseudoTab> allTabs;
try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) {
allTabs = PseudoTab.getAllPseudoTabsFromStateFile(mContext);
}
mResetHandler.resetWithTabs(allTabs, TabUiFeatureUtilities.isTabToGtsAnimationEnabled(),
mShowTabsInMruOrder);
}
if (!animate) mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false);
setVisibility(true);
mModelIndexWhenShown = mTabModelSelector.getCurrentModelIndex();
mTabIdWhenShown = mTabModelSelector.getCurrentTabId();
mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true);
}
@Override
public void startedShowing(boolean isAnimating) {
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.startedShowing();
}
}
@Override
public void finishedShowing() {
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.finishedShowing();
}
}
@Override
public void startedHiding(boolean isAnimating) {
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.startedHiding();
}
}
@Override
public void finishedHiding() {
for (TabSwitcher.OverviewModeObserver observer : mObservers) {
observer.finishedHiding();
}
}
@Override
public boolean onBackPressed(boolean isOnHomepage) {
// The TabSelectionEditor dialog can be shown on the Start surface without showing the Grid
// Tab switcher, so skip the check of visibility of mContainerViewModel here.
if (mTabSelectionEditorController != null
&& mTabSelectionEditorController.handleBackPressed()) {
return true;
}
if (!mContainerViewModel.get(IS_VISIBLE)) return false;
if (mTabGridDialogController != null && mTabGridDialogController.handleBackPressed()) {
return true;
}
// When the Start surface is showing, we no longer need to call onTabSelecting().
if (isOnHomepage && mMode == TabListCoordinator.TabListMode.CAROUSEL) return false;
if (mTabModelSelector.getCurrentTab() == null) return false;
onTabSelecting(mTabModelSelector.getCurrentTabId(), false);
return true;
}
@Override
public boolean isDialogVisible() {
if (mTabSelectionEditorController != null && mTabSelectionEditorController.isVisible()) {
return true;
}
if (mTabGridDialogController != null && mTabGridDialogController.isVisible()) {
return true;
}
return false;
}
@Override
public void showTabSelectionEditor(List<Tab> tabs) {
if (mTabSelectionEditorController == null) {
return;
}
mTabSelectionEditorController.show(tabs);
}
@Override
public void enableRecordingFirstMeaningfulPaint(long activityCreateTimeMs) {
mFirstMeaningfulPaintRecorder = new FirstMeaningfulPaintRecorder(activityCreateTimeMs);
}
@Override
public void onOverviewShownAtLaunch(long activityCreationTimeMs) {}
/**
* Do clean-up work after the overview hiding animation is finished.
* @see TabSwitcher.TabListDelegate#postHiding
*/
void postHiding() {
Log.d(TAG, "SoftCleanupDelay = " + getSoftCleanupDelay());
mHandler.postDelayed(mSoftClearTabListRunnable, getSoftCleanupDelay());
Log.d(TAG, "CleanupDelay = " + getCleanupDelay());
mHandler.postDelayed(mClearTabListRunnable, getCleanupDelay());
}
/**
* Set the delay for soft cleanup.
*/
void setSoftCleanupDelayForTesting(int timeoutMs) {
mSoftCleanupDelayMsForTesting = timeoutMs;
}
/**
* Set the delay for lazy cleanup.
*/
void setCleanupDelayForTesting(int timeoutMs) {
mCleanupDelayMsForTesting = timeoutMs;
}
/**
* Destroy any members that needs clean up.
*/
public void destroy() {
mTabModelSelector.removeObserver(mTabModelSelectorObserver);
mBrowserControlsStateProvider.removeObserver(mBrowserControlsObserver);
mTabModelSelector.getTabModelFilterProvider().removeTabModelFilterObserver(
mTabModelObserver);
mMultiWindowModeStateDispatcher.removeObserver(mMultiWindowModeObserver);
}
void setOnTabSelectingListener(TabSwitcher.OnTabSelectingListener listener) {
mOnTabSelectingListener = listener;
}
void setPriceMessageService(PriceMessageService priceMessageService) {
mPriceMessageService = priceMessageService;
}
// GridCardOnClickListenerProvider implementation.
@Override
@Nullable
public TabListMediator.TabActionListener openTabGridDialog(Tab tab) {
if (!ableToOpenDialog(tab)) return null;
assert getRelatedTabs(tab.getId()).size() != 1;
assert mTabGridDialogController != null;
return tabId -> {
List<Tab> relatedTabs = getRelatedTabs(tabId);
if (relatedTabs.size() == 0) {
relatedTabs = null;
}
mTabGridDialogController.resetWithListOfTabs(relatedTabs);
RecordUserAction.record("TabGridDialog.ExpandedFromSwitcher");
};
}
@Override
public void onTabSelecting(int tabId, boolean fromActionButton) {
if (fromActionButton && (mMode == TabListMode.CAROUSEL || mMode == TabListMode.GRID)) {
Tab newlySelectedTab =
TabModelUtils.getTabById(mTabModelSelector.getCurrentModel(), tabId);
StartSurfaceUserData.setKeepTab(newlySelectedTab, true);
if (mMode == TabListMode.CAROUSEL) {
StartSurfaceUserData.setOpenedFromStart(newlySelectedTab);
}
}
mIsSelectingInTabSwitcher = true;
if (mOnTabSelectingListener != null) {
mOnTabSelectingListener.onTabSelecting(LayoutManagerImpl.time(), tabId);
}
}
@Override
public void scrollToTab(int tabIndex) {
mContainerViewModel.set(TabListContainerProperties.INITIAL_SCROLL_INDEX, tabIndex);
}
private boolean ableToOpenDialog(Tab tab) {
return TabUiFeatureUtilities.isTabGroupsAndroidEnabled(mContext)
&& mTabModelSelector.isIncognitoSelected() == tab.isIncognito()
&& getRelatedTabs(tab.getId()).size() != 1;
}
private List<Tab> getRelatedTabs(int tabId) {
return mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.getRelatedTabList(tabId);
}
private void recordTabCounts() {
final TabModel model = mTabModelSelector.getCurrentModel();
if (model == null) return;
RecordHistogram.recordCountHistogram(TAB_COUNT_HISTOGRAM, model.getCount());
final TabModelFilter filter =
mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter();
if (filter == null) return;
RecordHistogram.recordCountHistogram(TAB_ENTRIES_HISTOGRAM, filter.getCount());
}
private int getTabCount() {
if (mTabModelSelector.isTabStateInitialized()) {
return mTabModelSelector.getTabModelFilterProvider()
.getCurrentTabModelFilter()
.getCount();
} else {
return SharedPreferencesManager.getInstance().readInt(
ChromePreferenceKeys.REGULAR_TAB_COUNT);
}
}
}