chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/StaticLayout.java

// Copyright 2015 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.compositor.layouts;

import android.animation.Animator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
import android.os.Handler;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.supplier.Supplier;
import org.chromium.cc.input.BrowserControlsOffsetTagsInfo;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.compositor.scene_layer.StaticTabSceneLayer;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.layouts.CompositorModelChangeProcessor;
import org.chromium.chrome.browser.layouts.EventFilter;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimator;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.components.browser_ui.widget.animation.CancelAwareAnimatorListener;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.url.GURL;

import java.util.Collections;

// TODO(meiliang): Rename to StaticLayoutMediator.
/**
 * A {@link Layout} that shows a single tab at full screen. This tab is chosen based on the {@link
 * #tabSelecting(long, int)} call, and is used to show a thumbnail of a {@link Tab} until that
 * {@link Tab} is ready to be shown.
 */
public class StaticLayout extends Layout {
    public static final String TAG = "StaticLayout";

    private static final int HIDE_TIMEOUT_MS = 2000;
    private static final int HIDE_DURATION_MS = 500;

    private boolean mHandlesTabLifecycles;
    private boolean mNeedsOffsetTag;

    private class UnstallRunnable implements Runnable {
        @Override
        public void run() {
            mUnstalling = false;
            CompositorAnimator.ofWritableFloatPropertyKey(
                            mAnimationHandler,
                            mModel,
                            LayoutTab.SATURATION,
                            mModel.get(LayoutTab.SATURATION),
                            1.0f,
                            HIDE_DURATION_MS)
                    .start();
            CompositorAnimator animator =
                    CompositorAnimator.ofWritableFloatPropertyKey(
                            mAnimationHandler,
                            mModel,
                            LayoutTab.STATIC_TO_VIEW_BLEND,
                            mModel.get(LayoutTab.STATIC_TO_VIEW_BLEND),
                            0.0f,
                            HIDE_DURATION_MS);
            animator.addListener(
                    new CancelAwareAnimatorListener() {
                        @Override
                        public void onEnd(Animator animation) {
                            updateVisibleIdsCheckingLiveLayer(
                                    mModel.get(LayoutTab.TAB_ID),
                                    mModel.get(LayoutTab.CAN_USE_LIVE_TEXTURE));
                        }
                    });
            animator.start();
            mModel.set(LayoutTab.SHOULD_STALL, false);
        }
    }

    private final Context mContext;
    private final LayoutManagerHost mViewHost;
    private final CompositorModelChangeProcessor.FrameRequestSupplier mRequestSupplier;

    private final PropertyModel mModel;
    private CompositorModelChangeProcessor mMcp;

    private StaticTabSceneLayer mSceneLayer;

    private final UnstallRunnable mUnstallRunnable;
    private final Handler mHandler;
    private boolean mUnstalling;

    private TabModelSelectorTabModelObserver mTabModelSelectorTabModelObserver;
    private TabModelSelectorTabObserver mTabModelSelectorTabObserver;

    private BrowserControlsStateProvider mBrowserControlsStateProvider;
    private BrowserControlsStateProvider.Observer mBrowserControlsStateProviderObserver;

    private final CompositorAnimationHandler mAnimationHandler;
    private final Supplier<TopUiThemeColorProvider> mTopUiThemeColorProvider;

    private boolean mIsActive;

    private static Integer sToolbarTextBoxBackgroundColorForTesting;

    private float mPxToDp;

    /**
     * Creates an instance of the {@link StaticLayout}.
     * @param context             The current Android's context.
     * @param updateHost          The {@link LayoutUpdateHost} view for this layout.
     * @param renderHost          The {@link LayoutRenderHost} view for this layout.
     * @param viewHost            The {@link LayoutManagerHost} view for this layout
     * @param requestSupplier Frame request supplier for Compositor MCP.
     * @param tabModelSelector {@link TabModelSelector} instance.
     * @param tabContentManager {@link TabContentsManager} instance.
     * @param browserControlsStateProvider A {@link BrowserControlsStateProvider}.
     * @param topUiThemeColorProvider {@link ThemeColorProvider} for top UI.
     */
    public StaticLayout(
            Context context,
            LayoutUpdateHost updateHost,
            LayoutRenderHost renderHost,
            LayoutManagerHost viewHost,
            CompositorModelChangeProcessor.FrameRequestSupplier requestSupplier,
            TabModelSelector tabModelSelector,
            TabContentManager tabContentManager,
            BrowserControlsStateProvider browserControlsStateProvider,
            Supplier<TopUiThemeColorProvider> topUiThemeColorProvider) {
        this(
                context,
                updateHost,
                renderHost,
                viewHost,
                requestSupplier,
                tabModelSelector,
                tabContentManager,
                browserControlsStateProvider,
                topUiThemeColorProvider,
                null);
    }

