| // 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.tabbed_mode; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| |
| import org.chromium.base.Callback; |
| import org.chromium.base.TraceEvent; |
| import org.chromium.base.supplier.ObservableSupplier; |
| import org.chromium.base.supplier.ObservableSupplierImpl; |
| import org.chromium.base.supplier.Supplier; |
| import org.chromium.base.task.PostTask; |
| import org.chromium.chrome.R; |
| import org.chromium.chrome.browser.ActivityTabProvider; |
| import org.chromium.chrome.browser.ChromeActivity; |
| import org.chromium.chrome.browser.bookmarks.BookmarkBridge; |
| import org.chromium.chrome.browser.browser_controls.BrowserControlsSizer; |
| import org.chromium.chrome.browser.compositor.bottombar.ephemeraltab.EphemeralTabCoordinator; |
| import org.chromium.chrome.browser.compositor.layouts.LayoutManager; |
| import org.chromium.chrome.browser.compositor.layouts.OverviewModeBehavior; |
| import org.chromium.chrome.browser.contextualsearch.ContextualSearchManager; |
| import org.chromium.chrome.browser.datareduction.DataReductionPromoScreen; |
| import org.chromium.chrome.browser.firstrun.FirstRunStatus; |
| import org.chromium.chrome.browser.flags.ChromeFeatureList; |
| import org.chromium.chrome.browser.gesturenav.HistoryNavigationCoordinator; |
| import org.chromium.chrome.browser.gesturenav.NavigationSheet; |
| import org.chromium.chrome.browser.gesturenav.TabbedSheetDelegate; |
| import org.chromium.chrome.browser.history.HistoryManagerUtils; |
| import org.chromium.chrome.browser.language.LanguageAskPrompt; |
| import org.chromium.chrome.browser.lifecycle.NativeInitObserver; |
| import org.chromium.chrome.browser.locale.LocaleManager; |
| import org.chromium.chrome.browser.multiwindow.MultiWindowUtils; |
| import org.chromium.chrome.browser.offlinepages.indicator.OfflineIndicatorControllerV2; |
| import org.chromium.chrome.browser.omnibox.UrlFocusChangeListener; |
| import org.chromium.chrome.browser.preferences.ChromePreferenceKeys; |
| import org.chromium.chrome.browser.preferences.SharedPreferencesManager; |
| import org.chromium.chrome.browser.profiles.Profile; |
| import org.chromium.chrome.browser.share.ShareDelegate; |
| import org.chromium.chrome.browser.signin.SigninPromoUtil; |
| import org.chromium.chrome.browser.status_indicator.StatusIndicatorCoordinator; |
| import org.chromium.chrome.browser.tab.Tab; |
| import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities; |
| import org.chromium.chrome.browser.toolbar.ToolbarButtonInProductHelpController; |
| import org.chromium.chrome.browser.toolbar.bottom.BottomToolbarConfiguration; |
| import org.chromium.chrome.browser.ui.RootUiCoordinator; |
| import org.chromium.chrome.browser.ui.appmenu.AppMenuHandler; |
| import org.chromium.chrome.browser.ui.default_browser_promo.DefaultBrowserPromoUtils; |
| import org.chromium.chrome.browser.ui.tablet.emptybackground.EmptyBackgroundViewWrapper; |
| import org.chromium.chrome.browser.vr.VrModuleProvider; |
| import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver; |
| import org.chromium.content_public.browser.UiThreadTaskTraits; |
| import org.chromium.ui.base.DeviceFormFactor; |
| import org.chromium.ui.base.WindowAndroid; |
| |
| /** |
| * A {@link RootUiCoordinator} variant that controls tabbed-mode specific UI. |
| */ |
| public class TabbedRootUiCoordinator extends RootUiCoordinator implements NativeInitObserver { |
| private static boolean sEnableStatusIndicatorForTests; |
| |
| private final ObservableSupplierImpl<EphemeralTabCoordinator> mEphemeralTabCoordinatorSupplier; |
| private TabbedSystemUiCoordinator mSystemUiCoordinator; |
| private @Nullable EmptyBackgroundViewWrapper mEmptyBackgroundViewWrapper; |
| |
| private StatusIndicatorCoordinator mStatusIndicatorCoordinator; |
| private StatusIndicatorCoordinator.StatusIndicatorObserver mStatusIndicatorObserver; |
| private OfflineIndicatorControllerV2 mOfflineIndicatorController; |
| private UrlFocusChangeListener mUrlFocusChangeListener; |
| private @Nullable ToolbarButtonInProductHelpController mToolbarButtonInProductHelpController; |
| private boolean mIntentWithEffect; |
| private HistoryNavigationCoordinator mHistoryNavigationCoordinator; |
| private NavigationSheet mNavigationSheet; |
| |
| /** |
| * Construct a new TabbedRootUiCoordinator. |
| * @param activity The activity whose UI the coordinator is responsible for. |
| * @param onOmniboxFocusChangedListener callback to invoke when Omnibox focus |
| * changes. |
| * @param intentWithEffect Whether or not {@code activity} was launched with an |
| * intent to open a single tab. |
| * @param shareDelegateSupplier |
| * @param tabProvider The {@link ActivityTabProvider} to get current tab of the activity. |
| * @param profileSupplier Supplier of the currently applicable profile. |
| * @param bookmarkBridgeSupplier Supplier of the bookmark bridge for the current profile. |
| * @param overviewModeBehaviorSupplier Supplier of the overview mode manager. |
| * @param contextualSearchManagerSupplier Supplier of the {@link ContextualSearchManager}. |
| */ |
| public TabbedRootUiCoordinator(ChromeActivity activity, |
| Callback<Boolean> onOmniboxFocusChangedListener, boolean intentWithEffect, |
| ObservableSupplier<ShareDelegate> shareDelegateSupplier, |
| ActivityTabProvider tabProvider, |
| ObservableSupplierImpl<EphemeralTabCoordinator> ephemeralTabCoordinatorSupplier, |
| ObservableSupplier<Profile> profileSupplier, |
| ObservableSupplier<BookmarkBridge> bookmarkBridgeSupplier, |
| ObservableSupplier<OverviewModeBehavior> overviewModeBehaviorSupplier, |
| Supplier<ContextualSearchManager> contextualSearchManagerSupplier) { |
| super(activity, onOmniboxFocusChangedListener, shareDelegateSupplier, tabProvider, |
| profileSupplier, bookmarkBridgeSupplier, overviewModeBehaviorSupplier, |
| contextualSearchManagerSupplier); |
| mIntentWithEffect = intentWithEffect; |
| mEphemeralTabCoordinatorSupplier = ephemeralTabCoordinatorSupplier; |
| mCanAnimateBrowserControls = () -> { |
| // These null checks prevent any exceptions that may be caused by callbacks after |
| // destruction. |
| if (mActivity == null || mActivity.getActivityTabProvider() == null) return false; |
| final Tab tab = mActivity.getActivityTabProvider().get(); |
| return tab != null && tab.isUserInteractable() && !tab.isNativePage(); |
| }; |
| } |
| |
| @Override |
| public void destroy() { |
| if (mSystemUiCoordinator != null) mSystemUiCoordinator.destroy(); |
| if (mEmptyBackgroundViewWrapper != null) mEmptyBackgroundViewWrapper.destroy(); |
| |
| if (mOfflineIndicatorController != null) { |
| mOfflineIndicatorController.destroy(); |
| } |
| |
| if (mToolbarManager != null) { |
| mToolbarManager.getFakeboxDelegate().removeUrlFocusChangeListener( |
| mUrlFocusChangeListener); |
| } |
| |
| if (mStatusIndicatorCoordinator != null) { |
| mStatusIndicatorCoordinator.removeObserver(mStatusIndicatorObserver); |
| mStatusIndicatorCoordinator.removeObserver(mActivity.getStatusBarColorController()); |
| mStatusIndicatorCoordinator.destroy(); |
| } |
| |
| if (mToolbarButtonInProductHelpController != null) { |
| mToolbarButtonInProductHelpController.destroy(); |
| } |
| |
| if (mHistoryNavigationCoordinator != null) { |
| mHistoryNavigationCoordinator.destroy(); |
| mHistoryNavigationCoordinator = null; |
| } |
| super.destroy(); |
| } |
| |
| @Override |
| public void onPostInflationStartup() { |
| super.onPostInflationStartup(); |
| |
| mSystemUiCoordinator = new TabbedSystemUiCoordinator(mActivity.getWindow(), |
| mActivity.getTabModelSelector(), mActivity.getOverviewModeBehaviorSupplier()); |
| } |
| |
| @Override |
| protected void onFindToolbarShown() { |
| super.onFindToolbarShown(); |
| EphemeralTabCoordinator coordinator = mEphemeralTabCoordinatorSupplier.get(); |
| if (coordinator != null && coordinator.isOpened()) coordinator.close(); |
| } |
| |
| /** |
| * @return The toolbar button IPH controller for the tabbed UI this coordinator controls. |
| * TODO(pnoland, https://crbug.com/865801): remove this in favor of wiring it directly. |
| */ |
| public ToolbarButtonInProductHelpController getToolbarButtonInProductHelpController() { |
| return mToolbarButtonInProductHelpController; |
| } |
| |
| /** |
| * Show navigation history sheet. |
| */ |
| public void showFullHistorySheet() { |
| Tab tab = mActivity.getActivityTabProvider().get(); |
| if (tab == null || tab.getWebContents() == null || !tab.isUserInteractable()) return; |
| mNavigationSheet = NavigationSheet.create( |
| mActivity.getWindow().getDecorView().findViewById(android.R.id.content), mActivity, |
| this::getBottomSheetController); |
| mNavigationSheet.setDelegate(new TabbedSheetDelegate(tab, aTab -> { |
| HistoryManagerUtils.showHistoryManager(mActivity, aTab); |
| }, mActivity.getResources().getString(R.string.show_full_history))); |
| if (!mNavigationSheet.startAndExpand(/* forward=*/false, /* animate=*/true)) { |
| mNavigationSheet = null; |
| } else { |
| getBottomSheetController().addObserver(new EmptyBottomSheetObserver() { |
| @Override |
| public void onSheetClosed(int reason) { |
| getBottomSheetController().removeObserver(this); |
| mNavigationSheet = null; |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onFinishNativeInitialization() { |
| // TODO(twellington): Supply TabModelSelector as well and move initialization earlier. |
| if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity)) { |
| AppMenuHandler appMenuHandler = |
| mAppMenuCoordinator == null ? null : mAppMenuCoordinator.getAppMenuHandler(); |
| mEmptyBackgroundViewWrapper = new EmptyBackgroundViewWrapper( |
| mActivity.getTabModelSelector(), mActivity.getTabCreator(false), mActivity, |
| appMenuHandler, mActivity.getSnackbarManager(), |
| mActivity.getOverviewModeBehaviorSupplier()); |
| mEmptyBackgroundViewWrapper.initialize(); |
| } |
| |
| if (EphemeralTabCoordinator.isSupported()) { |
| mEphemeralTabCoordinatorSupplier.set(new EphemeralTabCoordinator(mActivity, |
| mActivity.getWindowAndroid(), mActivity.getWindow().getDecorView(), |
| mActivity.getActivityTabProvider(), mActivity::getCurrentTabCreator, |
| mActivity.getBottomSheetController(), true)); |
| } |
| |
| PostTask.postTask(UiThreadTaskTraits.DEFAULT, this::initializeIPH); |
| } |
| |
| // Protected class methods |
| |
| @Override |
| protected void onLayoutManagerAvailable(LayoutManager layoutManager) { |
| super.onLayoutManagerAvailable(layoutManager); |
| |
| initStatusIndicatorCoordinator(layoutManager); |
| |
| // clang-format off |
| mHistoryNavigationCoordinator = HistoryNavigationCoordinator.create( |
| mActivity.getLifecycleDispatcher(), mActivity.getCompositorViewHolder(), |
| mActivity.getActivityTabProvider(), mActivity.getInsetObserverView(), |
| mActivity::backShouldCloseTab, mActivity::onBackPressed, |
| tab -> HistoryManagerUtils.showHistoryManager(mActivity, tab), |
| mActivity.getResources().getString(R.string.show_full_history), |
| () -> mActivity.isActivityFinishingOrDestroyed() ? null |
| : getBottomSheetController()); |
| // clang-format on |
| } |
| |
| @Override |
| protected void initializeToolbar() { |
| super.initializeToolbar(); |
| if (!mActivity.isTablet() |
| && (BottomToolbarConfiguration.isBottomToolbarEnabled() |
| || TabUiFeatureUtilities.isTabGroupsAndroidEnabled() |
| || TabUiFeatureUtilities.isConditionalTabStripEnabled())) { |
| getToolbarManager().enableBottomToolbar(); |
| } |
| } |
| |
| @Override |
| protected boolean shouldShowMenuUpdateBadge() { |
| return true; |
| } |
| |
| // Private class methods |
| |
| private void initializeIPH() { |
| WindowAndroid window = mActivity.getWindowAndroid(); |
| mToolbarButtonInProductHelpController = |
| new ToolbarButtonInProductHelpController(mActivity, mAppMenuCoordinator, |
| mActivity.getLifecycleDispatcher(), mActivity.getActivityTabProvider()); |
| if (!triggerPromo()) { |
| mToolbarButtonInProductHelpController.showColdStartIPH(); |
| } |
| } |
| |
| private void initStatusIndicatorCoordinator(LayoutManager layoutManager) { |
| // TODO(crbug.com/1035584): Disable on tablets for now as we need to do one or two extra |
| // things for tablets. |
| if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(mActivity) |
| || (!ChromeFeatureList.isEnabled(ChromeFeatureList.OFFLINE_INDICATOR_V2) |
| && !sEnableStatusIndicatorForTests)) { |
| return; |
| } |
| |
| final BrowserControlsSizer browserControlsSizer = mActivity.getBrowserControlsManager(); |
| mStatusIndicatorCoordinator = new StatusIndicatorCoordinator(mActivity, |
| mActivity.getCompositorViewHolder().getResourceManager(), browserControlsSizer, |
| mActivity.getStatusBarColorController()::getStatusBarColorWithoutStatusIndicator, |
| mCanAnimateBrowserControls, layoutManager::requestUpdate); |
| layoutManager.setStatusIndicatorSceneOverlay(mStatusIndicatorCoordinator.getSceneLayer()); |
| mStatusIndicatorObserver = new StatusIndicatorCoordinator.StatusIndicatorObserver() { |
| @Override |
| public void onStatusIndicatorHeightChanged(int indicatorHeight) { |
| final int resourceId = mActivity.getControlContainerHeightResource(); |
| final int topControlsNewHeight = |
| mActivity.getResources().getDimensionPixelSize(resourceId) |
| + indicatorHeight; |
| browserControlsSizer.setAnimateBrowserControlsHeightChanges(true); |
| browserControlsSizer.setTopControlsHeight(topControlsNewHeight, indicatorHeight); |
| browserControlsSizer.setAnimateBrowserControlsHeightChanges(false); |
| } |
| }; |
| mStatusIndicatorCoordinator.addObserver(mStatusIndicatorObserver); |
| mStatusIndicatorCoordinator.addObserver(mActivity.getStatusBarColorController()); |
| |
| // Don't initialize the offline indicator controller if the feature is disabled. |
| if (!ChromeFeatureList.isEnabled(ChromeFeatureList.OFFLINE_INDICATOR_V2)) { |
| return; |
| } |
| |
| ObservableSupplierImpl<Boolean> isUrlBarFocusedSupplier = new ObservableSupplierImpl<>(); |
| isUrlBarFocusedSupplier.set(mToolbarManager.getFakeboxDelegate().isUrlBarFocused()); |
| mUrlFocusChangeListener = new UrlFocusChangeListener() { |
| @Override |
| public void onUrlFocusChange(boolean hasFocus) { |
| // Offline indicator should assume the UrlBar is focused if it's focusing. |
| if (hasFocus) { |
| isUrlBarFocusedSupplier.set(true); |
| } |
| } |
| |
| @Override |
| public void onUrlAnimationFinished(boolean hasFocus) { |
| // Wait for the animation to finish before notifying that UrlBar is unfocused. |
| if (!hasFocus) { |
| isUrlBarFocusedSupplier.set(false); |
| } |
| } |
| }; |
| mOfflineIndicatorController = new OfflineIndicatorControllerV2(mActivity, |
| mStatusIndicatorCoordinator, isUrlBarFocusedSupplier, mCanAnimateBrowserControls); |
| mToolbarManager.getFakeboxDelegate().addUrlFocusChangeListener(mUrlFocusChangeListener); |
| } |
| |
| @VisibleForTesting |
| public StatusIndicatorCoordinator getStatusIndicatorCoordinatorForTesting() { |
| return mStatusIndicatorCoordinator; |
| } |
| |
| @VisibleForTesting |
| public static void setEnableStatusIndicatorForTests(boolean disable) { |
| sEnableStatusIndicatorForTests = disable; |
| } |
| |
| @VisibleForTesting |
| public EphemeralTabCoordinator getEphemeralTabCoordinatorForTesting() { |
| return mEphemeralTabCoordinatorSupplier.get(); |
| } |
| |
| @VisibleForTesting |
| public HistoryNavigationCoordinator getHistoryNavigationCoordinatorForTesting() { |
| return mHistoryNavigationCoordinator; |
| } |
| |
| @VisibleForTesting |
| public NavigationSheet getNavigationSheetForTesting() { |
| return mNavigationSheet; |
| } |
| |
| /** |
| * Triggers the display of an appropriate promo, if any, returning true if a promo is actually |
| * displayed. |
| */ |
| private boolean triggerPromo() { |
| try (TraceEvent e = TraceEvent.scoped("TabbedRootUiCoordinator.triggerPromo")) { |
| SharedPreferencesManager preferenceManager = SharedPreferencesManager.getInstance(); |
| // Promos can only be shown when we start with ACTION_MAIN intent and |
| // after FRE is complete. Native initialization can finish before the FRE flow is |
| // complete, and this will only show promos on the second opportunity. This is |
| // because the FRE is shown on the first opportunity, and we don't want to show such |
| // content back to back. |
| // |
| // TODO(https://crbug.com/865801, pnoland): Unify promo dialog logic and move into a |
| // single PromoDialogCoordinator. |
| boolean isShowingPromo = |
| LocaleManager.getInstance().hasShownSearchEnginePromoThisSession(); |
| // Promo dialogs in multiwindow mode are broken on some devices: |
| // http://crbug.com/354696 |
| boolean isLegacyMultiWindow = |
| MultiWindowUtils.getInstance().isLegacyMultiWindow(mActivity); |
| if (!isShowingPromo && !mIntentWithEffect && FirstRunStatus.getFirstRunFlowComplete() |
| && preferenceManager.readBoolean( |
| ChromePreferenceKeys.PROMOS_SKIPPED_ON_FIRST_START, false) |
| && !VrModuleProvider.getDelegate().isInVr() |
| // VrModuleProvider.getDelegate().isInVr may not return true at this point |
| // even though Chrome is about to enter VR, so we need to also check whether |
| // we're launching into VR. |
| && !VrModuleProvider.getIntentDelegate().isLaunchingIntoVr( |
| mActivity, mActivity.getIntent()) |
| && !isLegacyMultiWindow) { |
| isShowingPromo = maybeShowPromo(); |
| } else { |
| preferenceManager.writeBoolean( |
| ChromePreferenceKeys.PROMOS_SKIPPED_ON_FIRST_START, true); |
| } |
| return isShowingPromo; |
| } |
| } |
| |
| private boolean maybeShowPromo() { |
| // Only one promo can be shown in one run to avoid nagging users too much. |
| if (SigninPromoUtil.launchSigninPromoIfNeeded(mActivity)) return true; |
| if (DataReductionPromoScreen.launchDataReductionPromo( |
| mActivity, mActivity.getTabModelSelector().getCurrentModel().isIncognito())) { |
| return true; |
| } |
| if (DefaultBrowserPromoUtils.prepareLaunchPromoIfNeeded( |
| mActivity, mActivity.getLifecycleDispatcher(), mActivity.getWindowAndroid())) { |
| return true; |
| } |
| |
| return LanguageAskPrompt.maybeShowLanguageAskPrompt(mActivity); |
| } |
| } |