chromium/chrome/android/java/src/org/chromium/chrome/browser/hub/HubLayout.java

// Copyright 2023 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.hub;

import static org.chromium.chrome.browser.hub.HubLayoutConstants.EXPAND_NEW_TAB_DURATION_MS;
import static org.chromium.chrome.browser.hub.HubLayoutConstants.FADE_DURATION_MS;
import static org.chromium.chrome.browser.hub.HubLayoutConstants.TIMEOUT_MS;
import static org.chromium.chrome.browser.hub.HubLayoutConstants.TRANSLATE_DURATION_MS;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.FrameLayout;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.Promise;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.supplier.SyncOneshotSupplierImpl;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
import org.chromium.chrome.browser.compositor.layouts.LayoutUpdateHost;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.compositor.scene_layer.SolidColorSceneLayer;
import org.chromium.chrome.browser.compositor.scene_layer.StaticTabSceneLayer;
import org.chromium.chrome.browser.layouts.EventFilter;
import org.chromium.chrome.browser.layouts.LayoutManager;
import org.chromium.chrome.browser.layouts.LayoutStateProvider;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabLoadIfNeededCaller;
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.TabModelUtils;
import org.chromium.chrome.browser.ui.desktop_windowing.AppHeaderState;
import org.chromium.chrome.browser.ui.desktop_windowing.DesktopWindowStateProvider;
import org.chromium.chrome.browser.ui.desktop_windowing.DesktopWindowStateProvider.AppHeaderObserver;
import org.chromium.components.browser_ui.styles.ChromeColors;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.resources.ResourceManager;

import java.util.Collections;
import java.util.function.DoubleConsumer;

/**
 * A {@link Layout} for Hub that has an empty or single tab {@link SceneLayer}. Android UI for a
 * toolbar and panes will be rendered atop this layout.
 *
 * <p>This implementation is a heavily modified fork of {@link TabSwitcherLayout} that will delegate
 * animations to the current pane.
 *
 * <p>Normally, this layout will show an empty {@link SceneLayer}. However, to facilitate thumbnail
 * capture and animations it may transiently host a {@link StaticTabSceneLayer}.
 */
public class HubLayout extends Layout implements HubLayoutController, AppHeaderObserver {
    private final @NonNull Callback<Pane> mOnPaneFocused = this::updateEmptyLayerColor;
    private final @NonNull LayoutStateProvider mLayoutStateProvider;
    private final @NonNull ViewGroup mRootView;
    private final @NonNull HubManager mHubManager;
    private final @NonNull HubController mHubController;
    private final @NonNull PaneManager mPaneManager;
    private final @NonNull HubLayoutScrimController mScrimController;
    private final @NonNull DoubleConsumer mOnToolbarAlphaChange;
    private final @NonNull HubShowPaneHelper mHubShowPaneHelper;
    private final @Nullable DesktopWindowStateProvider mDesktopWindowStateProvider;

    /**
     * The previous {@link LayoutType}, valid between {@link #show(long, boolean)} and {@link
     * #doneShowing()}.
     */
    private final @NonNull ObservableSupplierImpl<Integer> mPreviousLayoutTypeSupplier =
            new ObservableSupplierImpl<>();

    private @Nullable SceneLayer mCurrentSceneLayer;

    private boolean mFullyShown;

    /** Scene layer to facilitate thumbnail capture prior to starting a transition animation. */
    private @Nullable StaticTabSceneLayer mTabSceneLayer;

    /** An empty scene layer used to avoid drawing anything. */
    private @Nullable SolidColorSceneLayer mEmptySceneLayer;

    private @Nullable HubLayoutAnimationRunner mCurrentAnimationRunner;