    /** Protected constructor for testing, allows specifying a custom SceneLayer. */
    @VisibleForTesting
    StaticLayout(
            Context context,
            LayoutUpdateHost updateHost,
            LayoutRenderHost renderHost,
            LayoutManagerHost viewHost,
            CompositorModelChangeProcessor.FrameRequestSupplier requestSupplier,
            TabModelSelector tabModelSelector,
            TabContentManager tabContentManager,
            BrowserControlsStateProvider browserControlsStateProvider,
            Supplier<TopUiThemeColorProvider> topUiThemeColorProvider,
            StaticTabSceneLayer testSceneLayer) {
        super(context, updateHost, renderHost);

        mContext = context;
        boolean isTablet = DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext);
        // Only handle tab lifecycle on tablets.
        mHandlesTabLifecycles = isTablet;
        // On tablets, StaticTabSceneLayer is a subtree of TabStripSceneLayer,
        // and the tag would have been set on the TabStripSceneLayer already.
        mNeedsOffsetTag = !isTablet;

        mViewHost = viewHost;
        mRequestSupplier = requestSupplier;

        setTabContentManager(tabContentManager);
        setTabModelSelector(tabModelSelector);

        mModel =
                new PropertyModel.Builder(LayoutTab.ALL_KEYS)
                        .with(LayoutTab.TAB_ID, Tab.INVALID_TAB_ID)
                        .with(LayoutTab.SCALE, 1.0f)
                        .with(LayoutTab.X, 0.0f)
                        .with(LayoutTab.Y, 0.0f)
                        .with(LayoutTab.RENDER_X, 0.0f)
                        .with(LayoutTab.RENDER_Y, 0.0f)
                        .with(LayoutTab.SATURATION, 1.0f)
                        .with(LayoutTab.STATIC_TO_VIEW_BLEND, 0.0f)
                        .with(LayoutTab.IS_ACTIVE_LAYOUT_SUPPLIER, this::isActive)
                        .build();

        mAnimationHandler = updateHost.getAnimationHandler();
        mTopUiThemeColorProvider = topUiThemeColorProvider;

        mHandler = new Handler();
        mUnstallRunnable = new UnstallRunnable();
        mUnstalling = false;

        Resources res = context.getResources();
        float dpToPx = res.getDisplayMetrics().density;
        mPxToDp = 1.0f / dpToPx;

        mBrowserControlsStateProvider = browserControlsStateProvider;
        mModel.set(LayoutTab.CONTENT_OFFSET, mBrowserControlsStateProvider.getContentOffset());
        mBrowserControlsStateProviderObserver =
                new BrowserControlsStateProvider.Observer() {
                    @Override
                    public void onControlsConstraintsChanged(
                            BrowserControlsOffsetTagsInfo oldOffsetTagsInfo,
                            BrowserControlsOffsetTagsInfo offsetTagsInfo,
                            @BrowserControlsState int constraints) {
                        if (ChromeFeatureList.sBrowserControlsInViz.isEnabled()) {
                            // On tablets, StaticTabSceneLayer is a subtree of TabStripSceneLayer,
                            // and the tag would have been set on the TabStripSceneLayer already.
                            if (mNeedsOffsetTag) {
                                mModel.set(
                                        LayoutTab.CONTENT_OFFSET_TAG,
                                        offsetTagsInfo.getTopControlsOffsetTag());
                            }

                            // With BCIV enabled, scrolling will not update the content offset of
                            // the browser's compositor frame. If we transition to a HIDDEN state
                            // while the controls are already scrolled offscreen, then there is no
                            // need to move the top controls, which means the renderer will not
                            // notify the browser to move them. We set the content offset here so
                            // the browser will submit a compositor frame with the correct offset.
                            int contentOffset = mBrowserControlsStateProvider.getContentOffset();
                            if (constraints == BrowserControlsState.HIDDEN
                                    && contentOffset
                                            == mBrowserControlsStateProvider
                                                    .getTopControlsMinHeight()) {
                                mModel.set(LayoutTab.CONTENT_OFFSET, contentOffset);
                            }
                        }
                    }

                    @Override
                    public void onControlsOffsetChanged(
                            int topOffset,
                            int topControlsMinHeightOffset,
                            int bottomOffset,
                            int bottomControlsMinHeightOffset,
                            boolean needsAnimate,
                            boolean isVisibilityForced) {
                        if (!ChromeFeatureList.sBrowserControlsInViz.isEnabled()
                                || needsAnimate
                                || isVisibilityForced) {
                            mModel.set(
                                    LayoutTab.CONTENT_OFFSET,
                                    mBrowserControlsStateProvider.getContentOffset());
                        }
                    }
                };
        mBrowserControlsStateProvider.addObserver(mBrowserControlsStateProviderObserver);

