chromium/chrome/android/java/src/org/chromium/chrome/browser/ui/BottomSheetManager.java

// 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.ui;

import org.chromium.base.Callback;
import org.chromium.base.CallbackController;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
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.lifecycle.DestroyObserver;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabObserver;
import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.ExpandedSheetHelper;
import org.chromium.components.browser_ui.bottomsheet.ManagedBottomSheetController;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.url.GURL;

/**
 * A class that manages activity-specific interactions with the BottomSheet component that it
 * otherwise shouldn't know about.
 */
class BottomSheetManager extends EmptyBottomSheetObserver implements DestroyObserver {
    /** A means of accessing the focus state of the omibox. */
    private final ObservableSupplier<Boolean> mOmniboxFocusStateSupplier;

    /** An observer of the omnibox that suppresses the sheet when the omnibox is focused. */
    private final Callback<Boolean> mOmniboxFocusObserver;

    /** A listener for browser controls offset changes. */
    private final BrowserControlsVisibilityManager.Observer mBrowserControlsObserver;

    /** A tab observer that is only attached to the active tab. */
    private final TabObserver mTabObserver;

    private final CallbackController mCallbackController;

    private final OneshotSupplier<LayoutStateProvider> mLayoutStateProviderSupplier;
    private LayoutStateProvider.LayoutStateObserver mLayoutStateObserver;

    private final ExpandedSheetHelper mExpandedSheetHelper;

    /** A browser controls manager for polling browser controls offsets. */
    private BrowserControlsVisibilityManager mBrowserControlsVisibilityManager;

    /**
     * A handle to the {@link ManagedBottomSheetController} this class manages interactions with.
     */
    private ManagedBottomSheetController mSheetController;

    /** A mechanism for accessing the currently active tab. */
    private ActivityTabProvider mTabProvider;

    /** A supplier of a snackbar manager for the bottom sheet. */
    private Supplier<SnackbarManager> mSnackbarManager;

    /** The manager for overlay panels to attach listeners to. */
    private Supplier<OverlayPanelManager> mOverlayPanelManager;

    /** The last known activity tab, if available. */
    private Tab mLastActivityTab;

    /**
     * Used to track whether the active content has a custom scrim lifecycle. This is kept here
     * because there are some instances where the active content is changed prior to the close event
     * being called.
     */
    private boolean mContentHasCustomScrimLifecycle;

    /** The token used to enable browser controls persistence. */
    private int mPersistentControlsToken;

    public BottomSheetManager(
            ManagedBottomSheetController controller,
            ActivityTabProvider tabProvider,
            BrowserControlsVisibilityManager controlsVisibilityManager,
            ExpandedSheetHelper expandedSheetHelper,
            Supplier<SnackbarManager> snackbarManagerSupplier,
            ObservableSupplier<Boolean> omniboxFocusStateSupplier,
            Supplier<OverlayPanelManager> overlayManager,
            OneshotSupplier<LayoutStateProvider> layoutStateProviderSupplier) {
        mSheetController = controller;
        mTabProvider = tabProvider;
        mBrowserControlsVisibilityManager = controlsVisibilityManager;
        mSnackbarManager = snackbarManagerSupplier;
        mOmniboxFocusStateSupplier = omniboxFocusStateSupplier;
        mOverlayPanelManager = overlayManager;
        mCallbackController = new CallbackController();
        mExpandedSheetHelper = expandedSheetHelper;

        mLayoutStateProviderSupplier = layoutStateProviderSupplier;
        mLayoutStateProviderSupplier.onAvailable(
                mCallbackController.makeCancelable(this::addLayoutStateObserver));

        mSheetController.addObserver(this);

        // TODO(crbug.com/40134698): We should wait to instantiate all of these observers until the
        // bottom
        //                sheet is actually used.
        mTabObserver =
                new EmptyTabObserver() {
                    @Override
                    public void onPageLoadStarted(Tab tab, GURL url) {
                        controller.clearRequestsAndHide();
                    }

                    @Override
                    public void onCrash(Tab tab) {
                        controller.clearRequestsAndHide();
                    }

                    @Override
                    public void onDestroyed(Tab tab) {
                        if (mLastActivityTab != tab) return;
                        mLastActivityTab = null;

                        // Remove the suppressed sheet if its lifecycle is tied to the tab being
                        // destroyed.
                        controller.clearRequestsAndHide();
                    }
                };

        mTabProvider.addObserver(this::setActivityTab);
        setActivityTab(mTabProvider.get());

        mBrowserControlsObserver =
                new BrowserControlsVisibilityManager.Observer() {
                    @Override
                    public void onControlsOffsetChanged(
                            int topOffset,
                            int topControlsMinHeightOffset,
                            int bottomOffset,
                            int bottomControlsMinHeightOffset,
                            boolean needsAnimate,
                            boolean isVisibilityForced) {
                        controller.setBrowserControlsHiddenRatio(
                                mBrowserControlsVisibilityManager.getBrowserControlHiddenRatio());
                    }
                };
        mBrowserControlsVisibilityManager.addObserver(mBrowserControlsObserver);

        mOmniboxFocusObserver =
                new Callback<Boolean>() {
                    /** A token held while this object is suppressing the bottom sheet. */
                    private int mToken;

                    @Override
                    public void onResult(Boolean focused) {
                        if (focused) {
                            mToken =
                                    controller.suppressSheet(
                                            BottomSheetController.StateChangeReason.NONE);
                        } else {
                            controller.unsuppressSheet(mToken);
                        }
                    }
                };
        mOmniboxFocusStateSupplier.addObserver(mOmniboxFocusObserver);
    }