    /**
     * Create the {@link Layout} to show the Hub on.
     *
     * @param context The current Android {@link Context}.
     * @param updateHost The {@link LayoutUpdateHost} for the {@link LayoutManager}.
     * @param renderHost The {@link LayoutRenderHost} for the {@link LayoutManager}.
     * @param layoutStateProvider The {@link LayoutStateProvider} for the {@link LayoutManager}.
     * @param dependencyHolder The {@link HubLayoutDependencyHolder} that holds dependencies for
     *     HubLayout.
     * @param tabModelSelectorSupplier Supplier for an interface to talk to the Tab Model Selector.
     */
    public HubLayout(
            @NonNull Context context,
            @NonNull LayoutUpdateHost updateHost,
            @NonNull LayoutRenderHost renderHost,
            @NonNull LayoutStateProvider layoutStateProvider,
            @NonNull HubLayoutDependencyHolder dependencyHolder,
            Supplier<TabModelSelector> tabModelSelectorSupplier,
            @Nullable DesktopWindowStateProvider desktopWindowStateProvider) {
        super(context, updateHost, renderHost);
        mPreviousLayoutTypeSupplier.set(layoutStateProvider.getActiveLayoutType());

        mLayoutStateProvider = layoutStateProvider;
        // This is the R.id.tab_switcher_view_holder. It is used by tablets for the tab switcher,
        // but in that case it is the animated object and is opaque. Here we will animate the
        // HubContainerView instead and just animate and just use this view as a transparent
        // container for z-indexing and geometry.
        // TODO(crbug.com/40288013): Consider removing the tab_switcher_view_holder post launch.
        mRootView = dependencyHolder.getHubRootView();
        mRootView.setBackgroundColor(Color.TRANSPARENT);

        mHubManager = dependencyHolder.getHubManager();
        mHubController = mHubManager.getHubController();
        mHubController.setHubLayoutController(this);
        mPaneManager = mHubManager.getPaneManager();
        mPaneManager.getFocusedPaneSupplier().addObserver(mOnPaneFocused);
        mHubShowPaneHelper = mHubManager.getHubShowPaneHelper();
        mScrimController = dependencyHolder.getScrimController();
        mOnToolbarAlphaChange = dependencyHolder.getOnToolbarAlphaChange();
        mTabModelSelector = tabModelSelectorSupplier.get();
        mDesktopWindowStateProvider = desktopWindowStateProvider;
        if (mDesktopWindowStateProvider != null) {
            mDesktopWindowStateProvider.addObserver(this);
            maybeUpdateLayout();
        }
    }

    @Override
    public void onAppHeaderStateChanged(AppHeaderState newState) {
        maybeUpdateLayout();
    }

    /** Returns the current {@link HubLayoutAnimationType}. */
    @HubLayoutAnimationType
    int getCurrentAnimationType() {
        return mCurrentAnimationRunner != null
                ? mCurrentAnimationRunner.getAnimationType()
                : HubLayoutAnimationType.NONE;
    }

    @Override
    public void selectTabAndHideHubLayout(int tabId) {
        TabModelUtils.selectTabById(mTabModelSelector, tabId, TabSelectionType.FROM_USER);
        startHiding();
    }

    @Override
    public ObservableSupplier<Integer> getPreviousLayoutTypeSupplier() {
        return mPreviousLayoutTypeSupplier;
    }

    @Override
    public void onFinishNativeInitialization() {
        super.onFinishNativeInitialization();
        ensureSceneLayersExist();
    }

