chromium/chrome/browser/ui/android/toolbar/java/src/org/chromium/chrome/browser/toolbar/bottom/BottomControlsCoordinator.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.toolbar.bottom;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.ColorInt;

import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.OneshotSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.supplier.TransitiveObservableSupplier;
import org.chromium.chrome.browser.browser_controls.BottomControlsStacker;
import org.chromium.chrome.browser.browser_controls.BrowserStateBrowserControlsVisibilityDelegate;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.tab.TabObscuringHandler;
import org.chromium.chrome.browser.toolbar.R;
import org.chromium.chrome.browser.toolbar.bottom.BottomControlsViewBinder.ViewHolder;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeController;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;
import org.chromium.ui.widget.Toast;

/**
 * The root coordinator for the bottom controls component. This component is intended for use with
 * bottom UI that re-sizes the web contents, scrolls off-screen, and hides when the keyboard is
 * shown. This class has two primary components, an Android view and a composited texture that draws
 * when the controls are being scrolled off-screen. The Android version does not draw unless the
 * controls offset is 0.
 */
public class BottomControlsCoordinator implements BackPressHandler {
    /** Interface for the BottomControls component to hide and show itself. */
    public interface BottomControlsVisibilityController {
        void setBottomControlsVisible(boolean isVisible);

        void setBottomControlsColor(@ColorInt int color);
    }

    /** The mediator that handles events from outside the bottom controls. */
    private final BottomControlsMediator mMediator;

    /** The Delegate for the split toolbar's bottom toolbar component UI operation. */
    private final OneshotSupplier<BottomControlsContentDelegate> mContentDelegateSupplier;

    private final ObservableSupplierImpl<BottomControlsContentDelegate> mContentDelegateWrapper =
            new ObservableSupplierImpl<>();
    private final TransitiveObservableSupplier<BottomControlsContentDelegate, Boolean>
            mHandleBackPressChangedSupplier =
                    new TransitiveObservableSupplier<>(
                            mContentDelegateWrapper, cd -> cd.getHandleBackPressChangedSupplier());

    private final ScrollingBottomViewResourceFrameLayout mRootFrameLayout;
    private final ScrollingBottomViewSceneLayer mSceneLayer;

