chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/LayoutManagerImpl.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.content.Context;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.TraceEvent;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.back_press.BackPressManager;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsUtils;
import org.chromium.chrome.browser.browser_controls.BrowserControlsVisibilityManager;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchPanel;
import org.chromium.chrome.browser.compositor.layouts.Layout.Orientation;
import org.chromium.chrome.browser.compositor.layouts.components.LayoutTab;
import org.chromium.chrome.browser.compositor.overlays.strip.StripLayoutHelperManager;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fullscreen.BrowserControlsManager;
import org.chromium.chrome.browser.layouts.CompositorModelChangeProcessor;
import org.chromium.chrome.browser.layouts.EventFilter;
import org.chromium.chrome.browser.layouts.EventFilter.EventType;
import org.chromium.chrome.browser.layouts.LayoutType;
import org.chromium.chrome.browser.layouts.ManagedLayoutManager;
import org.chromium.chrome.browser.layouts.SceneOverlay;
import org.chromium.chrome.browser.layouts.animation.CompositorAnimationHandler;
import org.chromium.chrome.browser.layouts.components.VirtualView;
import org.chromium.chrome.browser.layouts.scene_layer.SceneLayer;
import org.chromium.chrome.browser.layouts.scene_layer.SceneOverlayLayer;
import org.chromium.chrome.browser.readaloud.ReadAloudMiniPlayerSceneLayer;
import org.chromium.chrome.browser.status_indicator.StatusIndicatorCoordinator;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabLaunchType;
import org.chromium.chrome.browser.tab.TabSelectionType;
import org.chromium.chrome.browser.tab_ui.TabContentManager;
import org.chromium.chrome.browser.tabmodel.TabCreatorManager;
import org.chromium.chrome.browser.tabmodel.TabModel;
import org.chromium.chrome.browser.tabmodel.TabModelObserver;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.chrome.browser.tabmodel.TabModelUtils;
import org.chromium.chrome.browser.theme.ThemeUtils;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.toolbar.ControlContainer;
import org.chromium.chrome.browser.toolbar.bottom.ScrollingBottomViewSceneLayer;
import org.chromium.chrome.browser.toolbar.top.TopToolbarOverlayCoordinator;
import org.chromium.chrome.browser.ui.edge_to_edge.EdgeToEdgeBottomChinSceneLayer;
import org.chromium.components.browser_ui.widget.gesture.BackPressHandler;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.base.SPenSupport;
import org.chromium.ui.modelutil.PropertyKey;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import org.chromium.ui.util.TokenHolder;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A class that is responsible for managing an active {@link Layout} to show to the screen.  This
 * includes lifecycle managment like showing/hiding this {@link Layout}.
 */