    @Override
    public void setTabContentManager(TabContentManager tabContentManager) {
        super.setTabContentManager(tabContentManager);
        if (mTabSceneLayer != null && mTabContentManager != null) {
            mTabSceneLayer.setTabContentManager(mTabContentManager);
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        if (mTabSceneLayer != null) {
            mTabSceneLayer.destroy();
            mTabSceneLayer = null;
        }
        if (mEmptySceneLayer != null) {
            mEmptySceneLayer.destroy();
            mEmptySceneLayer = null;
        }
        mCurrentSceneLayer = null;
        mPaneManager.getFocusedPaneSupplier().removeObserver(mOnPaneFocused);
        if (mDesktopWindowStateProvider != null) {
            mDesktopWindowStateProvider.removeObserver(this);
        }
    }

    @Override
    protected void updateLayout(long time, long dt) {
        ensureSceneLayersExist();
        super.updateLayout(time, dt);
        if (!hasLayoutTab()) return;

        boolean needUpdate = updateSnap(dt, getLayoutTab());
        if (needUpdate) requestUpdate();
    }

    @Override
    public void contextChanged(Context context) {
        super.contextChanged(context);
        // This is called before show() and before getActiveLayoutType() changes so we can know what
        // layout the Hub is showing from.
        mPreviousLayoutTypeSupplier.set(mLayoutStateProvider.getActiveLayoutType());
    }

    @Override
    public @ViewportMode int getViewportMode() {
        // Hub has its own toolbar.
        // TODO(crbug.com/40283200): Confirm this doesn't cause the toolbar to disappear too early
        // or
        // without animation.
        return ViewportMode.ALWAYS_FULLSCREEN;
    }

    @Override
    public void show(long time, boolean animate) {
        if (isStartingToShow()) return;

        try (TraceEvent e = TraceEvent.scoped("HubLayout.show")) {
            super.show(time, animate);

            forceAnimationToFinish();

            Promise<Bitmap> bitmapPromise = new Promise<>();
            @LayoutType int previousLayoutType = mPreviousLayoutTypeSupplier.get();
            if (previousLayoutType == LayoutType.BROWSING) {
                final Tab currentTab = mTabModelSelector.getCurrentTab();
                createLayoutTabForTabId(getIdForTab(currentTab));
                mCurrentSceneLayer = mTabSceneLayer;
                captureTabThumbnail(currentTab, bitmapPromise);
            } else {
                mCurrentSceneLayer = mEmptySceneLayer;
                bitmapPromise.fulfill(null);
            }

            mPaneManager.focusPane(
                    mHubShowPaneHelper.consumeNextPaneId(mTabModelSelector.isIncognitoSelected()));

            mHubController.onHubLayoutShow();

            HubContainerView containerView = mHubController.getContainerView();
            HubLayoutAnimatorProvider animatorProvider = createShowAnimatorProvider(containerView);

            Callback<Bitmap> thumbnailCallback = animatorProvider.getThumbnailCallback();
            if (thumbnailCallback != null) {
                bitmapPromise.then(thumbnailCallback);
            }
            updateEmptyLayerColor(mPaneManager.getFocusedPaneSupplier().get());

            assert mCurrentAnimationRunner == null;
            mCurrentAnimationRunner =
                    HubLayoutAnimationRunnerFactory.createHubLayoutAnimationRunner(
                            animatorProvider);
            mCurrentAnimationRunner.addListener(
                    new HubLayoutAnimationListener() {
                        @Override
                        public void onEnd(boolean wasForcedToFinish) {
                            doneShowing();
                            if (!wasForcedToFinish) {
                                // We don't want to hide the tab if the animation was forced to
                                // finish since that means another layout is going to show and
                                // hiding the tab could leave the tab in a bad state.
                                hideCurrentTab();
                            }
                        }
                    });
            maybeAddPaneAnimationListener(mCurrentAnimationRunner);

            mRootView.setVisibility(View.VISIBLE);
            containerView.setVisibility(View.INVISIBLE);
            LayoutParams params = (LayoutParams) containerView.getLayoutParams();
            // TODO(crbug.com/41495991): Change this to an assert and fix any broken tests.
            if (params == null) {
                params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            }
            mRootView.addView(containerView, /* index= */ 0, params);

            if (!animate) {
                // Don't post or wait for a layout as HubLayout is not in control of when the
                // previous layout was hidden and this avoids a possibly empty frame.
                queueAnimation();
                forceAnimationToFinish();
                hideCurrentTab();
            } else {
                containerView.runOnNextLayout(this::queueAnimation);
            }
        }
    }

    @Override
    public void doneShowing() {
        try (TraceEvent e = TraceEvent.scoped("HubLayout.doneShowing")) {
            super.doneShowing();
            mCurrentSceneLayer = mEmptySceneLayer;
            mCurrentAnimationRunner = null;
            resetLayoutTabs(/* clearVisibleIds= */ true);
            // We are now fully shown as all animations are finished.
            mFullyShown = true;

            // This is a legacy value from the stack tab switcher, we are using at a proxy for Hub
            // shown. Prior to Hub this was recorded in when the TabSwitcherMediator finished
            // showing. However, this location is a better analog than TabSwitcherPaneMediator and
            // it keeps the call with doneHiding() symmetric.
            RecordUserAction.record("MobileToolbarShowStackView");
        }
    }

    @Override
    public void startHiding() {
        if (isStartingToHide()) return;

        try (TraceEvent e = TraceEvent.scoped("HubLayout.startHiding")) {
            super.startHiding();

            // Since we are hiding this is no-longer fully shown.
            mFullyShown = false;

            // Use the EXPAND_NEW_TAB animation if it is already prepared.
            if (getCurrentAnimationType() == HubLayoutAnimationType.EXPAND_NEW_TAB) {
                PostTask.postTask(TaskTraits.UI_DEFAULT, this::queueAnimation);
                return;
            }

            forceAnimationToFinish();

            Tab currentTab = mTabModelSelector.getCurrentTab();
            if (currentTab != null && currentTab.isHidden()) {
                currentTab.show(TabSelectionType.FROM_USER, TabLoadIfNeededCaller.SET_TAB);
            }

            int tabId = mTabModelSelector.getCurrentTabId();
            @LayoutType int nextLayoutType = mLayoutStateProvider.getNextLayoutType();
            if (nextLayoutType == LayoutType.BROWSING) {
                // During fade and translate animations the composited scene layer is visible. At
                // the end of the animation a composited tab will be fully visible. To ensure
                // continuity during the animation create and show the mTabSceneLayer with the
                // LayoutTab for the tabId that will be shown once the animation finishes.
                createLayoutTabForTabId(tabId);
                mCurrentSceneLayer = mTabSceneLayer;
            } else {
                mCurrentSceneLayer = mEmptySceneLayer;
            }
            updateEmptyLayerColor(mPaneManager.getFocusedPaneSupplier().get());

            HubContainerView containerView = mHubController.getContainerView();
            HubLayoutAnimatorProvider animatorProvider = createHideAnimatorProvider(containerView);

            Callback<Bitmap> thumbnailCallback = animatorProvider.getThumbnailCallback();
            if (thumbnailCallback != null) {
                if (nextLayoutType == LayoutType.BROWSING
                        && mTabContentManager != null
                        && tabId != Tab.INVALID_TAB_ID) {
                    mTabContentManager.getEtc1TabThumbnailWithCallback(tabId, thumbnailCallback);
                } else {
                    thumbnailCallback.onResult(null);
                }
            }

            assert mCurrentAnimationRunner == null;
            mCurrentAnimationRunner =
                    HubLayoutAnimationRunnerFactory.createHubLayoutAnimationRunner(
                            animatorProvider);
            mCurrentAnimationRunner.addListener(
                    new HubLayoutAnimationListener() {
                        @Override
                        public void onEnd(boolean wasForcedToFinish) {
                            doneHiding();
                        }
                    });
            maybeAddPaneAnimationListener(mCurrentAnimationRunner);

            PostTask.postTask(TaskTraits.UI_DEFAULT, this::queueAnimation);
        }
    }

    @Override
    public void doneHiding() {
        try (TraceEvent e = TraceEvent.scoped("HubLayout.doneHiding")) {
            HubContainerView containerView = mHubController.getContainerView();
            containerView.setVisibility(View.INVISIBLE);
            mRootView.removeView(containerView);
            mRootView.setVisibility(View.GONE);
            mCurrentAnimationRunner = null;
            mHubController.onHubLayoutDoneHiding();

            mCurrentSceneLayer = mEmptySceneLayer;
            // Don't clear the visible ids because the next layout might have already updated them.
            resetLayoutTabs(/* clearVisibleIds= */ false);

            // This is defensive and probably is not necessary as it is set in startHiding; however,
            // to ensure the toolbar does not become broken also set this here.
            mFullyShown = false;

            // This is a legacy value from the stack tab switcher, we are using at a proxy for Hub
            // hidden.
            RecordUserAction.record("MobileExitStackView");

            // Do this last so the Hub is ready to show again.
            super.doneHiding();
        }
    }

    @Override
    protected void forceAnimationToFinish() {
        if (mCurrentAnimationRunner == null) return;

        // Immediately start any pending animations.
        mHubController.getContainerView().runOnNextLayoutRunnables();

        // Force the animation to run to completion.
        mCurrentAnimationRunner.forceAnimationToFinish();
        mCurrentAnimationRunner = null;

        if (mScrimController != null) {
            mScrimController.forceAnimationToFinish();
        }
    }

    @Override
    public boolean onBackPressed() {
        // This is for the legacy backpress handler which will soon be obsolete.
        return mHubController.onHubLayoutBackPressed();
    }

    @Override
    public void onTabCreated(
            long time,
            int tabId,
            int tabIndex,
            int sourceTabId,
            boolean newIsIncognito,
            boolean background,
            float originX,
            float originY) {
        super.onTabCreated(
                time, tabId, tabIndex, sourceTabId, newIsIncognito, background, originX, originY);

        // Background tab creation or creation while hiding does not trigger a Hub layout
        // transition.
        if (background || isStartingToHide()) return;

        // Tablet Hub doesn't handle new tab animations.
        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())) {
            selectTabAndHideHubLayout(tabId);
            return;
        }

