chromium/chrome/browser/ui/android/edge_to_edge/internal/java/src/org/chromium/chrome/browser/ui/edge_to_edge/EdgeToEdgeBottomChinMediator.java

// Copyright 2024 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.edge_to_edge;

import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinProperties.COLOR;
import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinProperties.DIVIDER_COLOR;
import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinProperties.HEIGHT;
import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinProperties.IS_VISIBLE;
import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinProperties.Y_OFFSET;
import static org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeUtils.isBottomChinAllowed;

import androidx.annotation.NonNull;

import org.chromium.chrome.browser.browser_controls.BottomControlsLayer;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker.LayerScrollBehavior;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker.LayerType;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker.LayerVisibility;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.fullscreen.FullscreenOptions;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.modelutil.PropertyModel;

class EdgeToEdgeBottomChinMediator
        implements LayoutStateProvider.LayoutStateObserver,
                KeyboardVisibilityDelegate.KeyboardVisibilityListener,
                EdgeToEdgeSupplier.ChangeObserver,
                NavigationBarColorProvider.Observer,
                FullscreenManager.Observer,
                BottomControlsLayer {
    private static final String TAG = "E2EBottomChin";

    /** The model for the bottom controls component that holds all of its view state. */
    private final PropertyModel mModel;

    private int mEdgeToEdgeBottomInsetDp;
    private int mEdgeToEdgeBottomInsetPx;
    private boolean mIsDrawingToEdge;
    private boolean mIsPagedOptedIntoEdgeToEdge;

    private boolean mIsKeyboardVisible;

    private final @NonNull KeyboardVisibilityDelegate mKeyboardVisibilityDelegate;
    private final @NonNull LayoutManager mLayoutManager;
    private final @NonNull EdgeToEdgeController mEdgeToEdgeController;
    private final @NonNull NavigationBarColorProvider mNavigationBarColorProvider;
    private final @NonNull BottomControlsStacker mBottomControlsStacker;
    private final @NonNull FullscreenManager mFullscreenManager;

    /**
     * Build a new mediator for the bottom chin component.
     *
     * @param model The {@link EdgeToEdgeBottomChinProperties} that holds all the view state for the
     *     bottom chin component.
     * @param keyboardVisibilityDelegate A {@link KeyboardVisibilityDelegate} for watching keyboard
     *     visibility events.
     * @param layoutManager The {@link LayoutManager} for observing active layout type.
     * @param edgeToEdgeController The {@link EdgeToEdgeController} for observing the edge-to-edge
     *     status and window bottom insets.
     * @param navigationBarColorProvider The {@link NavigationBarColorProvider} for observing the
     *     color for the navigation bar.
     * @param bottomControlsStacker The {@link BottomControlsStacker} for observing and changing
     *     browser controls heights.
     * @param fullscreenManager The {@link FullscreenManager} for provide the fullscreen state.
     */
    EdgeToEdgeBottomChinMediator(
            PropertyModel model,
            @NonNull KeyboardVisibilityDelegate keyboardVisibilityDelegate,
            @NonNull LayoutManager layoutManager,
            @NonNull EdgeToEdgeController edgeToEdgeController,
            @NonNull NavigationBarColorProvider navigationBarColorProvider,
            @NonNull BottomControlsStacker bottomControlsStacker,
            @NonNull FullscreenManager fullscreenManager) {
        mModel = model;
        mKeyboardVisibilityDelegate = keyboardVisibilityDelegate;
        mLayoutManager = layoutManager;
        mEdgeToEdgeController = edgeToEdgeController;
        mNavigationBarColorProvider = navigationBarColorProvider;
        mBottomControlsStacker = bottomControlsStacker;
        mFullscreenManager = fullscreenManager;

        // Add observers.
        mKeyboardVisibilityDelegate.addKeyboardVisibilityListener(this);
        mLayoutManager.addObserver(this);
        mEdgeToEdgeController.registerObserver(this);
        mNavigationBarColorProvider.addObserver(this);
        mBottomControlsStacker.addLayer(this);
        mFullscreenManager.addObserver(this);

        // Initialize model with appropriate values.
        mModel.set(Y_OFFSET, 0);
        mModel.set(COLOR, mNavigationBarColorProvider.getNavigationBarColor());

        // Call observer methods to trigger initial value.
        onToEdgeChange(
                mEdgeToEdgeController.getBottomInsetPx(),
                mEdgeToEdgeController.isDrawingToEdge(),
                mEdgeToEdgeController.isPageOptedIntoEdgeToEdge());
        updateHeightAndVisibility();
    }

    void destroy() {
        assert mKeyboardVisibilityDelegate != null;
        assert mLayoutManager != null;
        assert mEdgeToEdgeController != null;
        assert mNavigationBarColorProvider != null;
        assert mBottomControlsStacker != null;
        assert mFullscreenManager != null;

        mKeyboardVisibilityDelegate.removeKeyboardVisibilityListener(this);
        mLayoutManager.removeObserver(this);
        mEdgeToEdgeController.unregisterObserver(this);
        mNavigationBarColorProvider.removeObserver(this);
        mBottomControlsStacker.removeLayer(this);
        mFullscreenManager.removeObserver(this);
    }

    /**
     * Updates the height and visibility for the bottom chin. If either of these changes, that will
     * affect how the bottom chin interacts with the bottom controls, so a layer update will be
     * requested - unifying height and visibility updates in a single method avoids potential
     * redundant layer update requests.
     */
    private void updateHeightAndVisibility() {
        int newHeight = mEdgeToEdgeBottomInsetPx;
        boolean newVisibility =
                mIsDrawingToEdge
                        && !mIsPagedOptedIntoEdgeToEdge
                        && isBottomChinAllowed(
                                mLayoutManager.getActiveLayoutType(), mEdgeToEdgeBottomInsetDp)
                        && !mFullscreenManager.getPersistentFullscreenMode()
                        && !mIsKeyboardVisible;

        boolean heightChanged = mModel.get(HEIGHT) != newHeight;
        boolean visibilityChanged = mModel.get(IS_VISIBLE) != newVisibility;

        if (heightChanged) mModel.set(HEIGHT, newHeight);
        if (visibilityChanged) mModel.set(IS_VISIBLE, newVisibility);
        if (heightChanged || visibilityChanged) {
            mBottomControlsStacker.requestLayerUpdate(false);
        }
    }

    // LayoutStateProvider.LayoutStateObserver

    @Override
    public void onStartedShowing(int layoutType) {
        updateHeightAndVisibility();
    }

    // EdgeToEdgeSupplier.ChangeObserver

    @Override
    public void onToEdgeChange(
            int bottomInset, boolean isDrawingToEdge, boolean isPageOptInToEdge) {
        if (mEdgeToEdgeBottomInsetDp == bottomInset
                && mIsDrawingToEdge == isDrawingToEdge
                && mIsPagedOptedIntoEdgeToEdge == isPageOptInToEdge) {
            return;
        }

        mEdgeToEdgeBottomInsetDp = bottomInset;
        mEdgeToEdgeBottomInsetPx = mEdgeToEdgeController.getBottomInsetPx();
        mIsDrawingToEdge = isDrawingToEdge;
        mIsPagedOptedIntoEdgeToEdge = isPageOptInToEdge;
        updateHeightAndVisibility();
    }

    @Override
    public void onNavigationBarColorChanged(int color) {
        // TODO(): Animate the color change.
        mModel.set(COLOR, color);
    }

    @Override
    public void onNavigationBarDividerChanged(int dividerColor) {
        mModel.set(DIVIDER_COLOR, dividerColor);
    }

    // KeyboardVisibilityDelegate.KeyboardVisibilityListener

    @Override
    public void keyboardVisibilityChanged(boolean isShowing) {
        mIsKeyboardVisible = isShowing;
        updateHeightAndVisibility();
    }

    // FullscreenManager.Observer

    @Override
    public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
        updateHeightAndVisibility();
    }

    @Override
    public void onExitFullscreen(Tab tab) {
        updateHeightAndVisibility();
    }

    // BottomControlsLayer

    @Override
    public int getType() {
        return LayerType.BOTTOM_CHIN;
    }

    @Override
    public int getScrollBehavior() {
        return LayerScrollBehavior.DEFAULT_SCROLL_OFF;
    }

    @Override
    public int getHeight() {
        return mModel.get(HEIGHT);
    }

    @Override
    public @LayerVisibility int getLayerVisibility() {
        return mModel.get(IS_VISIBLE)
                ? LayerVisibility.VISIBLE
                : LayerVisibility.VISIBLE_IF_OTHERS_VISIBLE;
    }

    @Override
    public void onBrowserControlsOffsetUpdate(int layerYOffset) {
        assert BottomControlsStacker.isDispatchingYOffset();
        mModel.set(Y_OFFSET, layerYOffset);
    }
}