        if (testSceneLayer != null) {
            mSceneLayer = testSceneLayer;
        } else {
            mSceneLayer = new StaticTabSceneLayer();
        }
        mSceneLayer.setTabContentManager(mTabContentManager);

        mMcp =
                CompositorModelChangeProcessor.create(
                        mModel, mSceneLayer, StaticTabSceneLayer::bind, mRequestSupplier);
    }

    @Override
    public void setTabModelSelector(TabModelSelector tabModelSelector) {
        assert tabModelSelector != null;
        assert mTabModelSelector == null : "The TabModelSelector should set at most once";
        super.setTabModelSelector(tabModelSelector);

        // TODO(crbug.com/40126259): Investigating to use ActivityTabProvider instead.
        mTabModelSelectorTabModelObserver =
                new TabModelSelectorTabModelObserver(tabModelSelector) {
                    @Override
                    public void didSelectTab(Tab tab, int type, int lastId) {
                        if (!mIsActive) return;

                        setStaticTab(tab);
                        requestFocus(tab);
                    }
                };

        mTabModelSelectorTabObserver =
                new TabModelSelectorTabObserver(tabModelSelector) {
                    @Override
                    public void onPageLoadFinished(Tab tab, GURL url) {
                        if (mIsActive) unstallImmediately(tab.getId());
                    }

                    @Override
                    public void onShown(Tab tab, @TabSelectionType int type) {
                        if (mModel.get(LayoutTab.TAB_ID) != tab.getId()) {
                            setStaticTab(tab);
                        } else {
                            updateStaticTab(tab, /* skipUpdateVisibleIds= */ false);
                        }
                    }

                    @Override
                    public void onDestroyed(Tab tab) {
                        if (mModel.get(LayoutTab.TAB_ID) != tab.getId()) return;

                        mModel.set(LayoutTab.TAB_ID, Tab.INVALID_TAB_ID);
                    }

                    @Override
                    public void onTabUnregistered(Tab tab) {
                        if (mModel.get(LayoutTab.TAB_ID) != tab.getId()) return;

                        mModel.set(LayoutTab.TAB_ID, Tab.INVALID_TAB_ID);
                    }

                    @Override
                    public void onContentChanged(Tab tab) {
                        updateStaticTab(tab, /* skipUpdateVisibleIds= */ false);
                    }

                    @Override
                    public void onBackgroundColorChanged(Tab tab, int color) {
                        updateStaticTab(tab, /* skipUpdateVisibleIds= */ false);
                    }

                    @Override
                    public void onDidChangeThemeColor(Tab tab, int color) {
                        updateStaticTab(tab, /* skipUpdateVisibleIds= */ false);
                    }

                    @Override
                    public void didBackForwardTransitionAnimationChange() {
                        updateStaticTab(
                                tabModelSelector.getCurrentTab(),
                                /* skipUpdateVisibleIds= */ false);
                    }
                };
    }

    @Override
    public @ViewportMode int getViewportMode() {
        return ViewportMode.DYNAMIC_BROWSER_CONTROLS;
    }

    /**
     * Initialize the layout to be shown.
     * @param time   The current time of the app in ms.
     * @param animate Whether to play an entry animation.
     */
    @Override
    public void show(long time, boolean animate) {
        super.show(time, animate);

        mIsActive = true;
        Tab tab = mTabModelSelector.getCurrentTab();
        if (tab == null) return;
        setStaticTab(tab);
    }

    @Override
    protected void updateLayout(long time, long dt) {
        super.updateLayout(time, dt);
        updateSnap(dt, mModel);
    }

    @Override
    public void doneShowing() {
        super.doneShowing();
        Tab tab = mTabModelSelector.getCurrentTab();
        if (tab == null) return;
        requestFocus(tab);
    }

    @Override
    public void doneHiding() {
        mIsActive = false;
        mModel.set(LayoutTab.TAB_ID, Tab.INVALID_TAB_ID);

        // Call super last because it might re-show this layout. If we do any work after
        // super.doneHiding() the layout might become unexpectedly inactive or have an
        // incorrect tab id. See crbug/1468214.
        super.doneHiding();
    }

    private void setPreHideState() {
        mHandler.removeCallbacks(mUnstallRunnable);
        mModel.set(LayoutTab.STATIC_TO_VIEW_BLEND, 1.0f);
        mModel.set(LayoutTab.SATURATION, 0.0f);
        mUnstalling = true;
    }

    private void setPostHideState() {
        mHandler.removeCallbacks(mUnstallRunnable);
        mModel.set(LayoutTab.STATIC_TO_VIEW_BLEND, 0.0f);
        mModel.set(LayoutTab.SATURATION, 1.0f);
        mUnstalling = false;
    }

    private void requestFocus(Tab tab) {
        // TODO(crbug.com/40249125): Investigating guarded removal of this behavior (requesting
        // focus on
        // a tab) since it may no longer be relevant.
        // We will restrict avoidance of tab focus request only on tablet devices, since this is
        // known to cause regressions on phones - see crbug.com/1471887 for details.
        if (ChromeFeatureList.isEnabled(
                        ChromeFeatureList.AVOID_SELECTED_TAB_FOCUS_ON_LAYOUT_DONE_SHOWING)
                && DeviceFormFactor.isNonMultiDisplayContextOnTablet(mContext)) {
            return;
        }

        if (mIsActive && tab.getView() != null) tab.getView().requestFocus();
    }

    private void updateVisibleIdsCheckingLiveLayer(int tabId, boolean useLiveTexture) {
        // May be called when inactive. Prevent this from updating until the layout is shown.
        if (!isActive()) return;

        // Check if we can use the live texture as frozen or native pages don't support live layer.
        if (useLiveTexture) {
            updateCacheVisibleIdsAndPrimary(Collections.emptyList(), tabId);
        } else {
            updateCacheVisibleIdsAndPrimary(Collections.singletonList(tabId), tabId);
        }
    }

    private void updateVisibleIdsFromTab(Tab tab) {
        // May be called when inactive. Prevent this from updating until the layout is shown.
        if (!isActive()) return;

        final int tabId = tab.getId();
        if (shouldStall(tab)) {
            updateCacheVisibleIdsAndPrimary(Collections.singletonList(tabId), tabId);
        } else {
            updateVisibleIdsCheckingLiveLayer(tabId, canUseLiveTexture(tab));
        }
    }

    private void setStaticTab(Tab tab) {
        assert tab != null;

        if (mModel.get(LayoutTab.TAB_ID) == tab.getId() && !mModel.get(LayoutTab.SHOULD_STALL)) {
            updateVisibleIdsCheckingLiveLayer(tab.getId(), canUseLiveTexture(tab));
            setPostHideState();
            return;
        }

        updateVisibleIdsFromTab(tab);

        mModel.set(LayoutTab.TAB_ID, tab.getId());
        mModel.set(LayoutTab.IS_INCOGNITO, tab.isIncognito());
        mModel.set(LayoutTab.ORIGINAL_CONTENT_WIDTH_IN_DP, mViewHost.getWidth() * mPxToDp);
        mModel.set(LayoutTab.ORIGINAL_CONTENT_HEIGHT_IN_DP, mViewHost.getHeight() * mPxToDp);
        mModel.set(LayoutTab.MAX_CONTENT_WIDTH, mViewHost.getWidth() * mPxToDp);
        mModel.set(LayoutTab.MAX_CONTENT_HEIGHT, mViewHost.getHeight() * mPxToDp);

        updateStaticTab(tab, /* skipUpdateVisibleIds= */ true);

        if (mModel.get(LayoutTab.SHOULD_STALL)) {
            setPreHideState();
            mHandler.postDelayed(mUnstallRunnable, HIDE_TIMEOUT_MS);
        } else {
            setPostHideState();
        }
    }

    private void updateStaticTab(Tab tab, boolean skipUpdateVisibleIds) {
        if (mModel.get(LayoutTab.TAB_ID) != tab.getId()) return;

        if (!skipUpdateVisibleIds) {
            updateVisibleIdsFromTab(tab);
        }

        TopUiThemeColorProvider topUiTheme = mTopUiThemeColorProvider.get();
        mModel.set(LayoutTab.BACKGROUND_COLOR, topUiTheme.getBackgroundColor(tab));
        mModel.set(LayoutTab.TOOLBAR_BACKGROUND_COLOR, topUiTheme.getSceneLayerBackground(tab));
        mModel.set(LayoutTab.SHOULD_STALL, shouldStall(tab));
        mModel.set(LayoutTab.TEXT_BOX_BACKGROUND_COLOR, getToolbarTextBoxBackgroundColor(tab));
        mModel.set(LayoutTab.CAN_USE_LIVE_TEXTURE, canUseLiveTexture(tab));
    }

    private int getToolbarTextBoxBackgroundColor(Tab tab) {
        if (sToolbarTextBoxBackgroundColorForTesting != null) {
            return sToolbarTextBoxBackgroundColorForTesting;
        }

        return ThemeUtils.getTextBoxColorForToolbarBackground(
                mContext,
                tab,
                mTopUiThemeColorProvider.get().calculateColor(tab, tab.getThemeColor()));
    }

    void setTextBoxBackgroundColorForTesting(Integer color) {
        sToolbarTextBoxBackgroundColorForTesting = color;
    }

    // Whether the tab is ready to display or it should be faded in as it loads.
    private boolean shouldStall(Tab tab) {
        return (tab.isFrozen() || tab.needsReload()) && !tab.isNativePage();
    }

    private boolean canUseLiveTexture(Tab tab) {
        final WebContents webContents = tab.getWebContents();
        if (webContents == null) return false;

        final GURL url = tab.getUrl();
        final boolean isNativePage =
                tab.isNativePage() || url.getScheme().equals(UrlConstants.CHROME_NATIVE_SCHEME);
        final boolean isBFScreenshotDrawing =
                isNativePage && tab.isDisplayingBackForwardAnimation();
        assert !isBFScreenshotDrawing
                        || ChromeFeatureList.isEnabled(ChromeFeatureList.BACK_FORWARD_TRANSITIONS)
                : "Must not draw bf screenshot if back forward transition is disabled";
        return !SadTab.isShowing(tab) && (!isNativePage || isBFScreenshotDrawing);
    }

    @Override
    public void unstallImmediately(int tabId) {
        if (mModel.get(LayoutTab.TAB_ID) == tabId
                && mModel.get(LayoutTab.SHOULD_STALL)
                && mUnstalling) {
            unstallImmediately();
        }
    }

    @Override
    public void unstallImmediately() {
        if (mModel.get(LayoutTab.SHOULD_STALL) && mUnstalling) {
            mHandler.removeCallbacks(mUnstallRunnable);
            mUnstallRunnable.run();
        }
    }

    @Override
    public boolean handlesTabCreating() {
        return super.handlesTabCreating() || mHandlesTabLifecycles;
    }

    @Override
    public boolean handlesTabClosing() {
        return mHandlesTabLifecycles;
    }

    @Override
    public boolean shouldDisplayContentOverlay() {
        return true;
    }

    @Override
    protected EventFilter getEventFilter() {
        return null;
    }

    @Override
    protected SceneLayer getSceneLayer() {
        return mSceneLayer;
    }

    @Override
    protected void updateSceneLayer(
            RectF viewport,
            RectF contentViewport,
            TabContentManager tabContentManager,
            ResourceManager resourceManager,
            BrowserControlsStateProvider browserControls) {
        super.updateSceneLayer(
                viewport, contentViewport, tabContentManager, resourceManager, browserControls);
        assert mSceneLayer != null;
    }

    @Override
    public int getLayoutType() {
        return LayoutType.BROWSING;
    }

    @Override
    public void destroy() {
        if (mSceneLayer != null) {
            mSceneLayer.destroy();
            mSceneLayer = null;
        }
        if (mMcp != null) {
            mMcp.destroy();
            mMcp = null;
        }
        if (mTabModelSelector != null) {
            mTabModelSelectorTabModelObserver.destroy();
            mTabModelSelectorTabObserver.destroy();
        }
    }

    PropertyModel getModelForTesting() {
        return mModel;
    }

    TabModelSelector getTabModelSelectorForTesting() {
        return mTabModelSelector;
    }

    TabContentManager getTabContentManagerForTesting() {
        return mTabContentManager;
    }

    BrowserControlsStateProvider getBrowserControlsStateProviderForTesting() {
        return mBrowserControlsStateProvider;
    }

    public int getCurrentTabIdForTesting() {
        return mModel.get(LayoutTab.TAB_ID);
    }
}