        forceAnimationToFinish();

        mCurrentSceneLayer = mEmptySceneLayer;
        updateEmptyLayerColor(mPaneManager.getFocusedPaneSupplier().get());

        @ColorInt int backgroundColor;
        if (newIsIncognito) {
            backgroundColor = ChromeColors.getPrimaryBackgroundColor(getContext(), newIsIncognito);
        } else {
            // See https://crbug/1507124.
            backgroundColor =
                    ChromeColors.getSurfaceColor(
                            getContext(), R.dimen.home_surface_background_color_elevation);
        }
        SyncOneshotSupplierImpl<ShrinkExpandAnimationData> animationDataSupplier =
                new SyncOneshotSupplierImpl<>();
        HubLayoutAnimatorProvider animatorProvider =
                ShrinkExpandHubLayoutAnimationFactory.createNewTabAnimatorProvider(
                        mHubController.getContainerView(),
                        animationDataSupplier,
                        backgroundColor,
                        EXPAND_NEW_TAB_DURATION_MS,
                        mOnToolbarAlphaChange);

        HubContainerView containerView = mHubController.getContainerView();
        assert containerView.isLaidOut();
        Rect containerViewRect = new Rect();
        containerView.getGlobalVisibleRect(containerViewRect);

        View paneHost = mHubController.getPaneHostView();
        assert paneHost.isLaidOut();
        Rect finalRect = new Rect();
        paneHost.getGlobalVisibleRect(finalRect);
        // Ignore edge offset and just ensure the width is correct. See crbug/1502437.
        finalRect.offset(-finalRect.left, -containerViewRect.top);

