| // Copyright 2019 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.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.BLOCK_TOUCH_INPUT; |
| 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.MODE; |
| 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 static org.chromium.chrome.browser.tasks.tab_management.TabSwitcherConstants.HARD_CLEANUP_DELAY_MS; |
| import static org.chromium.chrome.browser.tasks.tab_management.TabSwitcherConstants.SOFT_CLEANUP_DELAY_MS; |
| |
| import android.content.Context; |
| import android.os.Handler; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.LinearLayout; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.base.CallbackController; |
| import org.chromium.base.Log; |
| import org.chromium.base.ObserverList; |
| import org.chromium.base.metrics.RecordHistogram; |
| import org.chromium.base.metrics.RecordUserAction; |
| import org.chromium.base.supplier.LazyOneshotSupplier; |
| import org.chromium.base.supplier.ObservableSupplier; |
| import org.chromium.base.supplier.ObservableSupplierImpl; |
| import org.chromium.base.supplier.OneshotSupplier; |
| import org.chromium.base.supplier.TransitiveObservableSupplier; |
| import org.chromium.chrome.browser.back_press.BackPressManager; |
| import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider; |
| import org.chromium.chrome.browser.flags.ChromeFeatureList; |
| import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthController; |
| import org.chromium.chrome.browser.incognito.reauth.IncognitoReauthManager; |
| import org.chromium.chrome.browser.layouts.LayoutStateProvider; |
| import org.chromium.chrome.browser.layouts.LayoutStateProvider.LayoutStateObserver; |
| import org.chromium.chrome.browser.layouts.LayoutType; |
| import org.chromium.chrome.browser.price_tracking.PriceTrackingUtilities; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tab.TabCreationState; |
| import org.chromium.chrome.browser.tab.TabSelectionType; |
| import org.chromium.chrome.browser.tab.state.ShoppingPersistedTabData; |
| import org.chromium.chrome.browser.tab_ui.TabSwitcher; |
| import org.chromium.chrome.browser.tab_ui.TabSwitcher.TabSwitcherType; |
| import org.chromium.chrome.browser.tab_ui.TabSwitcher.TabSwitcherViewObserver; |
| import org.chromium.chrome.browser.tab_ui.TabSwitcherCustomViewManager; |
| 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.tab_management.TabGridDialogMediator.DialogController; |
| import org.chromium.chrome.browser.tasks.tab_management.TabListCoordinator.TabListMode; |
| import org.chromium.chrome.browser.tasks.tab_management.TabListEditorCoordinator.TabListEditorController; |
| import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager; |
| import org.chromium.chrome.features.start_surface.StartSurfaceUserData; |
| import org.chromium.chrome.tab_ui.R; |
| import org.chromium.components.browser_ui.widget.gesture.BackPressHandler; |
| import org.chromium.ui.base.DeviceFormFactor; |
| 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, |
| TabSwitcherCustomViewManager.Delegate, |
| BackPressHandler { |
| private static final String TAG = "TabSwitcherMediator"; |
| |
| private final Handler mHandler; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| final Runnable mSoftClearTabListRunnable; |
| |
| @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) |
| final Runnable mClearTabListRunnable; |
| |
| private final TabSwitcherResetHandler mResetHandler; |
| private final PropertyModel mContainerViewModel; |
| private final TabModelSelector mTabModelSelector; |
| private final TabModelObserver mTabModelObserver; |
| private final TabModelSelectorObserver mTabModelSelectorObserver; |
| private final ObserverList<TabSwitcherViewObserver> mObservers = new ObserverList<>(); |
| private final BrowserControlsStateProvider mBrowserControlsStateProvider; |
| private final BrowserControlsStateProvider.Observer mBrowserControlsObserver; |
| private final ViewGroup mContainerView; |
| private final boolean mIsTablet; |
| private final ObservableSupplierImpl<Boolean> mBackPressChangedSupplier = |
| new ObservableSupplierImpl<>(); |
| private final ObservableSupplierImpl<Boolean> mIsDialogVisibleSupplier = |
| new ObservableSupplierImpl<>(); |
| private final Callback<Boolean> mNotifyBackPressedCallback = this::notifyBackPressStateChanged; |
| private final @NonNull LazyOneshotSupplier<TabGridDialogMediator.DialogController> |
| mTabGridDialogControllerSupplier; |
| private Runnable mOnTabSwitcherShownCallback; |
| |
| /** |
| * The callback which is supplied to the {@link IncognitoReauthController} that takes care of |
| * resetting the Incognito tab list with actual Incognito tabs upon successful authentication. |
| */ |
| private final IncognitoReauthManager.IncognitoReauthCallback mIncognitoReauthCallback = |
| new IncognitoReauthManager.IncognitoReauthCallback() { |
| @Override |
| public void onIncognitoReauthNotPossible() {} |
| |
| @Override |
| public void onIncognitoReauthSuccess() { |
| assert mTabModelSelector |
| .getTabModelFilterProvider() |
| .getCurrentTabModelFilter() |
| .isIncognito() |
| : "The incognito re-auth controller only affects Incognito tab list."; |
| mResetHandler.resetWithTabList( |
| mTabModelSelector |
| .getTabModelFilterProvider() |
| .getCurrentTabModelFilter(), |
| false); |
| setInitialScrollIndexOffset(); |
| requestAccessibilityFocusOnCurrentTab(); |
| } |
| |
| @Override |
| public void onIncognitoReauthFailure() {} |
| }; |
| |
| private CallbackController mCallbackController; |
| private @Nullable ObservableSupplier<TabListEditorController> mTabListEditorControllerSupplier; |
| private @Nullable TransitiveObservableSupplier<TabListEditorController, Boolean> |
| mCurrentTabListEditorControllerBackSupplier; |
| private TabSwitcher.OnTabSelectingListener mOnTabSelectingListener; |
| |
| /** |
| * This allows to check if re-auth is pending when tab switcher is shown in Incognito mode. |
| * If so, we clear the Incognito tab lists until the re-auth is successful. |
| */ |
| private @Nullable IncognitoReauthController mIncognitoReauthController; |
| |
| /** |
| * A custom view that can be supplied by clients to be shown inside the tab grid. |
| * Only one client at a time is supported. The custom view is set to null when the client |
| * signals the mediator for removal. |
| */ |
| private @Nullable View mCustomView; |
| |
| /** A back press {@link Runnable} that can be supplied by clients when adding a custom view. */ |
| private @Nullable Runnable mCustomViewBackPressRunnable; |
| |
| /** |
| * In cases where a didSelectTab was due to switching models with a toggle, |
| * we don't change tab grid visibility. |
| */ |
| private boolean mShouldIgnoreNextSelect; |
| |
| private boolean mIncognitoStateWhenShown; |
| private int mTabIdWhenShown; |
| private int mIndexInNewModelWhenSwitched; |
| private boolean mIsSelectingInTabSwitcher; |
| |
| private @TabListCoordinator.TabListMode int mMode; |
| private Context mContext; |
| private SnackbarManager mSnackbarManager; |
| private boolean mIsTransitionInProgress; |
| private boolean mIsTabSwitcherShowing; |
| |
| @Nullable private LayoutStateProvider mLayoutStateProvider; |
| @Nullable private LayoutStateObserver mLayoutStateObserver; |
| // The layout type of the last active layout which was shown before showing the TAB_SWITCHER |
| // layout. |
| private @LayoutType int mLastActiveLayoutType; |
| |
| /** |
| * Basic constructor for the Mediator. |
| * |
| * @param context The context to use for accessing {@link android.content.res.Resources}. |
| * @param resetHandler The {@link TabSwitcherResetHandler} 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 handler The {@link Handler} for running cleanup callbacks. |
| * @param mode One of the {@link TabListMode}. |
| * @param incognitoReauthControllerSupplier {@link OneshotSupplier<IncognitoReauthController>} |
| * to detect pending re-auth when tab switcher is shown. |
| * @param backPressManager {@link BackPressManager} to handle back press gesture. |
| * @param tabGridDialogControllerSupplier {@link DialogController} supplier for lazy |
| * initialization on first use. |
| * @param onTabSwitcherShownCallback is a callback method to notify {@link |
| * TabSwitcherCoordinator} class to attach empty view when #showTabSwitcherView is invoked. |
| * @param layoutStateProviderSupplier {@link OneshotSupplier<LayoutStateProvider>} to provide |
| * layout state changes. |
| */ |
| TabSwitcherMediator( |
| Context context, |
| TabSwitcherResetHandler resetHandler, |
| PropertyModel containerViewModel, |
| TabModelSelector tabModelSelector, |
| BrowserControlsStateProvider browserControlsStateProvider, |
| ViewGroup containerView, |
| @NonNull Handler handler, |
| @TabListMode int mode, |
| @Nullable OneshotSupplier<IncognitoReauthController> incognitoReauthControllerSupplier, |
| @Nullable BackPressManager backPressManager, |
| @NonNull LazyOneshotSupplier<DialogController> tabGridDialogControllerSupplier, |
| Runnable onTabSwitcherShownCallback, |
| @Nullable OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier) { |
| mResetHandler = resetHandler; |
| mContainerViewModel = containerViewModel; |
| mTabModelSelector = tabModelSelector; |
| mBrowserControlsStateProvider = browserControlsStateProvider; |
| mMode = mode; |
| mHandler = handler; |
| mContainerViewModel.set(MODE, mode); |
| mContext = context; |
| mIsTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(context); |
| mOnTabSwitcherShownCallback = onTabSwitcherShownCallback; |
| mTabGridDialogControllerSupplier = tabGridDialogControllerSupplier; |
| // Any observer events get posted so it is safe to set up early. |
| mTabGridDialogControllerSupplier.onAvailable( |
| (tabGridDialogController) -> { |
| tabGridDialogController |
| .getHandleBackPressChangedSupplier() |
| .addObserver(mNotifyBackPressedCallback); |
| }); |
| if (layoutStateProviderSupplier != null) { |
| if (layoutStateProviderSupplier.hasValue()) { |
| onLayoutStateProviderAvailable(layoutStateProviderSupplier.get()); |
| } else { |
| // Only use onAvailable if no value is available as waiting for the Promise to |
| // resolve risks getActiveLayoutType changing before the value is supplied. |
| layoutStateProviderSupplier.onAvailable(this::onLayoutStateProviderAvailable); |
| } |
| } |
| |
| if (incognitoReauthControllerSupplier != null) { |
| mCallbackController = new CallbackController(); |
| incognitoReauthControllerSupplier.onAvailable( |
| mCallbackController.makeCancelable( |
| (incognitoReauthController) -> { |
| mIncognitoReauthController = incognitoReauthController; |
| mIncognitoReauthController.addIncognitoReauthCallback( |
| mIncognitoReauthCallback); |
| })); |
| } |
| |
| 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()); |
| notifyBackPressStateChangedInternal(); |
| if (mTabGridDialogControllerSupplier.hasValue()) { |
| mTabGridDialogControllerSupplier.get().hideDialog(false); |
| } |
| if (!mContainerViewModel.get(IS_VISIBLE)) return; |
| |
| if (clearIncognitoTabListForReauth()) return; |
| mResetHandler.resetWithTabList(currentTabModelFilter, false); |
| setInitialScrollIndexOffset(); |
| requestAccessibilityFocusOnCurrentTab(); |
| } |
| }; |
| mTabModelSelector.addObserver(mTabModelSelectorObserver); |
| |
| mTabModelObserver = |
| new TabModelObserver() { |
| @Override |
| public void didAddTab( |
| Tab tab, |
| int type, |
| @TabCreationState int creationState, |
| boolean markedForSelection) { |
| // 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; |
| } |
| notifyBackPressStateChangedInternal(); |
| if (type == TabSelectionType.FROM_CLOSE |
| || mShouldIgnoreNextSelect |
| || type == TabSelectionType.FROM_UNDO) { |
| mShouldIgnoreNextSelect = false; |
| return; |
| } |
| if (mIsSelectingInTabSwitcher) { |
| mIsSelectingInTabSwitcher = false; |
| |
| // 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); |
| setInitialScrollIndexOffset(); |
| } |
| |
| |
| @Override |
| public void tabClosureUndone(Tab tab) { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| @Override |
| public void tabPendingClosure(Tab tab) { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| @Override |
| public void onFinishingTabClosure(Tab tab) { |
| // If tab is closed by the site itself rather than user's input, |
| // tabPendingClosure & tabClosureCommitted won't be called. |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| @Override |
| public void tabRemoved(Tab tab) { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| @Override |
| public void multipleTabsPendingClosure(List<Tab> tabs, boolean isAllTabs) { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| }; |
| |
| mBrowserControlsObserver = |
| new BrowserControlsStateProvider.Observer() { |
| @Override |
| public void onControlsOffsetChanged( |
| int topOffset, |
| int topControlsMinHeightOffset, |
| int bottomOffset, |
| int bottomControlsMinHeightOffset, |
| boolean needsAnimate) { |
| updateTopControlsProperties(); |
| } |
| |
| @Override |
| public void onTopControlsHeightChanged( |
| int topControlsHeight, int topControlsMinHeight) { |
| 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); |
| |
| 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)); |
| if (backPressManager != null && BackPressManager.isEnabled()) { |
| backPressManager.addHandler(this, BackPressHandler.Type.TAB_SWITCHER); |
| notifyBackPressStateChangedInternal(); |
| } |
| } |
| |
| mContainerView = containerView; |
| |
| mSoftClearTabListRunnable = mResetHandler::softCleanup; |
| mClearTabListRunnable = |
| () -> { |
| mResetHandler.hardCleanup(); |
| mResetHandler.resetWithTabList(null, false); |
| }; |
| |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| /** Called after native initialization is completed. */ |
| public void initWithNative(@Nullable SnackbarManager snackbarManager) { |
| mSnackbarManager = snackbarManager; |
| } |
| |
| /** |
| * @param tabListEditorControllerSupplier The supllier for the controller of the tab list |
| * editor. |
| */ |
| public void setTabListEditorControllerSupplier( |
| @NonNull ObservableSupplier<TabListEditorController> tabListEditorControllerSupplier) { |
| assert mTabListEditorControllerSupplier == null |
| : "setTabListEditorControllerSupplier should be called only once."; |
| mTabListEditorControllerSupplier = tabListEditorControllerSupplier; |
| mCurrentTabListEditorControllerBackSupplier = |
| new TransitiveObservableSupplier<>( |
| tabListEditorControllerSupplier, |
| tabListEditorController -> { |
| return tabListEditorController.getHandleBackPressChangedSupplier(); |
| }); |
| mCurrentTabListEditorControllerBackSupplier.addObserver(mNotifyBackPressedCallback); |
| } |
| |
| private void setVisibility(boolean isVisible) { |
| if (isVisible) { |
| RecordUserAction.record("MobileToolbarShowStackView"); |
| } |
| |
| mContainerViewModel.set(IS_VISIBLE, isVisible); |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| private void blockTouchInput(boolean blockTouchInput) { |
| mContainerViewModel.set(BLOCK_TOUCH_INPUT, blockTouchInput); |
| } |
| |
| private void updateTopControlsProperties() { |
| // The grid tab switcher for tablets translates up over top of the browser controls. |
| if (mIsTablet) { |
| final int toolbarHeight = getToolbarHeight(); |
| |
| mContainerViewModel.set(TOP_MARGIN, toolbarHeight); |
| mContainerViewModel.set(SHADOW_TOP_OFFSET, toolbarHeight); |
| 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; |
| TabModelFilter filter = |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(); |
| if (mIncognitoStateWhenShown == mTabModelSelector.isIncognitoSelected()) { |
| if (tab.getId() == mTabIdWhenShown) { |
| if (mMode == TabListCoordinator.TabListMode.GRID) { |
| RecordUserAction.record("MobileTabReturnedToCurrentTab.TabGrid"); |
| } else { |
| // TODO(crbug.com/40132120): Differentiate others. |
| } |
| RecordUserAction.record("MobileTabReturnedToCurrentTab"); |
| RecordHistogram.recordSparseHistogram( |
| "Tabs.TabOffsetOfSwitch." + TabSwitcherCoordinator.COMPONENT_NAME, 0); |
| } else { |
| int fromIndex = filter.indexOf(fromTab); |
| int toIndex = filter.indexOf(tab); |
| |
| if (fromIndex != toIndex) { |
| // Only log when you switch a tab page directly from tab switcher. |
| if (!filter.isTabInTabGroup(tab)) { |
| 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 (!filter.isTabInTabGroup(tab)) { |
| RecordUserAction.record( |
| "MobileTabSwitched." + TabSwitcherCoordinator.COMPONENT_NAME); |
| } |
| } |
| Profile profile = mTabModelSelector.getCurrentModel().getProfile(); |
| if (mMode == TabListCoordinator.TabListMode.GRID |
| && !profile.isOffTheRecord() |
| && PriceTrackingUtilities.isTrackPricesOnTabsEnabled(profile)) { |
| RecordUserAction.record( |
| "Commerce.TabGridSwitched." |
| + (ShoppingPersistedTabData.hasPriceDrop(tab) |
| ? "HasPriceDrop" |
| : "NoPriceDrop")); |
| } |
| } |
| |
| @Override |
| public ViewGroup getTabSwitcherContainer() { |
| return mContainerView; |
| } |
| |
| @Override |
| public void addTabSwitcherViewObserver(TabSwitcherViewObserver observer) { |
| mObservers.addObserver(observer); |
| } |
| |
| @Override |
| public void removeTabSwitcherViewObserver(TabSwitcherViewObserver observer) { |
| mObservers.removeObserver(observer); |
| } |
| |
| @Override |
| public void prepareHideTabSwitcherView() { |
| hideTabSwitcherViewInternal(/* animate= */ false, /* skipVisibility= */ true); |
| } |
| |
| @Override |
| public void hideTabSwitcherView(boolean animate) { |
| hideTabSwitcherViewInternal(animate, /* skipVisibility= */ false); |
| } |
| |
| private void hideTabSwitcherViewInternal(boolean animate, boolean skipVisibility) { |
| if (mMode == TabListMode.GRID) { |
| mIsTransitionInProgress = true; |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| blockTouchInput(true); |
| |
| if (!skipVisibility) { |
| if (!animate) mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false); |
| setVisibility(false); |
| mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true); |
| } |
| |
| if (mTabGridDialogControllerSupplier.hasValue()) { |
| // Don't wait until didSelectTab(), which is after the GTS animation. |
| // We need to hide the dialog immediately. |
| mTabGridDialogControllerSupplier.get().hideDialog(false); |
| } |
| } |
| |
| boolean prepareTabSwitcherView() { |
| mHandler.removeCallbacks(mSoftClearTabListRunnable); |
| mHandler.removeCallbacks(mClearTabListRunnable); |
| boolean quick = false; |
| if (!mTabModelSelector.isTabStateInitialized()) return quick; |
| if (TabUiFeatureUtilities.isTabToGtsAnimationEnabled(mContext)) { |
| quick = |
| mResetHandler.resetWithTabList( |
| mTabModelSelector |
| .getTabModelFilterProvider() |
| .getCurrentTabModelFilter(), |
| false); |
| } |
| setInitialScrollIndexOffset(); |
| |
| return quick; |
| } |
| |
| private void setInitialScrollIndexOffset() { |
| int initialPosition = |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter().index(); |
| |
| mContainerViewModel.set(INITIAL_SCROLL_INDEX, initialPosition); |
| } |
| |
| @Override |
| public void prepareShowTabSwitcherView() { |
| if (mMode != TabListMode.GRID) return; |
| |
| mIsTransitionInProgress = true; |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| // @Todo(crbug.com/1464856) clean up empty state implementation after Start Surface Refactor is |
| // fully launched. |
| @Override |
| public void showTabSwitcherView(boolean animate) { |
| mIsTransitionInProgress = false; |
| mHandler.removeCallbacks(mSoftClearTabListRunnable); |
| mHandler.removeCallbacks(mClearTabListRunnable); |
| mOnTabSwitcherShownCallback.run(); |
| |
| if (!mTabModelSelector.isIncognitoSelected() || !clearIncognitoTabListForReauth()) { |
| if (mTabModelSelector.isTabStateInitialized()) { |
| mResetHandler.resetWithTabList( |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), |
| TabUiFeatureUtilities.isTabToGtsAnimationEnabled(mContext)); |
| // 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(); |
| } |
| } |
| |
| if (!animate) mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false); |
| setVisibility(true); |
| blockTouchInput(false); |
| mIncognitoStateWhenShown = mTabModelSelector.isIncognitoSelected(); |
| mTabIdWhenShown = mTabModelSelector.getCurrentTabId(); |
| mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, true); |
| } |
| |
| @Override |
| public void startedShowing(boolean isAnimating) { |
| for (TabSwitcherViewObserver observer : mObservers) { |
| observer.startedShowing(); |
| } |
| } |
| |
| @Override |
| public void finishedShowing() { |
| mIsTabSwitcherShowing = true; |
| |
| requestAccessibilityFocusOnCurrentTab(); |
| |
| for (TabSwitcherViewObserver observer : mObservers) { |
| observer.finishedShowing(); |
| } |
| } |
| |
| @Override |
| public void startedHiding(boolean isAnimating) { |
| for (TabSwitcherViewObserver observer : mObservers) { |
| observer.startedHiding(); |
| } |
| } |
| |
| @Override |
| public void finishedHiding() { |
| mIsTabSwitcherShowing = false; |
| for (TabSwitcherViewObserver observer : mObservers) { |
| observer.finishedHiding(); |
| } |
| } |
| |
| @Override |
| public boolean onBackPressed() { |
| boolean ret = onBackPressedInternal(); |
| if (ret) { |
| // When SS is enabled or refactor is disabled, StartSurfaceMediator will consume back |
| // press and call this method if necessary. |
| BackPressManager.record(BackPressHandler.Type.TAB_SWITCHER); |
| } |
| return ret; |
| } |
| |
| private boolean onBackPressedInternal() { |
| // The TabListEditor dialog can be shown on the Start surface without showing the Grid |
| // Tab switcher, so skip the check of visibility of mContainerViewModel here. |
| TabListEditorController editorController = getTabListEditorController(); |
| if (editorController != null && editorController.handleBackPressed()) { |
| return true; |
| } |
| |
| if (mCustomViewBackPressRunnable != null) { |
| mCustomViewBackPressRunnable.run(); |
| return true; |
| } |
| |
| if (!mIsTablet && mIsTransitionInProgress && mMode == TabListCoordinator.TabListMode.GRID) { |
| // crbug.com/1420410: intentionally do nothing to wait for tab-to-GTS transition to be |
| // finished. Note this has to be before following if-branch since during transition, the |
| // container is still invisible. On tablet, the translation transition replaces the |
| // tab-to-GTS (expand/shrink) animation, which does not suffer from the same issue. |
| return true; |
| } |
| |
| if (!mContainerViewModel.get(IS_VISIBLE)) { |
| assert !BackPressManager.isEnabled() |
| : "Invisible container: Back press must be handled"; |
| return false; |
| } |
| |
| if (mTabGridDialogControllerSupplier.hasValue() |
| && mTabGridDialogControllerSupplier.get().handleBackPressed()) { |
| return true; |
| } |
| |
| if (mTabModelSelector.getCurrentTab() == null) { |
| assert !BackPressManager.isEnabled() : "No tab: Back press must be handled"; |
| return false; |
| } |
| |
| // Going back to the Start surface isn't handled by the TabSwitcherMediator any more, but in |
| // {@link ReturnToChromeBackPressHandler} when it isn't in incognito mode. |
| if (mLastActiveLayoutType == LayoutType.START_SURFACE |
| && !mTabModelSelector.isIncognitoSelected()) { |
| return false; |
| } |
| |
| onTabSelecting(mTabModelSelector.getCurrentTabId(), false); |
| |
| return true; |
| } |
| |
| @Override |
| public @BackPressResult int handleBackPress() { |
| return onBackPressedInternal() ? BackPressResult.SUCCESS : BackPressResult.FAILURE; |
| } |
| |
| @Override |
| public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() { |
| return mBackPressChangedSupplier; |
| } |
| |
| @Override |
| public boolean isDialogVisible() { |
| TabListEditorController editorController = getTabListEditorController(); |
| if (editorController != null && editorController.isVisible()) { |
| return true; |
| } |
| |
| if (mTabGridDialogControllerSupplier.hasValue() |
| && mTabGridDialogControllerSupplier.get().isVisible()) { |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public ObservableSupplier<Boolean> isDialogVisibleSupplier() { |
| return mIsDialogVisibleSupplier; |
| } |
| |
| @Override |
| public void onOverviewShownAtLaunch(long activityCreationTimeMs) {} |
| |
| @Override |
| public @TabSwitcherType int getTabSwitcherType() { |
| switch (mMode) { |
| case TabListMode.GRID: |
| return TabSwitcherType.GRID; |
| case TabListMode.LIST: |
| case TabListMode.STRIP: |
| default: |
| return TabSwitcherType.NONE; |
| } |
| } |
| |
| @Override |
| public void onHomepageChanged() { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| @Override |
| public void setSnackbarParentView(ViewGroup parentView) { |
| if (mSnackbarManager == null) return; |
| mSnackbarManager.setParentView(parentView); |
| } |
| |
| /** |
| * A method to handle signal from outside world that a client is requesting to show a custom |
| * view inside the tab switcher. |
| * |
| * @param customView A {@link View} view that needs to be shown. |
| * @param backPressRunnable A {@link Runnable} which can be supplied if clients also wish to |
| * handle back presses while the custom view is shown. A null value |
| * can be passed to not |
| * intercept back presses. |
| * @param clearTabList A boolean to indicate whether we should clear the tab list when |
| * showing the custom view. |
| */ |
| @Override |
| public void addCustomView( |
| @NonNull View customView, @Nullable Runnable backPressRunnable, boolean clearTabList) { |
| assert mCustomView == null : "Only one client at a time is supported to add a custom view."; |
| |
| // Hide any tab grid dialog before we add the custom view. |
| if (mTabGridDialogControllerSupplier.hasValue()) { |
| mTabGridDialogControllerSupplier.get().hideDialog(false); |
| } |
| |
| if (clearTabList) { |
| mResetHandler.resetWithTabList(null, false); |
| } |
| |
| // The grid tab switcher for tablets translates up over top of the browser controls, causing |
| // the custom view to do the same. |
| if (mIsTablet) { |
| LinearLayout.LayoutParams params = |
| new LinearLayout.LayoutParams( |
| LinearLayout.LayoutParams.MATCH_PARENT, |
| LinearLayout.LayoutParams.MATCH_PARENT); |
| params.topMargin = getToolbarHeight(); |
| |
| mContainerView.addView(customView, params); |
| } else { |
| mContainerView.addView(customView); |
| } |
| |
| mCustomView = customView; |
| mCustomViewBackPressRunnable = backPressRunnable; |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| /** |
| * A method to handle signal from outside world that a client is requesting to remove the custom |
| * view from the tab switcher. |
| * |
| * @param customView A {@link View} view that needs to be removed. |
| */ |
| @Override |
| public void removeCustomView(@NonNull View customView) { |
| assert mCustomView != null |
| : "No client previously passed a custom view that needs removal."; |
| mContainerView.removeView(customView); |
| mCustomView = null; |
| mCustomViewBackPressRunnable = null; |
| notifyBackPressStateChangedInternal(); |
| mResetHandler.resetWithTabList( |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), |
| /* quickMode= */ false); |
| } |
| |
| /** |
| * Do clean-up work after the overview hiding animation is finished. |
| * @see TabSwitcher.TabListDelegate#postHiding |
| */ |
| void postHiding() { |
| Log.d(TAG, "SoftCleanupDelay = " + SOFT_CLEANUP_DELAY_MS); |
| mHandler.postDelayed(mSoftClearTabListRunnable, SOFT_CLEANUP_DELAY_MS); |
| Log.d(TAG, "HardCleanupDelay = " + HARD_CLEANUP_DELAY_MS); |
| mHandler.postDelayed(mClearTabListRunnable, HARD_CLEANUP_DELAY_MS); |
| mIsTransitionInProgress = false; |
| notifyBackPressStateChangedInternal(); |
| if (ChromeFeatureList.sGridTabSwitcherAndroidAnimations.isEnabled()) { |
| // Ensure we skip animating here as the UI is entirely occluded already. |
| boolean previousAnimateVisibilityChangesValue = |
| mContainerViewModel.get(ANIMATE_VISIBILITY_CHANGES); |
| mContainerViewModel.set(ANIMATE_VISIBILITY_CHANGES, false); |
| setVisibility(false); |
| mContainerViewModel.set( |
| ANIMATE_VISIBILITY_CHANGES, previousAnimateVisibilityChangesValue); |
| } |
| } |
| |
| /** Destroy any members that needs clean up. */ |
| public void destroy() { |
| if (mCurrentTabListEditorControllerBackSupplier != null) { |
| mCurrentTabListEditorControllerBackSupplier.removeObserver(mNotifyBackPressedCallback); |
| } |
| |
| if (mTabGridDialogControllerSupplier.hasValue()) { |
| mTabGridDialogControllerSupplier |
| .get() |
| .getHandleBackPressChangedSupplier() |
| .removeObserver(mNotifyBackPressedCallback); |
| } |
| |
| if (mIncognitoReauthController != null) { |
| mIncognitoReauthController.removeIncognitoReauthCallback(mIncognitoReauthCallback); |
| } |
| |
| if (mCallbackController != null) { |
| mCallbackController.destroy(); |
| } |
| |
| if (mLayoutStateProvider != null) { |
| mLayoutStateProvider.removeObserver(mLayoutStateObserver); |
| } |
| |
| mTabModelSelector.removeObserver(mTabModelSelectorObserver); |
| mBrowserControlsStateProvider.removeObserver(mBrowserControlsObserver); |
| mTabModelSelector |
| .getTabModelFilterProvider() |
| .removeTabModelFilterObserver(mTabModelObserver); |
| } |
| |
| void setOnTabSelectingListener(TabSwitcher.OnTabSelectingListener listener) { |
| mOnTabSelectingListener = listener; |
| } |
| |
| |
| void requestAccessibilityFocusOnCurrentTab() { |
| if (!mIsTabSwitcherShowing || !mTabModelSelector.isTabStateInitialized()) { |
| return; |
| } |
| |
| if (mTabModelSelector.isIncognitoSelected() |
| && mIncognitoReauthController != null |
| && mIncognitoReauthController.isReauthPageShowing()) { |
| return; |
| } |
| |
| mContainerViewModel.set( |
| TabListContainerProperties.FOCUS_TAB_INDEX_FOR_ACCESSIBILITY, |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter().index()); |
| } |
| |
| // GridCardOnClickListenerProvider implementation. |
| @Override |
| @Nullable |
| public TabListMediator.TabActionListener openTabGridDialog(Tab tab) { |
| if (!ableToOpenDialog(tab)) return null; |
| assert isTabInTabGroup(tab); |
| return tabId -> { |
| List<Tab> relatedTabs = getRelatedTabs(tabId); |
| if (relatedTabs.size() == 0) { |
| relatedTabs = null; |
| } |
| mTabGridDialogControllerSupplier.get().resetWithListOfTabs(relatedTabs); |
| RecordUserAction.record("TabGridDialog.ExpandedFromSwitcher"); |
| }; |
| } |
| |
| @Override |
| public void onTabSelecting(int tabId, boolean fromActionButton) { |
| if (fromActionButton && mMode == TabListMode.GRID) { |
| Tab newlySelectedTab = |
| TabModelUtils.getTabById(mTabModelSelector.getCurrentModel(), tabId); |
| StartSurfaceUserData.setKeepTab(newlySelectedTab, true); |
| } |
| mIsSelectingInTabSwitcher = true; |
| if (mOnTabSelectingListener != null) { |
| mShouldIgnoreNextSelect = true; |
| mOnTabSelectingListener.onTabSelecting(tabId); |
| } |
| } |
| |
| @Override |
| public void scrollToTab(int tabIndex) { |
| mContainerViewModel.set(TabListContainerProperties.INITIAL_SCROLL_INDEX, tabIndex); |
| } |
| |
| private boolean ableToOpenDialog(Tab tab) { |
| return mTabModelSelector.isIncognitoSelected() == tab.isIncognito() && isTabInTabGroup(tab); |
| } |
| |
| private TabModelFilter getCurrentTabModelFilter() { |
| return mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(); |
| } |
| |
| private List<Tab> getRelatedTabs(int tabId) { |
| return getCurrentTabModelFilter().getRelatedTabList(tabId); |
| } |
| |
| private boolean isTabInTabGroup(Tab tab) { |
| return getCurrentTabModelFilter().isTabInTabGroup(tab); |
| } |
| |
| private void notifyBackPressStateChanged(boolean noop) { |
| notifyBackPressStateChangedInternal(); |
| } |
| |
| private void notifyBackPressStateChangedInternal() { |
| mIsDialogVisibleSupplier.set(isDialogVisible()); |
| mBackPressChangedSupplier.set(shouldInterceptBackPress()); |
| } |
| |
| private int getToolbarHeight() { |
| return mContext.getResources().getDimensionPixelSize(R.dimen.toolbar_height_no_shadow); |
| } |
| |
| /** |
| * A method to indicate whether back press should be intercepted. The respective interceptors |
| * should also take care of invoking #notifyBackPressStateChangedInternal each time their |
| * decision to intercept back press has changed. |
| * |
| * @return A boolean to indicate if back press should be intercepted or not. |
| */ |
| @VisibleForTesting |
| boolean shouldInterceptBackPress() { |
| if (isDialogVisible()) return true; |
| if (mCustomViewBackPressRunnable != null) return true; |
| |
| if (!mIsTablet && mIsTransitionInProgress && mMode == TabListCoordinator.TabListMode.GRID) { |
| return true; |
| } |
| |
| if (!mContainerViewModel.get(IS_VISIBLE)) return false; |
| |
| if (mTabModelSelector.getCurrentTab() == null) return false; |
| |
| // Going back to the Start surface isn't handled by the TabSwitcherMediator any more, but in |
| // {@link ReturnToChromeBackPressHandler} when it isn't in incognito mode. |
| if (mLastActiveLayoutType == LayoutType.START_SURFACE |
| && !mTabModelSelector.isIncognitoSelected()) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * A method which clears the Incognito tab lists when a re-auth is pending. |
| * |
| * @return True, if the Incognito tab list was requested to be cleared and false otherwise. |
| */ |
| private boolean clearIncognitoTabListForReauth() { |
| if (!mTabModelSelector.isIncognitoSelected()) return false; |
| |
| if (mIncognitoReauthController != null |
| && mIncognitoReauthController.isIncognitoReauthPending()) { |
| mResetHandler.resetWithTabList(null, false); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void onLayoutStateProviderAvailable(LayoutStateProvider layoutStateProvider) { |
| mLayoutStateProvider = layoutStateProvider; |
| mLastActiveLayoutType = mLayoutStateProvider.getActiveLayoutType(); |
| if (mLayoutStateObserver == null) { |
| mLayoutStateObserver = |
| new LayoutStateObserver() { |
| @Override |
| public void onFinishedHiding(int layoutType) { |
| mLastActiveLayoutType = layoutType; |
| } |
| |
| @Override |
| public void onStartedShowing(int layoutType) { |
| if (layoutType == LayoutType.TAB_SWITCHER) { |
| mIsTransitionInProgress = true; |
| notifyBackPressStateChangedInternal(); |
| } |
| } |
| }; |
| } |
| mLayoutStateProvider.addObserver(mLayoutStateObserver); |
| } |
| |
| public void setLastActiveLayoutTypeForTesting(@LayoutType int lastActiveLayoutType) { |
| mLastActiveLayoutType = lastActiveLayoutType; |
| } |
| |
| private TabListEditorController getTabListEditorController() { |
| return mTabListEditorControllerSupplier == null |
| ? null |
| : mTabListEditorControllerSupplier.get(); |
| } |
| |
| /** |
| * Refresh the tab switcher's tab list and perform an out-of-band update on the UI. If the tab |
| * switcher is not visible, this will no-op. |
| */ |
| public void refreshTabList() { |
| if (!mContainerViewModel.get(IS_VISIBLE)) return; |
| |
| mResetHandler.resetWithTabList( |
| mTabModelSelector.getTabModelFilterProvider().getCurrentTabModelFilter(), false); |
| } |
| } |