    /**
     * Build the coordinator that manages the bottom controls.
     *
     * @param activity Activity instance to use.
     * @param windowAndroid A {@link WindowAndroid} for watching keyboard visibility events.
     * @param layoutManager A {@link LayoutManager} to attach overlays to.
     * @param resourceManager A {@link ResourceManager} for loading textures into the compositor.
     * @param controlsStacker A {@link BottomControlsStacker} to update the bottom controls.
     * @param fullscreenManager A {@link FullscreenManager} to listen for fullscreen changes.
     * @param edgeToEdgeControllerSupplier A supplier to control drawing to the edge of the screen.
     * @param root The parent {@link ViewGroup} for the bottom controls.
     * @param contentDelegateSupplier Supplier of delegate for bottom controls UI operations.
     * @param tabObscuringHandler Delegate object handling obscuring views.
     * @param overlayPanelVisibilitySupplier Notifies overlay panel visibility event.
     * @param constraintsSupplier Used to access current constraints of the browser controls.
     * @param readAloudRestoringSupplier Supplier that returns true if Read Aloud is currently
     *     restoring its player, e.g. after theme change.
     */
    @SuppressLint("CutPasteId") // Not actually cut and paste since it's View vs ViewGroup.
    public BottomControlsCoordinator(
            Activity activity,
            WindowAndroid windowAndroid,
            LayoutManager layoutManager,
            ResourceManager resourceManager,
            BottomControlsStacker controlsStacker,
            BrowserStateBrowserControlsVisibilityDelegate browserControlsVisibilityDelegate,
            FullscreenManager fullscreenManager,
            ObservableSupplier<EdgeToEdgeController> edgeToEdgeControllerSupplier,
            ScrollingBottomViewResourceFrameLayout root,
            OneshotSupplier<BottomControlsContentDelegate> contentDelegateSupplier,
            TabObscuringHandler tabObscuringHandler,
            ObservableSupplier<Boolean> overlayPanelVisibilitySupplier,
            ObservableSupplier<Integer> constraintsSupplier,
            Supplier<Boolean> readAloudRestoringSupplier) {
        mRootFrameLayout = root;
        root.setConstraintsSupplier(constraintsSupplier);
        PropertyModel model = new PropertyModel(BottomControlsProperties.ALL_KEYS);

        mSceneLayer = new ScrollingBottomViewSceneLayer(root, root.getTopShadowHeight());
        PropertyModelChangeProcessor.create(
                model, new ViewHolder(root, mSceneLayer), BottomControlsViewBinder::bind);
        layoutManager.createCompositorMCP(
                model, mSceneLayer, BottomControlsViewBinder::bindCompositorMCP);
        int bottomControlsHeightId = R.dimen.bottom_controls_height;

        View container = root.findViewById(R.id.bottom_container_slot);
        ViewGroup.LayoutParams params = container.getLayoutParams();

        int bottomControlsHeightRes =
                root.getResources().getDimensionPixelOffset(bottomControlsHeightId);
        params.height = bottomControlsHeightRes;

        mMediator =
                new BottomControlsMediator(
                        windowAndroid,
                        model,
                        controlsStacker,
                        browserControlsVisibilityDelegate,
                        fullscreenManager,
                        tabObscuringHandler,
                        bottomControlsHeightRes,
                        overlayPanelVisibilitySupplier,
                        edgeToEdgeControllerSupplier,
                        readAloudRestoringSupplier);
        resourceManager
                .getDynamicResourceLoader()
                .registerResource(root.getId(), root.getResourceAdapter());

        mContentDelegateSupplier = contentDelegateSupplier;
        Toast.setGlobalExtraYOffset(
                root.getResources().getDimensionPixelSize(bottomControlsHeightId));

        // Set the visibility of BottomControls to false by default. Components within
        // BottomControls should update the visibility explicitly if needed.
        setBottomControlsVisible(false);

        mSceneLayer.setIsVisible(mMediator.isCompositedViewVisible());
        layoutManager.addSceneOverlay(mSceneLayer);

        mContentDelegateSupplier.onAvailable(
                (contentDelegate) -> {
                    contentDelegate.initializeWithNative(
                            activity,
                            new BottomControlsVisibilityController() {
                                @Override
                                public void setBottomControlsVisible(boolean isVisible) {
                                    mMediator.setBottomControlsVisible(isVisible);
                                }

                                @Override
                                public void setBottomControlsColor(int color) {
                                    mMediator.setBottomControlsColor(color);
                                }
                            },
                            root::onModelTokenChange);
                    mContentDelegateWrapper.set(contentDelegate);
                });
    }

    /**
     * @param layoutStateProvider {@link LayoutStateProvider} object.
     */
    public void setLayoutStateProvider(LayoutStateProvider layoutStateProvider) {
        mMediator.setLayoutStateProvider(layoutStateProvider);
    }

    /**
     * @param isVisible Whether the bottom control is visible.
     */
    public void setBottomControlsVisible(boolean isVisible) {
        mMediator.setBottomControlsVisible(isVisible);
    }

    /**
     * Handles system back press action if needed.
     *
     * @return Whether or not the back press event is consumed here.
     */
    public boolean onBackPressed() {
        return mContentDelegateSupplier.hasValue()
                ? mContentDelegateSupplier.get().onBackPressed()
                : false;
    }

    @Override
    public @BackPressResult int handleBackPress() {
        return mContentDelegateSupplier.hasValue()
                ? mContentDelegateSupplier.get().handleBackPress()
                : BackPressResult.FAILURE;
    }

    @Override
    public ObservableSupplier<Boolean> getHandleBackPressChangedSupplier() {
        return mHandleBackPressChangedSupplier;
    }

    /** Clean up any state when the bottom controls component is destroyed. */
    public void destroy() {
        if (mContentDelegateSupplier.hasValue()) mContentDelegateSupplier.get().destroy();
        mMediator.destroy();
    }

    public void simulateEdgeToEdgeChangeForTesting(
            int bottomInset, boolean isDrawingToEdge, boolean isPageOptedIntoEdgeToEdge) {
        mMediator.simulateEdgeToEdgeChangeForTesting( // IN-TEST
                bottomInset, isDrawingToEdge, isPageOptedIntoEdgeToEdge); // IN-TEST
    }

    public ScrollingBottomViewSceneLayer getSceneLayerForTesting() {
        return mSceneLayer;
    }

    public ViewResourceAdapter getResourceAdapterForTesting() {
        return mRootFrameLayout.getResourceAdapter();
    }
}