public class LayoutManagerImpl
        implements ManagedLayoutManager, LayoutUpdateHost, LayoutProvider, BackPressHandler {
    /** Sampling at 60 fps. */
    private static final long FRAME_DELTA_TIME_MS = 16;

    /** Used to convert pixels to dp. */
    protected final float mPxToDp;

    /** The {@link LayoutManagerHost}, who is responsible for showing the active {@link Layout}. */
    protected final LayoutManagerHost mHost;

    /** The last X coordinate of the last {@link MotionEvent#ACTION_DOWN} event. */
    protected int mLastTapX;

    /** The last Y coordinate of the last {@link MotionEvent#ACTION_DOWN} event. */
    protected int mLastTapY;

    // Layouts
    /** A {@link Layout} used for showing a normal web page. */
    protected StaticLayout mStaticLayout;

    private final ViewGroup mContentContainer;

    // External Dependencies
    private TabModelSelector mTabModelSelector;

    private final Callback<TabModel> mCurrentTabModelObserver =
            (tabModel) -> {
                tabModelSwitched(tabModel.isIncognitoBranded());
            };
    private TabModelSelectorTabObserver mTabModelSelectorTabObserver;

    // An observer for watching TabModelFilters changes events.
    private TabModelObserver mTabModelFilterObserver;

    // External Observers
    private final ObserverList<LayoutStateObserver> mLayoutObservers = new ObserverList<>();
    // TODO(crbug.com/40141330): Remove after all SceneChangeObserver migrates to
    // LayoutStateObserver.
    private final ObserverList<SceneChangeObserver> mSceneChangeObservers = new ObserverList<>();

    // Current Layout State
    private Layout mActiveLayout;
    private Layout mNextActiveLayout;
    private boolean mAnimateNextLayout;

    // Current Event Fitler State
    private EventFilter mActiveEventFilter;

    // Internal State
    private final SparseArray<LayoutTab> mTabCache = new SparseArray<>();
    private int mControlsShowingToken = TokenHolder.INVALID_TOKEN;
    private int mControlsHidingToken = TokenHolder.INVALID_TOKEN;
    private boolean mUpdateRequested;
    private final OverlayPanelManager mOverlayPanelManager;

    private final Context mContext;

    // Whether or not the last layout was showing the browser controls.
    private boolean mPreviousLayoutShowingToolbar;

    // Used to store the visible viewport and not create a new Rect object every frame.
    private final RectF mCachedVisibleViewport = new RectF();
    private final RectF mCachedWindowViewport = new RectF();

    private final RectF mCachedRect = new RectF();
    private final PointF mCachedPoint = new PointF();

    // Whether the currently active event filter has changed.
    private boolean mIsNewEventFilter;

    /** The animation handler responsible for updating all the browser compositor's animations. */
    private final CompositorAnimationHandler mAnimationHandler;

    private final ObservableSupplierImpl<TabModelSelector> mTabModelSelectorSupplier =
            new ObservableSupplierImpl<>();
    private final ObservableSupplier<TabContentManager> mTabContentManagerSupplier;
    private final CompositorModelChangeProcessor.FrameRequestSupplier mFrameRequestSupplier;

    private BrowserControlsStateProvider mBrowserControlsStateProvider;

    /** The overlays that can be drawn on top of the active layout. */
    protected final List<SceneOverlay> mSceneOverlays = new ArrayList<>();

    /** A map of {@link SceneOverlay} to its position relative to the others. */
    private Map<Class, Integer> mOverlayOrderMap = new HashMap<>();

    /** The supplier of {@link ThemeColorProvider} for top UI. */
    private final Supplier<TopUiThemeColorProvider> mTopUiThemeColorProvider;

    /** The supplier of whether this is going to intercept back press gesture. */
    private final ObservableSupplierImpl<Boolean> mHandleBackPressChangedSupplier =
            new ObservableSupplierImpl<>();

    /** When non-null, #doneShowing should call into the sequencer instead of doing normal work. */
    private ShowingEventSequencer mShowingEventSequencer;

    /**
     * Protected class to handle {@link TabModelObserver} related tasks. Extending classes will
     * need to override any related calls to add new functionality
     */
    protected class LayoutManagerTabModelObserver implements TabModelObserver {
        @Override
        public void didSelectTab(Tab tab, @TabSelectionType int type, int lastId) {
            if (type == TabSelectionType.FROM_OMNIBOX) {
                switchToTab(tab, lastId);
            } else if (tab.getId() != lastId) {
                tabSelected(tab.getId(), lastId, tab.isIncognito());
            }
        }

        @Override
        public void willAddTab(Tab tab, @TabLaunchType int type) {
            // Open the new tab
            if (type == TabLaunchType.FROM_RESTORE
                    || type == TabLaunchType.FROM_REPARENTING
                    || type == TabLaunchType.FROM_EXTERNAL_APP
                    || type == TabLaunchType.FROM_LAUNCHER_SHORTCUT
                    || type == TabLaunchType.FROM_STARTUP
                    || type == TabLaunchType.FROM_APP_WIDGET
                    || type == TabLaunchType.FROM_SYNC_BACKGROUND) {
                return;
            }

            tabCreating(getTabModelSelector().getCurrentTabId(), tab.isIncognito());
        }

        @Override
        public void didAddTab(
                Tab tab,
                @TabLaunchType int launchType,
                @TabCreationState int creationState,
                boolean markedForSelection) {
            int tabId = tab.getId();
            if (launchType == TabLaunchType.FROM_RESTORE) {
                getActiveLayout().onTabRestored(time(), tabId);
            } else {
                boolean incognito = tab.isIncognito();
                boolean willBeSelected =
                        launchType != TabLaunchType.FROM_LONGPRESS_BACKGROUND
                                        && launchType
                                                != TabLaunchType.FROM_LONGPRESS_BACKGROUND_IN_GROUP
                                        && launchType != TabLaunchType.FROM_RECENT_TABS
                                        && launchType != TabLaunchType.FROM_RESTORE_TABS_UI
                                        && launchType != TabLaunchType.FROM_SYNC_BACKGROUND
                                || (!getTabModelSelector().isIncognitoSelected() && incognito);
                float lastTapX = LocalizationUtils.isLayoutRtl() ? mHost.getWidth() * mPxToDp : 0.f;
                float lastTapY = 0.f;
                if (launchType != TabLaunchType.FROM_CHROME_UI) {
                    lastTapX = mPxToDp * mLastTapX;
                    lastTapY = mPxToDp * mLastTapY;
                }

                tabCreated(
                        tabId,
                        getTabModelSelector().getCurrentTabId(),
                        launchType,
                        incognito,
                        willBeSelected,
                        lastTapX,
                        lastTapY);
            }
        }

        @Override
        public void willCloseAllTabs(boolean isIncognito) {
            onTabsAllClosing(isIncognito);
        }

        @Override
        public void onFinishingTabClosure(Tab tab) {
            tabClosed(tab.getId(), tab.isIncognito(), false);
        }

        @Override
        public void tabPendingClosure(Tab tab) {
            tabClosed(tab.getId(), tab.isIncognito(), false);
        }

        @Override
        public void multipleTabsPendingClosure(List<Tab> tabs, boolean isAllTabs) {
            // Handled by willCloseAllTabs;
            if (isAllTabs) return;

            for (Tab tab : tabs) {
                tabClosed(tab.getId(), tab.isIncognito(), false);
            }
        }

        @Override
        public void tabClosureCommitted(Tab tab) {
            LayoutManagerImpl.this.tabClosureCommitted(tab.getId(), tab.isIncognito());
        }

        @Override
        public void tabRemoved(Tab tab) {
            tabClosed(tab.getId(), tab.isIncognito(), true);
        }
    }

    /**
     * Scoped class that temporarily delays doneShowing. This stops reentrancy from Layouts without
     * animations that try to call {@link #doneShowing()} immediately. The done showing transition
     * is different from all the others in that the manager drives it instead, instead of the
     * layouts calling up into the host to drive it. This is why only done showing needs help
     * stopping reentrancy.
     */
    private class ShowingEventSequencer implements AutoCloseable {
        private boolean mPendingDoneShowing;

        private ShowingEventSequencer() {
            assert LayoutManagerImpl.this.mShowingEventSequencer == null;
            LayoutManagerImpl.this.mShowingEventSequencer = this;
        }

        @Override
        public void close() {
            assert LayoutManagerImpl.this.mShowingEventSequencer == this;
            LayoutManagerImpl.this.mShowingEventSequencer = null;
            if (mPendingDoneShowing) {
                LayoutManagerImpl.this.doneShowing();
            }
        }

        public void setPendingDoneShowing() {
            mPendingDoneShowing = true;
        }
    }

    /**
     * Creates a {@link LayoutManagerImpl} instance.
     * @param host A {@link LayoutManagerHost} instance.
     * @param contentContainer A {@link ViewGroup} for Android views to be bound to.
     * @param tabContentManagerSupplier Supplier of the {@link TabContentManager} instance.
     * @param topUiThemeColorProvider {@link ThemeColorProvider} for top UI.
     */
    public LayoutManagerImpl(
            LayoutManagerHost host,
            ViewGroup contentContainer,
            ObservableSupplier<TabContentManager> tabContentManagerSupplier,
            Supplier<TopUiThemeColorProvider> topUiThemeColorProvider) {
        mHost = host;
        mPxToDp = 1.f / mHost.getContext().getResources().getDisplayMetrics().density;
        mTabContentManagerSupplier = tabContentManagerSupplier;
        mTopUiThemeColorProvider = topUiThemeColorProvider;
        mContext = host.getContext();

        // Overlays are ordered back (closest to the web content) to front.
        Class[] overlayOrder;

        overlayOrder =
                new Class[] {
                    // Place the tab strip behind the toolbar scene layer as during tab strip
                    // transition, the toolbar will move up and cover the tab strip.
                    StripLayoutHelperManager.class,
                    TopToolbarOverlayCoordinator.class,
                    EdgeToEdgeBottomChinSceneLayer.class,
                    // StripLayoutHelperManager should be updated before
                    // ScrollingBottomViewSceneLayer Since ScrollingBottomViewSceneLayer change
                    // the container size, it causes relocation tab strip scene layer.
                    ScrollingBottomViewSceneLayer.class,
                    StatusIndicatorCoordinator.getSceneOverlayClass(),
                    ContextualSearchPanel.class,
                    ReadAloudMiniPlayerSceneLayer.class
                };

        for (int i = 0; i < overlayOrder.length; i++) mOverlayOrderMap.put(overlayOrder[i], i);

        assert contentContainer != null;
        mContentContainer = contentContainer;

        mAnimationHandler = new CompositorAnimationHandler(this::requestUpdate);

        mOverlayPanelManager = new OverlayPanelManager();

        mFrameRequestSupplier =
                new CompositorModelChangeProcessor.FrameRequestSupplier(this::requestUpdate);
    }

    /**
     * @return The layout manager's panel manager.
     */
    public OverlayPanelManager getOverlayPanelManager() {
        return mOverlayPanelManager;
    }

    @Override
    public CompositorAnimationHandler getAnimationHandler() {
        return mAnimationHandler;
    }

    /**
     * @return The actual current time of the app in ms.
     */
    public static long time() {
        return SystemClock.uptimeMillis();
    }

    /**
     * Gives the {@link LayoutManagerImpl} a chance to intercept and process motion events from the
     * Android {@link View} system.
     * @param e                 The {@link MotionEvent} that might be intercepted.
     * @param isKeyboardShowing Whether or not the keyboard is showing.
     * @param eventType         The type of input event that is processed by an {@link EventFilter}.
     * @return                  Whether or not this current motion event should be intercepted and
     *                          continually forwarded to this class.
     */
    public boolean onInterceptMotionEvent(
            MotionEvent e, boolean isKeyboardShowing, @EventType int eventType) {
        if (mActiveLayout == null) return false;

        if (e.getAction() == MotionEvent.ACTION_DOWN) {
            mLastTapX = (int) e.getX();
            mLastTapY = (int) e.getY();
        }

        PointF offsets = getMotionOffsets(e);

        // The last added overlay will be drawn on top of everything else, therefore the last
        // filter added should have the first chance to intercept any motion events.
        EventFilter layoutFilter = null;
        for (int i = mSceneOverlays.size() - 1; i >= 0; i--) {
            if (!mSceneOverlays.get(i).isSceneOverlayTreeShowing()) continue;
            EventFilter eventFilter = mSceneOverlays.get(i).getEventFilter();
            if (eventFilter == null) continue;
            if (offsets != null) eventFilter.setCurrentMotionEventOffsets(offsets.x, offsets.y);
            if (isEventInterceptedByEventFilter(e, eventFilter, eventType, isKeyboardShowing)) {
                layoutFilter = eventFilter;
                break;
            }
        }

        // If no overlay's filter took the event, check the layout.
        if (layoutFilter == null) {
            layoutFilter = mActiveLayout.findInterceptingEventFilter(e, offsets, isKeyboardShowing);
        }

        mIsNewEventFilter = layoutFilter != mActiveEventFilter;
        mActiveEventFilter = layoutFilter;

        if (mActiveEventFilter != null) mActiveLayout.unstallImmediately();

        return mActiveEventFilter != null;
    }

    private boolean isEventInterceptedByEventFilter(
            MotionEvent event,
            EventFilter eventFilter,
            @EventType int eventType,
            boolean isKeyboardShowing) {
        switch (eventType) {
            case EventType.TOUCH:
                return eventFilter.onInterceptTouchEvent(event, isKeyboardShowing);
            case EventType.HOVER:
                return eventFilter.onInterceptHoverEvent(event);
            default:
                break;
        }
        return false;
    }

    /**
     * Gives the {@link LayoutManagerImpl} a chance to process the touch events from the Android
     * {@link View} system.
     * @param e A {@link MotionEvent} instance.
     * @return  Whether or not {@code e} was consumed.
     */
    public boolean onTouchEvent(MotionEvent e) {
        if (mActiveEventFilter == null) return false;

        // Make sure the first event through the filter is an ACTION_DOWN.
        if (mIsNewEventFilter && e.getActionMasked() != MotionEvent.ACTION_DOWN) {
            MotionEvent downEvent = MotionEvent.obtain(e);
            downEvent.setAction(MotionEvent.ACTION_DOWN);
            if (!onTouchEventInternal(downEvent)) return false;
        }
        mIsNewEventFilter = false;

        return onTouchEventInternal(e);
    }

    private boolean onTouchEventInternal(MotionEvent e) {
        boolean consumed = mActiveEventFilter.onTouchEvent(e);
        PointF offsets = getMotionOffsets(e);
        if (offsets != null) mActiveEventFilter.setCurrentMotionEventOffsets(offsets.x, offsets.y);
        return consumed;
    }

    /**
     * Gives the {@link LayoutManagerImpl} a chance to process the hover events from the Android
     * {@link View} system.
     * @param e A {@link MotionEvent} instance.
     * @return  Whether or not {@code e} was consumed.
     */
    public boolean onHoverEvent(MotionEvent e) {
        if (mActiveEventFilter == null) return false;

        // Make sure the first event through the filter is an ACTION_HOVER_ENTER.
        if (mIsNewEventFilter && e.getActionMasked() != MotionEvent.ACTION_HOVER_ENTER) {
            MotionEvent hoverEnterEvent = MotionEvent.obtain(e);
            hoverEnterEvent.setAction(MotionEvent.ACTION_HOVER_ENTER);
            if (!onHoverEventInternal(hoverEnterEvent)) return false;
        }
        mIsNewEventFilter = false;

        return onHoverEventInternal(e);
    }

    private boolean onHoverEventInternal(MotionEvent e) {
        boolean consumed = mActiveEventFilter.onHoverEvent(e);
        PointF offsets = getMotionOffsets(e);
        if (offsets != null) mActiveEventFilter.setCurrentMotionEventOffsets(offsets.x, offsets.y);
        return consumed;
    }

    private PointF getMotionOffsets(MotionEvent e) {
        int actionMasked = SPenSupport.convertSPenEventAction(e.getActionMasked());

        if (actionMasked == MotionEvent.ACTION_DOWN
                || actionMasked == MotionEvent.ACTION_HOVER_ENTER) {
            getViewportPixel(mCachedRect);

            mCachedPoint.set(-mCachedRect.left, -mCachedRect.top);
            return mCachedPoint;
        } else if (actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_CANCEL
                || actionMasked == MotionEvent.ACTION_HOVER_EXIT) {
            mCachedPoint.set(0, 0);
            return mCachedPoint;
        }

        return null;
    }

    /**
     * Updates the state of the active {@link Layout} if needed.  This updates the animations and
     * cascades the changes to the tabs.
     */
    public void onUpdate() {
        TraceEvent.begin("LayoutDriver:onUpdate");
        onUpdate(time(), FRAME_DELTA_TIME_MS);
        TraceEvent.end("LayoutDriver:onUpdate");
    }

    /**
     * Updates the state of the layout.
     * @param timeMs The time in milliseconds.
     * @param dtMs   The delta time since the last update in milliseconds.
     * @return       Whether or not the {@link LayoutManagerImpl} needs more updates.
     */
    @VisibleForTesting
    boolean onUpdate(long timeMs, long dtMs) {
        if (!mUpdateRequested) {
            mFrameRequestSupplier.set(timeMs);
            return false;
        }
        mUpdateRequested = false;

        // TODO(crbug.com/40126259): Remove after the FrameRequestSupplier migrates to the animation
        //  system.
        final Layout layout = getActiveLayout();

        // TODO(mdjones): Remove the time related params from this method. The new animation system
        // has its own timer.
        boolean areAnimatorsComplete = mAnimationHandler.pushUpdate();
        if (layout != null) {
            areAnimatorsComplete &= !layout.isRunningAnimations();
        }

        // TODO(crbug.com/40126259): Layout itself should decide when it's done hiding and done
        //  showing.
        if (layout != null && layout.onUpdate(timeMs, dtMs) && areAnimatorsComplete) {
            if (layout.isStartingToHide()) {
                layout.doneHiding();
            } else if (layout.isStartingToShow()) {
                layout.doneShowing();
            }
        }

        // TODO(crbug.com/40137900): Once overlays are MVC, this should no longer be needed.
        for (int i = 0; i < mSceneOverlays.size(); i++) {
            mSceneOverlays.get(i).updateOverlay(timeMs, dtMs);
        }

        mFrameRequestSupplier.set(timeMs);
        return mUpdateRequested;
    }

    /**
     * Initializes the {@link LayoutManagerImpl}.  Must be called before using this object.
     * @param selector                 A {@link TabModelSelector} instance.
     * @param creator                  A {@link TabCreatorManager} instance.
     * @param controlContainer         A {@link ControlContainer} for browser controls' layout.
     * @param dynamicResourceLoader    A {@link DynamicResourceLoader} instance.
     * @param topUiColorProvider       A theme color provider for the top browser controls.
     */
    public void init(
            TabModelSelector selector,
            TabCreatorManager creator,
            @Nullable ControlContainer controlContainer,
            DynamicResourceLoader dynamicResourceLoader,
            TopUiThemeColorProvider topUiColorProvider) {
        LayoutRenderHost renderHost = mHost.getLayoutRenderHost();

        mBrowserControlsStateProvider = mHost.getBrowserControlsManager();

        // Build Layouts
        mStaticLayout =
                new StaticLayout(
                        mContext,
                        this,
                        renderHost,
                        mHost,
                        mFrameRequestSupplier,
                        selector,
                        mTabContentManagerSupplier.get(),
                        mBrowserControlsStateProvider,
                        mTopUiThemeColorProvider);

        setNextLayout(null, true);

        // Set the dynamic resource loader for all overlay panels.
        mOverlayPanelManager.setDynamicResourceLoader(dynamicResourceLoader);
        mOverlayPanelManager.setContainerView(mContentContainer);

        // The {@link setTabModelSelector} should be called after all of the initialization above
        // complete. See https://crbug.com/1132948.
        if (mTabModelSelector == null) {
            setTabModelSelector(selector);
        }
    }

    // TODO(hanxi): Passes the TabModelSelectorSupplier in the constructor since the
    // mTabModelSelector should only be set once.
    public void setTabModelSelector(TabModelSelector selector) {
        mTabModelSelector = selector;
        mTabModelSelectorSupplier.set(selector);
        mTabModelSelectorTabObserver =
                new TabModelSelectorTabObserver(mTabModelSelector) {
                    @Override
                    public void onShown(Tab tab, @TabSelectionType int type) {
                        initLayoutTabFromHost(tab.getId());
                    }

                    @Override
                    public void onHidden(Tab tab, @TabHidingType int type) {
                        initLayoutTabFromHost(tab.getId());
                    }

                    @Override
                    public void onContentChanged(Tab tab) {
                        initLayoutTabFromHost(tab.getId());
                    }

                    @Override
                    public void onBackgroundColorChanged(Tab tab, int color) {
                        // The NavBarColorMatchesTabBackground increases the frequency of these
                        // notifications, so Chrome should use a more targeted method to limit
                        // performance impact.
                        if (ChromeFeatureList.sNavBarColorMatchesTabBackground.isEnabled()) {
                            updateLayoutTabBackgroundColor(tab.getId());
                        } else {
                            initLayoutTabFromHost(tab.getId());
                        }
                    }

                    @Override
                    public void onDidChangeThemeColor(Tab tab, int color) {
                        initLayoutTabFromHost(tab.getId());
                    }
                };

        if (mNextActiveLayout != null) startShowing(mNextActiveLayout, true);

        selector.getCurrentTabModelSupplier().addObserver(mCurrentTabModelObserver);

        mTabModelFilterObserver = createTabModelObserver();
        getTabModelSelector()
                .getTabModelFilterProvider()
                .addTabModelFilterObserver(mTabModelFilterObserver);
    }

    @Override
    public void destroy() {
        mAnimationHandler.destroy();
        mSceneChangeObservers.clear();
        if (mStaticLayout != null) mStaticLayout.destroy();
        if (mOverlayPanelManager != null) mOverlayPanelManager.destroy();
        if (mTabModelSelectorTabObserver != null) mTabModelSelectorTabObserver.destroy();
        if (getTabModelSelector() != null) {
            getTabModelSelector()
                    .getCurrentTabModelSupplier()
                    .removeObserver(mCurrentTabModelObserver);
        }
        if (mTabModelFilterObserver != null) {
            getTabModelSelector()
                    .getTabModelFilterProvider()
                    .removeTabModelFilterObserver(mTabModelFilterObserver);
        }
    }

    /** @return A resource manager to pull textures from. */
    public ResourceManager getResourceManager() {
        if (mHost.getLayoutRenderHost() == null) return null;
        return mHost.getLayoutRenderHost().getResourceManager();
    }

    @Override
    public <V extends SceneLayer> CompositorModelChangeProcessor<V> createCompositorMCP(
            PropertyModel model,
            V view,
            PropertyModelChangeProcessor.ViewBinder<PropertyModel, V, PropertyKey> viewBinder) {
        return CompositorModelChangeProcessor.create(
                model, view, viewBinder, mFrameRequestSupplier, true);
    }

    /**
     * @param observer Adds {@code observer} to be notified when the active {@code Layout} changes.
     */
    public void addSceneChangeObserver(SceneChangeObserver observer) {
        mSceneChangeObservers.addObserver(observer);
    }

    /**
     * @param observer Removes {@code observer}.
     */
    public void removeSceneChangeObserver(SceneChangeObserver observer) {
        mSceneChangeObservers.removeObserver(observer);
    }

    @Override
    public SceneLayer getUpdatedActiveSceneLayer(
            TabContentManager tabContentManager,
            ResourceManager resourceManager,
            BrowserControlsManager browserControlsManager) {
        updateControlsHidingState(browserControlsManager);
        getViewportPixel(mCachedVisibleViewport);
        mHost.getWindowViewport(mCachedWindowViewport);
        SceneLayer layer =
                mActiveLayout.getUpdatedSceneLayer(
                        mCachedWindowViewport,
                        mCachedVisibleViewport,
                        tabContentManager,
                        resourceManager,
                        browserControlsManager);

        float offsetPx =
                mBrowserControlsStateProvider == null
                        ? 0
                        : mBrowserControlsStateProvider.getTopControlOffset();

        for (int i = 0; i < mSceneOverlays.size(); i++) {
            // If the SceneOverlay is not showing, don't bother adding it to the tree.
            if (!mSceneOverlays.get(i).isSceneOverlayTreeShowing()) continue;

            SceneOverlayLayer overlayLayer =
                    mSceneOverlays
                            .get(i)
                            .getUpdatedSceneOverlayTree(
                                    mCachedWindowViewport,
                                    mCachedVisibleViewport,
                                    resourceManager,
                                    offsetPx * mPxToDp);

            overlayLayer.setContentTree(layer);
            layer = overlayLayer;
        }

        return layer;
    }

    private void updateControlsHidingState(
            BrowserControlsVisibilityManager controlsVisibilityManager) {
        if (controlsVisibilityManager == null) {
            return;
        }

        boolean overlayHidesControls = false;
        for (int i = 0; i < mSceneOverlays.size(); i++) {
            // If any overlay wants to hide tha Android version of the browser controls, hide them.
            if (mSceneOverlays.get(i).shouldHideAndroidBrowserControls()) {
                overlayHidesControls = true;
                break;
            }
        }

        if (overlayHidesControls || mActiveLayout.forceHideBrowserControlsAndroidView()) {
            mControlsHidingToken =
                    controlsVisibilityManager.hideAndroidControlsAndClearOldToken(
                            mControlsHidingToken);
        } else {
            controlsVisibilityManager.releaseAndroidControlsHidingToken(mControlsHidingToken);
        }
    }

    /** Called when the viewport has been changed. */
    public void onViewportChanged() {
        if (getActiveLayout() != null) {
            float previousWidth = getActiveLayout().getWidth();
            float previousHeight = getActiveLayout().getHeight();

            float oldWindowViewportTop = mCachedWindowViewport.top;
            float oldVisibleViewportTop = mCachedVisibleViewport.top;
            mHost.getWindowViewport(mCachedWindowViewport);
            mHost.getVisibleViewport(mCachedVisibleViewport);
            getActiveLayout().sizeChanged(mCachedWindowViewport, getOrientation());

            float width = mCachedWindowViewport.width() * mPxToDp;
            float height = mCachedWindowViewport.height() * mPxToDp;
            if (width != previousWidth
                    || height != previousHeight
                    // TODO (crbug.com/325501037) - Clean up this odd check comparing the window
                    // and visible viewport values after fixing the contextual search menu's
                    // reliance on it.
                    || oldWindowViewportTop != mCachedVisibleViewport.top
                    || oldVisibleViewportTop != mCachedVisibleViewport.top) {
                for (int i = 0; i < mSceneOverlays.size(); i++) {
                    mSceneOverlays
                            .get(i)
                            .onSizeChanged(
                                    width, height, mCachedVisibleViewport.top, getOrientation());
                }
            }
        }

        for (int i = 0; i < mTabCache.size(); i++) {
            // This assumes that the content width/height is always the size of the host.
            mTabCache.valueAt(i).setContentSize(mHost.getWidth(), mHost.getHeight());
        }
    }

    /**
     * @return The default {@link Layout} to show when {@link Layout}s get hidden and the next
     *         {@link Layout} to show isn't known.
     */
    protected Layout getDefaultLayout() {
        return mStaticLayout;
    }

    /**
     * @return The {@link TabModelObserver} instance this class should be using.
     */
    protected LayoutManagerChrome.LayoutManagerTabModelObserver createTabModelObserver() {
        return new LayoutManagerChrome.LayoutManagerTabModelObserver();
    }

    @VisibleForTesting
    public void tabSelected(int tabId, int prevId, boolean incognito) {
        // Update the model here so we properly set the right selected TabModel.
        if (getActiveLayout() != null) {
            getActiveLayout().onTabSelected(time(), tabId, prevId, incognito);
        }
    }

    /**
     * Should be called when a tab creating event is triggered (called before the tab is done being
     * created).
     * @param sourceId    The id of the creating tab if any.
     * @param url         The url of the created tab.
     * @param isIncognito Whether or not created tab will be incognito.
     */
    protected void tabCreating(int sourceId, boolean isIncognito) {
        if (getActiveLayout() != null) getActiveLayout().onTabCreating(sourceId);
    }

    /**
     * Should be called when a tab created event is triggered.
     * @param id             The id of the tab that was created.
     * @param sourceId       The id of the creating tab if any.
     * @param launchType     How the tab was launched.
     * @param incognito      Whether or not the created tab is incognito.
     * @param willBeSelected Whether or not the created tab will be selected.
     * @param originX        The x coordinate of the action that created this tab in dp.
     * @param originY        The y coordinate of the action that created this tab in dp.
     */
    protected void tabCreated(
            int id,
            int sourceId,
            @TabLaunchType int launchType,
            boolean incognito,
            boolean willBeSelected,
            float originX,
            float originY) {
        int newIndex = TabModelUtils.getTabIndexById(getTabModelSelector().getModel(incognito), id);
        getActiveLayout()
                .onTabCreated(
                        time(),
                        id,
                        newIndex,
                        sourceId,
                        incognito,
                        !willBeSelected,
                        originX,
                        originY);
    }

    /**
     * Should be called when a tab closed event is triggered.
     * @param id         The id of the closed tab.
     * @param nextId     The id of the next tab that will be visible, if any.
     * @param incognito  Whether or not the closed tab is incognito.
     * @param tabRemoved Whether the tab was removed from the model (e.g. for reparenting), rather
     *                   than closed and destroyed.
     */
    protected void tabClosed(int id, int nextId, boolean incognito, boolean tabRemoved) {
        if (getActiveLayout() != null) getActiveLayout().onTabClosed(time(), id, nextId, incognito);
    }

    private void tabClosed(int tabId, boolean incognito, boolean tabRemoved) {
        Tab currentTab =
                getTabModelSelector() != null ? getTabModelSelector().getCurrentTab() : null;
        int nextTabId = currentTab != null ? currentTab.getId() : Tab.INVALID_TAB_ID;
        tabClosed(tabId, nextTabId, incognito, tabRemoved);
    }

    /**
     * Called when a tab closure has been committed and all tab cleanup should happen.
     * @param id        The id of the closed tab.
     * @param incognito Whether or not the closed tab is incognito.
     */
    protected void tabClosureCommitted(int id, boolean incognito) {
        if (getActiveLayout() != null) {
            getActiveLayout().onTabClosureCommitted(time(), id, incognito);
        }
    }

    /**
     * Called when the selected tab model has switched.
     * @param incognito Whether or not the new current tab model is incognito.
     */
    protected void tabModelSwitched(boolean incognito) {
        if (getActiveLayout() != null) getActiveLayout().onTabModelSwitched(incognito);
    }

    public void onTabsAllClosing(boolean incognito) {
        if (getActiveLayout() == null) return;

        getActiveLayout().onTabsAllClosing(incognito);
    }

    protected Supplier<TopUiThemeColorProvider> getTopUiThemeColorProvider() {
        return mTopUiThemeColorProvider;
    }

    @Override
    public void initLayoutTabFromHost(final int tabId) {
        if (getTabModelSelector() == null || getActiveLayout() == null) return;

        TabModelSelector selector = getTabModelSelector();
        Tab tab = selector.getTabById(tabId);
        if (tab == null) return;

        LayoutTab layoutTab = mTabCache.get(tabId);
        if (layoutTab == null) return;

        GURL url = tab.getUrl();
        boolean isNativePage =
                tab.isNativePage() || url.getScheme().equals(UrlConstants.CHROME_NATIVE_SCHEME);

        boolean canUseLiveTexture =
                tab.getWebContents() != null
                        && !SadTab.isShowing(tab)
                        && !isNativePage
                        && !tab.isHidden();

        TopUiThemeColorProvider topUiTheme = mTopUiThemeColorProvider.get();
        layoutTab.initFromHost(
                topUiTheme.getBackgroundColor(tab),
                shouldStall(tab),
                canUseLiveTexture,
                topUiTheme.getSceneLayerBackground(tab),
                ThemeUtils.getTextBoxColorForToolbarBackground(
                        mContext, tab, topUiTheme.calculateColor(tab, tab.getThemeColor())));

        mHost.requestRender();
    }

    private void updateLayoutTabBackgroundColor(final int tabId) {
        if (getTabModelSelector() == null || getActiveLayout() == null) return;

        TabModelSelector selector = getTabModelSelector();
        Tab tab = selector.getTabById(tabId);
        if (tab == null) return;

        LayoutTab layoutTab = mTabCache.get(tabId);
        if (layoutTab == null) return;

        layoutTab.set(
                LayoutTab.BACKGROUND_COLOR, mTopUiThemeColorProvider.get().getBackgroundColor(tab));
    }

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

    @Override
    public LayoutTab createLayoutTab(
            int id, boolean incognito, float maxContentWidth, float maxContentHeight) {
        LayoutTab tab = mTabCache.get(id);
        if (tab == null) {
            tab = new LayoutTab(id, incognito, mHost.getWidth(), mHost.getHeight());
            mTabCache.put(id, tab);
        } else {
            tab.init(mHost.getWidth(), mHost.getHeight());
        }
        if (maxContentWidth > 0.f) tab.setMaxContentWidth(maxContentWidth);
        if (maxContentHeight > 0.f) tab.setMaxContentHeight(maxContentHeight);

        return tab;
    }

    @Override
    public void releaseTabLayout(int id) {
        mTabCache.remove(id);
    }

    @Override
    public void releaseResourcesForTab(int tabId) {}

    /**
     * @return The {@link TabModelSelector} instance this class knows about.
     */
    protected TabModelSelector getTabModelSelector() {
        return mTabModelSelector;
    }

    /**
     * @return The next {@link Layout} that will be shown.  If no {@link Layout} has been set
     *         since the last time {@link #startShowing(Layout, boolean)} was called, this will be
     *         {@link #getDefaultLayout()}.
     */
    protected Layout getNextLayout() {
        return mNextActiveLayout != null ? mNextActiveLayout : getDefaultLayout();
    }

    /** @return Whether a next layout has been explicitly specified. */
    protected boolean hasExplicitNextLayout() {
        return mNextActiveLayout != null;
    }

    @Override
    public Layout getActiveLayout() {
        return mActiveLayout;
    }

    @Override
    public void getViewportPixel(RectF rect) {
        if (getActiveLayout() == null) {
            mHost.getWindowViewport(rect);
            return;
        }

        switch (getActiveLayout().getViewportMode()) {
            case Layout.ViewportMode.ALWAYS_FULLSCREEN:
                mHost.getWindowViewport(rect);
                break;
            case Layout.ViewportMode.ALWAYS_SHOWING_BROWSER_CONTROLS:
                mHost.getViewportFullControls(rect);
                break;
            case Layout.ViewportMode.USE_PREVIOUS_BROWSER_CONTROLS_STATE:
                if (mPreviousLayoutShowingToolbar) {
                    mHost.getViewportFullControls(rect);
                } else {
                    mHost.getWindowViewport(rect);
                }
                break;
            case Layout.ViewportMode.DYNAMIC_BROWSER_CONTROLS:
            default:
                mHost.getVisibleViewport(rect);
        }
    }

    @Override
    public BrowserControlsManager getBrowserControlsManager() {
        return mHost != null ? mHost.getBrowserControlsManager() : null;
    }

    @Override
    public void requestUpdate() {
        requestUpdate(null);
    }

    @Override
    public void requestUpdate(Runnable onUpdateEffective) {
        if (mUpdateRequested && onUpdateEffective == null) return;
        mHost.requestRender(onUpdateEffective);
        mUpdateRequested = true;
    }

    @Override
    public void startHiding() {
        requestUpdate();
        Layout layoutBeingHidden = getActiveLayout();
        for (LayoutStateObserver observer : mLayoutObservers) {
            observer.onStartedHiding(layoutBeingHidden.getLayoutType());
        }
    }

    @Override
    public void doneHiding() {
        // TODO: If next layout is default layout clear caches (should this be a sub layout thing?)

        assert mNextActiveLayout != null : "Need to have a next active layout.";
        if (mNextActiveLayout != null) {
            // Notify LayoutObservers the active layout is finished hiding.
            for (LayoutStateObserver observer : mLayoutObservers) {
                observer.onFinishedHiding(getActiveLayout().getLayoutType());
            }

            startShowing(mNextActiveLayout, mAnimateNextLayout);
        }
    }

    @Override
    public void doneShowing() {
        if (mShowingEventSequencer != null) {
            mShowingEventSequencer.setPendingDoneShowing();
            return;
        }

        // Notify LayoutObservers the active layout is finished showing.
        for (LayoutStateObserver observer : mLayoutObservers) {
            observer.onFinishedShowing(getActiveLayout().getLayoutType());
        }
    }

    @Override
    public void showLayout(int layoutType, boolean animate) {
        Layout activeLayout = getActiveLayout();
        if (activeLayout != null && !activeLayout.isStartingToHide()) {
            setNextLayout(getLayoutForType(layoutType), animate);
            activeLayout.startHiding();
        } else {
            startShowing(getLayoutForType(layoutType), animate);
        }
    }

    /**
     * @param layoutType A layout type to get the implementation for.
     * @return The layout implementation for the provided type.
     */
    protected Layout getLayoutForType(@LayoutType int layoutType) {
        // TODO(crbug.com/40790324): Register these types and look them up in a map rather than
        // overriding this
        //                method in multiple places.
        // Use the static layout by default or if explicitly specified.
        if (layoutType == LayoutType.NONE || layoutType == LayoutType.BROWSING) {
            return mStaticLayout;
        }

        assert false : "Unsupported layout type: " + layoutType;
        return null;
    }

    /**
     * Should be called by control logic to show a new {@link Layout}.
     *
     * <p>TODO(dtrainor, clholgat): Clean up the show logic to guarantee startHiding/doneHiding get
     * called.
     *
     * @param layout The new {@link Layout} to show.
     * @param animate Whether or not {@code layout} should animate as it shows.
     */
    protected void startShowing(Layout layout, boolean animate) {
        assert layout != null : "Can't show a null layout.";

        // Set the new layout
        setNextLayout(null, true);
        Layout oldLayout = getActiveLayout();
        if (oldLayout != layout) {
            if (oldLayout != null) {
                oldLayout.forceAnimationToFinish();
                oldLayout.detachViews();

                // TODO(crbug.com/40141330): hide oldLayout if it's not hidden.
            }
            layout.contextChanged(mHost.getContext());
            layout.attachViews(mContentContainer);
            mActiveLayout = layout;
        }

        BrowserControlsVisibilityManager controlsVisibilityManager =
                mHost.getBrowserControlsManager();
        if (controlsVisibilityManager != null) {
            mPreviousLayoutShowingToolbar =
                    !BrowserControlsUtils.areBrowserControlsOffScreen(controlsVisibilityManager);

            // Release any old fullscreen token we were holding.
            controlsVisibilityManager
                    .getBrowserVisibilityDelegate()
                    .releasePersistentShowingToken(mControlsShowingToken);

            // Grab a new fullscreen token if this layout can't be in fullscreen.
            if (getActiveLayout().forceShowBrowserControlsAndroidView()) {
                mControlsShowingToken =
                        controlsVisibilityManager
                                .getBrowserVisibilityDelegate()
                                .showControlsPersistent();
            }
        }

        onViewportChanged();

        // In order to prevent another state transition in the middle of processing this one,
        // scopedSequencer will add itself as a member of this class, and then remove itself once
        // its scope is closed.
        try (ShowingEventSequencer scopedSequencer = new ShowingEventSequencer()) {
            getActiveLayout().show(time(), animate);
            mHost.setContentOverlayVisibility(
                    getActiveLayout().shouldDisplayContentOverlay(),
                    getActiveLayout().canHostBeFocusable());
            requestUpdate();

            // TODO(crbug.com/40141330): Remove after migrates to
            // LayoutStateObserver#onStartedShowing. Notify observers about the new scene.
            for (SceneChangeObserver observer : mSceneChangeObservers) {
                observer.onSceneChange(getActiveLayout());
            }

            for (LayoutStateObserver observer : mLayoutObservers) {
                observer.onStartedShowing(layout.getLayoutType());
            }
        }
    }

    /**
     * Sets the next {@link Layout} to show after the current {@link Layout} is finished and is done
     * hiding.
     * @param layout The new {@link Layout} to show.
     * @param animate Whether the next layout should be animated.
     */
    protected void setNextLayout(Layout layout, boolean animate) {
        mNextActiveLayout = (layout == null) ? getDefaultLayout() : layout;
        mAnimateNextLayout = animate;
    }

    @Override
    public @LayoutType int getNextLayoutType() {
        return mNextActiveLayout != null ? mNextActiveLayout.getLayoutType() : LayoutType.NONE;
    }

    @Override
    public boolean isActiveLayout(Layout layout) {
        return layout == mActiveLayout;
    }

    @Override
    public int getActiveLayoutType() {
        return getActiveLayout() != null ? getActiveLayout().getLayoutType() : LayoutType.NONE;
    }

    /**
     * Get a list of virtual views for accessibility.
     *
     * @param views A List to populate with virtual views.
     */
    public void getVirtualViews(List<VirtualView> views) {
        // Nothing to do here yet.
    }

    /**
     * @return The {@link SwipeHandler} responsible for processing swipe events for the normal
     *         toolbar. By default this returns null.
     */
    public SwipeHandler getToolbarSwipeHandler() {
        return null;
    }

    /**
     * Creates a {@link SwipeHandler} instance.
     * @param supportSwipeDown Whether or not to the handler should support swipe down gesture.
     * @return The {@link SwipeHandler} cerated.
     */
    public SwipeHandler createToolbarSwipeHandler(boolean supportSwipeDown) {
        return null;
    }

    /**
     * Should be called when the user presses the back button on the phone.
     * @return Whether or not the back button was consumed by the active {@link Layout}.
     */
    public boolean onBackPressed() {
        for (int i = 0; i < mSceneOverlays.size(); i++) {
            if (!mSceneOverlays.get(i).isSceneOverlayTreeShowing()) continue;

            // If the back button was consumed by any overlays, return true.
            if (mSceneOverlays.get(i).onBackPressed()) {
                BackPressManager.record(BackPressHandler.Type.SCENE_OVERLAY);
                return true;
            }
        }
        // Back press metrics of active layout is recorded by their implementations.
        return getActiveLayout() != null && getActiveLayout().onBackPressed();
    }

    @Override
    public @BackPressResult int handleBackPress() {
        for (SceneOverlay sceneOverlay : mSceneOverlays) {
            Boolean enabled = sceneOverlay.getHandleBackPressChangedSupplier().get();
            if (enabled != null && enabled) {
                return sceneOverlay.handleBackPress();
            }
        }
        return BackPressResult.FAILURE;
    }

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

    private void onBackPressStateChanged() {
        for (SceneOverlay sceneOverlay : mSceneOverlays) {
            Boolean enabled = sceneOverlay.getHandleBackPressChangedSupplier().get();
            if (enabled != null && enabled) {
                mHandleBackPressChangedSupplier.set(true);
                return;
            }
        }
        mHandleBackPressChangedSupplier.set(false);
    }

    @Override
    public void addSceneOverlay(SceneOverlay overlay) {
        if (mSceneOverlays.contains(overlay)) throw new RuntimeException("Overlay already added!");

        if (!mOverlayOrderMap.containsKey(overlay.getClass())) {
            throw new RuntimeException("Please add overlay to order list in constructor.");
        }

        int overlayPosition = mOverlayOrderMap.get(overlay.getClass());

        int index;
        for (index = 0; index < mSceneOverlays.size(); index++) {
            if (overlayPosition < mOverlayOrderMap.get(mSceneOverlays.get(index).getClass())) break;
        }

        mSceneOverlays.add(index, overlay);
        overlay.getHandleBackPressChangedSupplier().addObserver((v) -> onBackPressStateChanged());
    }

    void setSceneOverlayOrderForTesting(Map<Class, Integer> order) {
        mOverlayOrderMap = order;
    }

    List<SceneOverlay> getSceneOverlaysForTesting() {
        return mSceneOverlays;
    }

    /**
     * Clears all content associated with {@code tabId} from the internal caches.
     * @param tabId The id of the tab to clear.
     */
    protected void emptyTabCachesExcept(int tabId) {
        LayoutTab tab = mTabCache.get(tabId);
        mTabCache.clear();
        if (tab != null) mTabCache.put(tabId, tab);
    }

    private @Orientation int getOrientation() {
        return mHost.getWidth() > mHost.getHeight() ? Orientation.LANDSCAPE : Orientation.PORTRAIT;
    }

    public LayoutTab getLayoutTabForTesting(int tabId) {
        return mTabCache.get(tabId);
    }

    /**
     * Should be called when a tab switch event is triggered, only can switch to the Tab which in
     * the current TabModel.
     * @param tab        The tab that will be switched to.
     * @param lastTabId  The id of the tab that was switched from.
     */
    protected void switchToTab(Tab tab, int lastTabId) {
        tabSelected(tab.getId(), lastTabId, tab.isIncognito());
    }

    // LayoutStateProvider implementation.
    @Override
    public boolean isLayoutVisible(int layoutType) {
        return getActiveLayout() != null && getActiveLayout().getLayoutType() == layoutType;
    }

    @Override
    public boolean isLayoutStartingToHide(int layoutType) {
        return isLayoutVisible(layoutType) && getActiveLayout().isStartingToHide();
    }

    @Override
    public boolean isLayoutStartingToShow(int layoutType) {
        return isLayoutVisible(layoutType) && getActiveLayout().isStartingToShow();
    }

    @Override
    public void addObserver(LayoutStateObserver listener) {
        mLayoutObservers.addObserver(listener);
    }

    @Override
    public void removeObserver(LayoutStateObserver listener) {
        mLayoutObservers.removeObserver(listener);
    }
}