    private void setActivityTab(Tab tab) {
        if (tab == null) return;

        if (mLastActivityTab == tab) return;

        // Move the observer to the new activity tab and clear the sheet.
        if (mLastActivityTab != null) mLastActivityTab.removeObserver(mTabObserver);
        mLastActivityTab = tab;
        mLastActivityTab.addObserver(mTabObserver);
        mSheetController.clearRequestsAndHide();
    }

    private void addLayoutStateObserver(LayoutStateProvider layoutStateProvider) {
        mLayoutStateObserver =
                new LayoutStateObserver() {
                    // On switching to a new layout act as though this is a tab switch by clearing
                    // all state. Use onStartedHiding to avoid the bottom sheet being visible
                    // during the transition if there is one.
                    @Override
                    public void onStartedHiding(int layoutType) {
                        if (layoutType != LayoutType.SIMPLE_ANIMATION) {
                            mSheetController.clearRequestsAndHide();
                        }
                    }
                };

        layoutStateProvider.addObserver(mLayoutStateObserver);
    }

    @Override
    public void onSheetOpened(int reason) {
        if (mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate() != null) {
            // Browser controls should stay visible until the sheet is closed.
            mPersistentControlsToken =
                    mBrowserControlsVisibilityManager
                            .getBrowserVisibilityDelegate()
                            .showControlsPersistent();
        }

        Tab activeTab = mTabProvider.get();
        if (activeTab != null) {
            WebContents webContents = activeTab.getWebContents();
            if (webContents != null) {
                SelectionPopupController.fromWebContents(webContents).clearSelection();
            }
        }

        if (mOverlayPanelManager.hasValue()
                && mOverlayPanelManager.get().getActivePanel() != null) {
            mOverlayPanelManager
                    .get()
                    .getActivePanel()
                    .closePanel(OverlayPanel.StateChangeReason.UNKNOWN, true);
        }

        BottomSheetContent content = mSheetController.getCurrentSheetContent();
        // Content with a custom scrim lifecycle should not obscure the tab. The feature
        // is responsible for adding itself to the list of obscuring views when applicable.
        if (content != null && content.hasCustomScrimLifecycle()) {
            mContentHasCustomScrimLifecycle = true;
            return;
        }

        mExpandedSheetHelper.onSheetExpanded();
    }

    @Override
    public void onSheetClosed(int reason) {
        if (mBrowserControlsVisibilityManager.getBrowserVisibilityDelegate() != null) {
            // Update the browser controls since they are permanently shown while the sheet is
            // open.
            mBrowserControlsVisibilityManager
                    .getBrowserVisibilityDelegate()
                    .releasePersistentShowingToken(mPersistentControlsToken);
        }

        BottomSheetContent content = mSheetController.getCurrentSheetContent();
        // If the content has a custom scrim, it wasn't obscuring tabs.
        if (mContentHasCustomScrimLifecycle) {
            mContentHasCustomScrimLifecycle = false;
            return;
        }

        mExpandedSheetHelper.onSheetCollapsed();
    }

    @Override
    public void onSheetOffsetChanged(float heightFraction, float offsetPx) {
        if (mSnackbarManager.get() != null) {
            mSnackbarManager.get().dismissAllSnackbars();
        }
    }

    @Override
    public void onDestroy() {
        mCallbackController.destroy();
        if (mLastActivityTab != null) mLastActivityTab.removeObserver(mTabObserver);
        mSheetController.removeObserver(this);
        mBrowserControlsVisibilityManager.removeObserver(mBrowserControlsObserver);
        mOmniboxFocusStateSupplier.removeObserver(mOmniboxFocusObserver);
        if (mLayoutStateProviderSupplier.get() != null) {
            mLayoutStateProviderSupplier.get().removeObserver(mLayoutStateObserver);
        }
    }
}