        // TODO(crbug.com/40285429): Supply this from HubController so it can look like the
        // animation originated from wherever on the Hub was clicked. This defaults to the top
        // left/right of the pane host view.
        boolean isRtl = LocalizationUtils.isLayoutRtl();
        Rect initialRect = null;
        int x = isRtl ? finalRect.right : finalRect.left;
        int y = finalRect.top;
        if (isRtl) {
            initialRect = new Rect(x - 1, y, x, y + 1);
        } else {
            initialRect = new Rect(x, y, x + 1, y + 1);
        }

        animationDataSupplier.set(
                new ShrinkExpandAnimationData(
                        initialRect,
                        finalRect,
                        /* thumbnailSize= */ null,
                        /* useFallbackAnimation= */ false));

        mCurrentAnimationRunner =
                HubLayoutAnimationRunnerFactory.createHubLayoutAnimationRunner(animatorProvider);
        mCurrentAnimationRunner.addListener(
                new HubLayoutAnimationListener() {
                    @Override
                    public void onEnd(boolean wasForcedToFinish) {
                        doneHiding();
                    }
                });
        maybeAddPaneAnimationListener(mCurrentAnimationRunner);

        selectTabAndHideHubLayout(tabId);
    }

    @Override
    protected boolean onUpdateAnimation(long time, boolean jumpToEnd) {
        // Return whether an animation is running. Ignore the inputs like TabSwitcherLayout.
        return mCurrentAnimationRunner != null;
    }

    @Override
    public boolean handlesTabClosing() {
        // Tabs can be closed from the Tab Switcher panes without changing layout.
        return true;
    }

    @Override
    public boolean handlesTabCreating() {
        // For the new tab animation.
        return true;
    }

    @Override
    public boolean canHostBeFocusable() {
        // TODO(crbug.com/40283200): Consider returning false here so that the omnibox doesn't steal
        // focus.
        return super.canHostBeFocusable();
    }

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

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

    @Override
    protected void updateSceneLayer(
            RectF viewport,
            RectF contentViewport,
            TabContentManager tabContentManager,
            ResourceManager resourceManager,
            BrowserControlsStateProvider browserControls) {
        ensureSceneLayersExist();
        super.updateSceneLayer(
                viewport, contentViewport, tabContentManager, resourceManager, browserControls);

        if (mCurrentSceneLayer != mTabSceneLayer) return;

        LayoutTab layoutTab = getLayoutTab();
        layoutTab.set(LayoutTab.IS_ACTIVE_LAYOUT_SUPPLIER, this::isActive);
        layoutTab.set(LayoutTab.CONTENT_OFFSET, browserControls.getContentOffset());
        mTabSceneLayer.update(layoutTab);
    }

    @Override
    public boolean forceHideBrowserControlsAndroidView() {
        // Fixes being able to click the toolbar through the Hub on LFF devices see b/337616153.
        // This is not always `true` because it results in a visible flicker when exiting the Hub
        // into an NTP when using the expand animation.
        return mFullyShown;
    }

    @Override
    public @LayoutType int getLayoutType() {
        // Pretend to be the TAB_SWITCHER for initial development to minimize churn outside of
        // LayoutManager.
        return LayoutType.TAB_SWITCHER;
    }

    @Override
    public boolean isRunningAnimations() {
        return mCurrentAnimationRunner != null;
    }

    // Visible for testing or spying

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    HubLayoutAnimatorProvider createShowAnimatorProvider(@NonNull HubContainerView containerView) {
        @Nullable Pane pane = mPaneManager.getFocusedPaneSupplier().get();

        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())) {
            return TranslateHubLayoutAnimationFactory.createTranslateUpAnimatorProvider(
                    containerView, mScrimController, TRANSLATE_DURATION_MS, getContainerYOffset());
        } else if (pane == null) {
            return FadeHubLayoutAnimationFactory.createFadeInAnimatorProvider(
                    containerView, FADE_DURATION_MS, mOnToolbarAlphaChange);
        }
        return pane.createShowHubLayoutAnimatorProvider(containerView);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    HubLayoutAnimatorProvider createHideAnimatorProvider(@NonNull HubContainerView containerView) {
        @Nullable Pane pane = mPaneManager.getFocusedPaneSupplier().get();

        if (DeviceFormFactor.isNonMultiDisplayContextOnTablet(getContext())) {
            return TranslateHubLayoutAnimationFactory.createTranslateDownAnimatorProvider(
                    containerView, mScrimController, TRANSLATE_DURATION_MS, getContainerYOffset());
        } else if (pane == null) {
            return FadeHubLayoutAnimationFactory.createFadeOutAnimatorProvider(
                    containerView, FADE_DURATION_MS, mOnToolbarAlphaChange);
        }
        return pane.createHideHubLayoutAnimatorProvider(containerView);
    }

    private void maybeAddPaneAnimationListener(HubLayoutAnimationRunner animationRunner) {
        @Nullable Pane pane = mPaneManager.getFocusedPaneSupplier().get();
        if (pane == null) return;

        HubLayoutAnimationListener listener = pane.getHubLayoutAnimationListener();
        if (listener == null) return;

        animationRunner.addListener(listener);
    }

    private void queueAnimation() {
        if (mCurrentAnimationRunner == null) return;

        mCurrentAnimationRunner.runWithWaitForAnimatorTimeout(TIMEOUT_MS);
    }

    private void ensureSceneLayersExist() {
        if (mTabSceneLayer == null) {
            mTabSceneLayer = new StaticTabSceneLayer();
            if (mTabContentManager != null) {
                mTabSceneLayer.setTabContentManager(mTabContentManager);
            }
        }
        if (mEmptySceneLayer == null) {
            mEmptySceneLayer = new SolidColorSceneLayer();
        }
        if (mCurrentSceneLayer == null && mEmptySceneLayer != null) {
            mCurrentSceneLayer = mEmptySceneLayer;
        }
    }

    private boolean hasLayoutTab() {
        return mLayoutTabs != null && mLayoutTabs.length > 0;
    }

    private LayoutTab getLayoutTab() {
        assert hasLayoutTab();
        return mLayoutTabs[0];
    }

    private void createLayoutTabForTabId(int tabId) {
        LayoutTab layoutTab = createLayoutTab(tabId, mTabModelSelector.isIncognitoSelected());
        mLayoutTabs = new LayoutTab[] {layoutTab};
        updateCacheVisibleIds(Collections.singletonList(tabId));
    }

    private void resetLayoutTabs(boolean clearVisibleIds) {
        if (clearVisibleIds) {
            // Clear the visible IDs as once mLayoutTabs is empty tabs thumbnails cannot be
            // captured. This prevents thumbnail requests from waiting indefinitely.
            updateCacheVisibleIds(Collections.emptyList());
        }

        // mLayoutTabs is used in conjunction with mTabSceneLayer to capture a tab thumbnail for
        // the last visible Tab prior to transitioning to the Hub. This should be nulled once
        // the capture is completed.
        mLayoutTabs = null;
    }

    private void hideCurrentTab() {
        Tab currentTab = mTabModelSelector.getCurrentTab();
        if (currentTab != null) {
            currentTab.hide(TabHidingType.TAB_SWITCHER_SHOWN);
        }
    }

    private void updateEmptyLayerColor(@Nullable Pane pane) {
        if (mEmptySceneLayer == null) return;

        mEmptySceneLayer.setBackgroundColor(mHubController.getBackgroundColor(pane));
    }

    private void captureTabThumbnail(
            @Nullable Tab currentTab, @NonNull Promise<Bitmap> bitmapPromise) {
        if (currentTab == null) {
            bitmapPromise.fulfill(null);
            return;
        }

        mTabContentManager.cacheTabThumbnailWithCallback(
                currentTab,
                /* returnBitmap= */ true,
                (bitmap) -> {
                    if (bitmap != null || !currentTab.isNativePage()) {
                        bitmapPromise.fulfill(bitmap);
                        return;
                    }

                    // NativePage may not produce a new bitmap if no state has changed. Refetch from
                    // disk. For a normal tab we can't do this fallback as the thumbnail may be
                    // stale.
                    mTabContentManager.getEtc1TabThumbnailWithCallback(
                            currentTab.getId(), bitmapPromise::fulfill);
                });
    }

    /**
     * Returns the tab id for a {@link Tab}.
     * @param tab The {@link Tab} to get an ID for or null.
     * @return the {@code tab}'s ID or {@link Tab#INVALID_TAB_ID} if null.
     */
    private int getIdForTab(@Nullable Tab tab) {
        return tab == null ? Tab.INVALID_TAB_ID : tab.getId();
    }

    /**
     * @return The y-offset for the container view that may be impacted by the status indicator and
     *     app header heights.
     */
    private float getContainerYOffset() {
        var params = (FrameLayout.LayoutParams) mHubController.getContainerView().getLayoutParams();
        return params.topMargin;
    }

    /**
     * Update the top margin and y-offset of the container view in response to an app header state
     * change.
     */
    private void maybeUpdateLayout() {
        int appHeaderHeight =
                (mDesktopWindowStateProvider != null
                                && mDesktopWindowStateProvider.getAppHeaderState() != null)
                        ? mDesktopWindowStateProvider.getAppHeaderState().getAppHeaderHeight()
                        : 0;
        mHubManager.setAppHeaderHeight(appHeaderHeight);
        // If the app header height or desktop windowing mode changes while the HubLayout is active,
        // adjust the container view's y-offset. This update will be posted because it has been
        // observed that an immediate update causes unexpected visual positioning in this scenario,
        // potentially because the top margin for the view is also updated at nearly the same time,
        // but the root cause is unknown.
        // TODO (crbug/335651375): Investigate and remove the use of a PostTask for this update.
        // TODO (crbug/334156232): Also update container height.
        if (isActive()) {
            PostTask.postTask(
                    TaskTraits.UI_DEFAULT,
                    () -> mHubController.getContainerView().setY(getContainerYOffset()));
        }
    }

    public HubController getHubControllerForTesting() {
        return mHubController;
    }
}