chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/CompositorViewHolder.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;

import static androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;

import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.accessibility.AccessibilityEvent;
import android.widget.FrameLayout;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.customview.widget.ExploreByTouchHelper;

import org.chromium.base.BuildInfo;
import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.SysUtils;
import org.chromium.base.TraceEvent;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.browser_controls.BrowserControlsUtils;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerHost;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.compositor.layouts.LayoutRenderHost;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.fullscreen.BrowserControlsManager;
import org.chromium.chrome.browser.fullscreen.FullscreenManager;
import org.chromium.chrome.browser.layouts.EventFilter.EventType;
import org.chromium.chrome.browser.layouts.components.VirtualView;
import org.chromium.chrome.browser.preferences.Pref;
import org.chromium.chrome.browser.tab.EmptyTabObserver;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabCreationState;
import org.chromium.chrome.browser.tab.TabLoadIfNeededCaller;
import org.chromium.chrome.browser.tab.TabObscuringHandler;
import org.chromium.chrome.browser.tab.TabObserver;
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.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorObserver;
import org.chromium.chrome.browser.tasks.tab_management.TabManagementFieldTrial;
import org.chromium.chrome.browser.tasks.tab_management.TabUiFeatureUtilities;
import org.chromium.chrome.browser.theme.TopUiThemeColorProvider;
import org.chromium.chrome.browser.toolbar.ControlContainer;
import org.chromium.chrome.browser.util.ChromeAccessibilityUtil;
import org.chromium.components.browser_ui.widget.TouchEventObserver;
import org.chromium.components.browser_ui.widget.TouchEventProvider;
import org.chromium.components.content_capture.OnscreenContentProvider;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.components.prefs.PrefService;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.KeyboardVisibilityDelegate;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.ApplicationViewportInsetSupplier;
import org.chromium.ui.base.EventForwarder;
import org.chromium.ui.base.EventOffsetHandler;
import org.chromium.ui.base.SPenSupport;
import org.chromium.ui.base.UiAndroidFeatureList;
import org.chromium.ui.base.UiAndroidFeatureMap;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.ViewportInsets;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.mojom.VirtualKeyboardMode;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;
import org.chromium.url.GURL;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * This class holds a {@link CompositorView}. This level of indirection is needed to benefit from
 * the {@link android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent)} capability on
 * available on {@link android.view.ViewGroup}s. This class also holds the {@link LayoutManagerImpl}
 * responsible to describe the items to be drawn by the UI compositor on the native side.
 */
public class CompositorViewHolder extends FrameLayout
        implements LayoutManagerHost,
                LayoutRenderHost,
                TouchEventProvider,
                BrowserControlsStateProvider.Observer,
                ChromeAccessibilityUtil.Observer,
                TabObscuringHandler.Observer,
                ViewGroup.OnHierarchyChangeListener {
    private static final long SYSTEM_UI_VIEWPORT_UPDATE_DELAY_MS = 500;

    /**
     * Initializer interface used to decouple initialization from the class that owns
     * the CompositorViewHolder.
     */
    public interface Initializer {
        /**
         * Initializes the {@link CompositorViewHolder} with the relevant content it needs to
         * properly show content on the screen.
         *
         * @param layoutManager A {@link LayoutManagerImpl} instance. This class is responsible for
         *     driving all high level screen content and determines which {@link Layout} is shown
         *     when.
         * @param urlBar The {@link View} representing the URL bar (must be focusable) or {@code
         *     null} if none exists.
         * @param controlContainer A {@link ControlContainer} instance to draw.
         */
        void initializeCompositorContent(
                LayoutManagerImpl layoutManager, View urlBar, ControlContainer controlContainer);
    }

    private final ObserverList<TouchEventObserver> mTouchEventObservers = new ObserverList<>();
    // Tracks current aggregated state of if the compositor is in motion. This could be an ongoing
    // touch by the user, or a scroll that's in progress.
    private final ObservableSupplierImpl<Boolean> mInMotionSupplier =
            new ObservableSupplierImpl<>();

    private boolean mIsKeyboardShowing;
    private boolean mNativeInitialized;
    private LayoutManagerImpl mLayoutManager;
    private Activity mActivity;
    private CompositorView mCompositorView;

    private boolean mContentOverlayVisiblity = true;
    private boolean mCanBeFocusable;

    /** A task to be performed after a resize event. */
    private Runnable mPostHideKeyboardTask;

    private TabModelSelector mTabModelSelector;
    private @Nullable BrowserControlsManager mBrowserControlsManager;
    private View mAccessibilityView;
    private CompositorAccessibilityProvider mNodeProvider;

    /** The toolbar control container. **/
    private @Nullable ControlContainer mControlContainer;

    private boolean mShowingFullscreen;
    private Runnable mSystemUiFullscreenResizeRunnable;

    /** The currently visible Tab. */
    @VisibleForTesting Tab mTabVisible;

    /** The currently attached View. */
    private View mView;

    /**
     * Current ContentView. Updates when active tab is switched or WebContents is swapped
     * in the current Tab.
     */
    private ContentView mContentView;

    // Cache objects that should not be created frequently.
    private final Rect mCacheRect = new Rect();
    private final Point mCachePoint = new Point();

    private boolean mControlsResizeView;
    private boolean mInGesture;
    private boolean mContentViewScrolling;
    // The number of active touch pointers. We are sending a gesture begin
    // event for every added touch point, and a gesnture end event for every
    // removed touch point.
    // TODO(crbug.com/265479149): We will remove |mInGesture| if we enable the
    // SUPPRESS_TOOLBAR_CAPTURES_AT_GESTURE_END feature.
    private int mNumGestureActiveTouches;
    private ApplicationViewportInsetSupplier mApplicationBottomInsetSupplier;

    // Handler for changes to viewport insets.
    private Callback<ViewportInsets> mOnViewportInsetsChanged;

    /**
     * Tracks whether geometrychange event is fired for the active tab when the keyboard
     *  is shown/hidden. When active tab changes, this flag is reset so we can fire
     *  geometrychange event for the new tab when the keyboard shows.
     */
    private boolean mHasKeyboardGeometryChangeFired;

    /**
     * By default, the virtual keyboard overlays content, only resizing the visual viewport.
     * Web content can use APIs that can change this to cause the WebContents to be resized.
     */
    @VirtualKeyboardMode.EnumType
    private int mVirtualKeyboardMode = VirtualKeyboardMode.RESIZES_VISUAL;

    private OnscreenContentProvider mOnscreenContentProvider;

    private final Set<Runnable> mOnCompositorLayoutCallbacks = new HashSet<>();
    private final Set<Runnable> mDidSwapFrameCallbacks = new HashSet<>();
    private final Set<Runnable> mDidSwapBuffersCallbacks = new HashSet<>();

    /** Used to remove the temporary tab strip on startup, once ready (or timed out). */
    private Runnable mSetBackgroundRunnable;

    private boolean mDelayTempStripRemoval;
    private boolean mSetBackgroundTimedOut;
    private boolean mCanSetBackground;
    private boolean mFirstTabCreated;
    private boolean mHasDrawnOnce;
    private int mDelayTempStripRemovalTimeoutMs;
    private long mBuffersSwappedTimestamp;
    private long mTabStateInitializedTimestamp;

    private TopUiThemeColorProvider mTopUiThemeColorProvider;

    // Permissions are requested on a drop event, and are released when another drag starts
    // (drag-started event) or when the current page navigates to a new URL or the tab changes.
    private DragAndDropPermissions mDragAndDropPermissions;

    private final EventOffsetHandler mEventOffsetHandler =
            new EventOffsetHandler(
                    new EventOffsetHandler.EventOffsetHandlerDelegate() {
                        // Cache objects that should not be created frequently.
                        private final RectF mCacheViewport = new RectF();

                        @Override
                        public float getTop() {
                            if (mLayoutManager != null) {
                                mLayoutManager.getViewportPixel(mCacheViewport);
                            }
                            return mCacheViewport.top;
                        }

                        @Override
                        public void setCurrentTouchEventOffsets(float top) {
                            EventForwarder forwarder = getEventForwarder();
                            if (forwarder != null) forwarder.setCurrentTouchOffsetY(top);
                        }

                        @Override
                        public void setCurrentDragEventOffsets(float dx, float dy) {
                            EventForwarder forwarder = getEventForwarder();
                            if (forwarder != null) forwarder.setDragDispatchingOffset(dx, dy);
                        }

                        private EventForwarder getEventForwarder() {
                            if (mTabVisible == null) return null;
                            WebContents webContents = mTabVisible.getWebContents();
                            if (webContents == null) return null;
                            return webContents.getEventForwarder();
                        }
                    });

    private final TabObserver mTabObserver =
            new EmptyTabObserver() {
                @Override
                public void onContentChanged(Tab tab) {
                    CompositorViewHolder.this.onContentChanged();
                }

                @Override
                public void onPageLoadStarted(Tab tab, GURL url) {
                    CompositorViewHolder.this.releaseDragAndDropPermissions();
                }

                @Override
                public void onContentViewScrollingStateChanged(boolean scrolling) {
                    mContentViewScrolling = scrolling;
                    updateInMotion();
                    if (!scrolling) updateContentViewChildrenDimension();
                }

                @Override
                public void onWillShowBrowserControls(Tab tab, boolean viewTransitionOptIn) {
                    CompositorViewHolder.this.onWillShowBrowserControls(viewTransitionOptIn);
                }

                @Override
                public void onVirtualKeyboardModeChanged(
                        Tab tab, @VirtualKeyboardMode.EnumType int mode) {
                    updateVirtualKeyboardMode(mode);
                }

                @Override
                public void onDidFinishNavigationInPrimaryMainFrame(
                        Tab tab, NavigationHandle navigation) {
                    if (!navigation.isSameDocument() && navigation.hasCommitted()) {
                        assert getWebContents() == tab.getWebContents();
                        assert getWebContents() != null;
                        updateVirtualKeyboardMode(getWebContents().getVirtualKeyboardMode());
                    }
                }

                // TODO(crbug.com/265479149): Split out a specific delegate for
                // gesture listening below and remove from TabObserver.
                @Override
                public void onGestureBegin() {
                    mNumGestureActiveTouches++;
                    updateInMotion();
                }

                @Override
                public void onGestureEnd() {
                    mNumGestureActiveTouches--;
                    updateInMotion();
                }
            };

    private View mUrlBar;

    private PrefService mPrefService;

    @Override
    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
        View activeView = getContentView();
        if (activeView == null || !ViewCompat.isAttachedToWindow(activeView)) return null;
        return activeView.onResolvePointerIcon(event, pointerIndex);
    }

    /**
     * Creates a {@link CompositorView}.
     *
     * @param c The Context to create this {@link CompositorView} in.
     * @param attrs The AttributeSet used to create this {@link CompositorView}.
     */
    public CompositorViewHolder(Context c, AttributeSet attrs) {
        super(c, attrs);
        internalInit();
    }

    private void internalInit() {
        addOnLayoutChangeListener(
                (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
                    Tab tab = getCurrentTab();
                    if (tab != null) {
                        // Set the size of NTP if we're in the attached state as it may have not
                        // been sized properly when initializing tab. See the comment in
                        // #initializeTab()
                        // for why.
                        boolean attachedNativePage =
                                tab.isNativePage() && isAttachedToWindow(tab.getView());
                        boolean sizeChanged =
                                (right - left) != (oldRight - oldLeft)
                                        || (top - bottom) != (oldTop - oldBottom);
                        if (attachedNativePage || sizeChanged) {
                            tryUpdateControlsAndWebContentsSizing();
                        }
                    }

                    onViewportChanged();

                    // If there's an event that needs to occur after the keyboard is hidden, post
                    // it as a delayed event.  Otherwise this happens in the midst of the
                    // ContentView's relayout, which causes the ContentView to relayout on top of
                    // the
                    // stack view.  The 30ms is arbitrary, hoping to let the view get one repaint
                    // in so the full page is shown.
                    if (mPostHideKeyboardTask != null) {
                        new Handler().postDelayed(mPostHideKeyboardTask, 30);
                        mPostHideKeyboardTask = null;
                    }
                });

        mCompositorView = new CompositorView(getContext(), this);
        // mCompositorView should always be the first child.
        addView(
                mCompositorView,
                0,
                new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

        setOnSystemUiVisibilityChangeListener(visibility -> handleSystemUiVisibilityChange());
        if (isFullscreenApiMigrationEnabled()) {
            setOnApplyWindowInsetsListener(
                    (view, windowInsets) -> {
                        handleSystemUiVisibilityChange();
                        return windowInsets;
                    });
        }
        handleSystemUiVisibilityChange();

        mDelayTempStripRemoval = TabUiFeatureUtilities.isDelayTempStripRemovalEnabled(getContext());
        mDelayTempStripRemovalTimeoutMs =
                TabManagementFieldTrial.DELAY_TEMP_STRIP_TIMEOUT_MS.getValue();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            setDefaultFocusHighlightEnabled(false);
        }
    }

    private Point getViewportSize() {
        // When in fullscreen mode, the window does not get resized when showing the onscreen
        // keyboard[1].  To work around this, we monitor the visible display frame to mimic the
        // resize state to ensure the web contents has the correct width and height.
        //
        // This path should not be used in the non-fullscreen case as it would negate the
        // performance benefits of the app setting SOFT_INPUT_ADJUST_PAN.  This would force the
        // app into a constant SOFT_INPUT_ADJUST_RESIZE mode, which causes more churn on the page
        // layout than required in cases that you're editing in Chrome UI outside of the web
        // contents.
        //
        // [1] -
        // https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN
        if (mShowingFullscreen
                && KeyboardVisibilityDelegate.getInstance().isKeyboardShowing(getContext(), this)) {
            getWindowVisibleDisplayFrame(mCacheRect);

            // On certain devices, getWindowVisibleDisplayFrame is larger than the screen size, so
            // this ensures we never draw beyond the underlying dimensions of the view.
            // https://crbug.com/854109
            mCachePoint.set(
                    Math.min(mCacheRect.width(), getWidth()),
                    Math.min(mCacheRect.height(), getHeight()));
        } else {
            mCachePoint.set(getWidth(), getHeight());
        }
        return mCachePoint;
    }

    @VisibleForTesting
    void handleSystemUiVisibilityChange() {
        View view = getContentView();
        if (view == null || !ViewCompat.isAttachedToWindow(view)) view = this;

        int uiVisibility = 0;
        while (view != null) {
            uiVisibility |= view.getSystemUiVisibility();
            if (!(view.getParent() instanceof View)) break;
            view = (View) view.getParent();
        }

        boolean isInFullscreen = isInFullscreenMode(uiVisibility, view);
        boolean layoutFullscreen = isLayoutFullscreen(uiVisibility);

        if (mShowingFullscreen == isInFullscreen) return;
        mShowingFullscreen = isInFullscreen;

        if (mSystemUiFullscreenResizeRunnable == null) {
            mSystemUiFullscreenResizeRunnable = this::handleWindowInsetChanged;
        } else {
            getHandler().removeCallbacks(mSystemUiFullscreenResizeRunnable);
        }

        // If SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN is set, defer updating the viewport to allow
        // Android's animations to complete.  The getWindowVisibleDisplayFrame values do not get
        // updated until a fair amount after onSystemUiVisibilityChange is broadcast.
        //
        // SYSTEM_UI_VIEWPORT_UPDATE_DELAY_MS was chosen by increasing the time until the UI did
        // not reliably jump from updating the viewport too early.
        long delay = layoutFullscreen ? SYSTEM_UI_VIEWPORT_UPDATE_DELAY_MS : 0;
        postDelayed(mSystemUiFullscreenResizeRunnable, delay);
    }

    private static boolean isFullscreenApiMigrationEnabled() {
        return ChromeFeatureList.sFullscreenInsetsApiMigration.isEnabled()
                || (BuildInfo.getInstance().isAutomotive
                        && ChromeFeatureList.sFullscreenInsetsApiMigrationOnAutomotive.isEnabled());
    }

    private boolean isInFullscreenMode(int uiVisibility, View view) {
        // If the fullscreen api migration is enabled, check the updated API instead.
        if (isFullscreenApiMigrationEnabled()) {
            if (view != null
                    && view.getRootWindowInsets() != null
                    && mActivity != null
                    && mActivity.getWindow() != null
                    && mActivity.getWindow().getDecorView() != null) {
                Window window = mActivity.getWindow();
                return !WindowInsetsCompat.toWindowInsetsCompat(view.getRootWindowInsets(), view)
                                .isVisible(WindowInsetsCompat.Type.statusBars())
                        || WindowCompat.getInsetsController(window, window.getDecorView())
                                        .getSystemBarsBehavior()
                                == BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
            } else {
                return false;
            }
        } else {
            // SYSTEM_UI_FLAG_FULLSCREEN is cleared when showing the soft keyboard in older version
            // of
            // Android (prior to P).  The immersive mode flags are not cleared, so use those in
            // combination to detect this state.
            return (uiVisibility & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0
                    || (uiVisibility & View.SYSTEM_UI_FLAG_IMMERSIVE) != 0
                    || (uiVisibility & View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) != 0;
        }
    }

    private boolean isLayoutFullscreen(int uiVisibility) {
        if (isFullscreenApiMigrationEnabled()) {
            if (mActivity != null
                    && mActivity.getWindow() != null
                    && mActivity.getWindow().getDecorView() != null) {
                // TODO(crbug.com/41492646): Coordinate usage of #setDecorFitsSystemWindows
                return !mActivity.getWindow().getDecorView().getFitsSystemWindows();
            } else {
                return false;
            }
        } else {
            return (uiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0;
        }
    }

    /**
     * @param layoutManager The {@link LayoutManagerImpl} instance that will be driving what
     *                      shows in this {@link CompositorViewHolder}.
     */
    public void setLayoutManager(LayoutManagerImpl layoutManager) {
        mLayoutManager = layoutManager;
        onViewportChanged();
    }

    /**
     * @param view The root view of the hierarchy.
     */
    public void setRootView(View view) {
        mCompositorView.setRootView(view);
    }

    /**
     * @param controlContainer The ControlContainer.
     */
    public void setControlContainer(@Nullable ControlContainer controlContainer) {
        DynamicResourceLoader loader =
                mCompositorView.getResourceManager() != null
                        ? mCompositorView.getResourceManager().getDynamicResourceLoader()
                        : null;
        if (loader != null && mControlContainer != null) {
            loader.unregisterResource(R.id.control_container);
        }
        mControlContainer = controlContainer;
        if (loader != null && mControlContainer != null) {
            loader.registerResource(
                    R.id.control_container, mControlContainer.getToolbarResourceAdapter());
        }

        mSetBackgroundRunnable =
                () -> {
                    // Wait until the second frame to turn off the placeholder background for the
                    // CompositorView and the tab strip, to ensure the compositor frame has been
                    // drawn.
                    final ViewGroup controlContainerVG = (ViewGroup) mControlContainer;
                    mCompositorView.setBackgroundResource(0);
                    if (controlContainerVG != null) {
                        mControlContainer.setCompositorBackgroundInitialized();
                    }
                };
    }

    /**
     * @param themeColorProvider {@link ThemeColorProvider} for top UI part.
     */
    public void setTopUiThemeColorProvider(TopUiThemeColorProvider themeColorProvider) {
        mTopUiThemeColorProvider = themeColorProvider;
    }

    /**
     * Sets the ApplicationViewportInsetSupplier that will notify CompositorViewHolder when the
     * WebContent must be resized by viewport insets.
     */
    public void setApplicationViewportInsetSupplier(ApplicationViewportInsetSupplier supplier) {
        assert mApplicationBottomInsetSupplier == null;
        mApplicationBottomInsetSupplier = supplier;
        mApplicationBottomInsetSupplier.setVirtualKeyboardMode(mVirtualKeyboardMode);
        mOnViewportInsetsChanged = (unused) -> handleWindowInsetChanged();
        mApplicationBottomInsetSupplier.addObserver(mOnViewportInsetsChanged);
    }

    // This method is called when any viewport insets change but is needed to watch for keyboard
    // state changes while fullscreened and is used to simulate a view resize. This is only needed
    // if the page has opted in to keyboard resizes.
    private void handleWindowInsetChanged() {
        if (mApplicationBottomInsetSupplier != null
                && mApplicationBottomInsetSupplier.insetsAffectWebContentsSize()) {
            tryUpdateControlsAndWebContentsSizing();
        }

        // Notify the compositor layout that the size has changed.  The layout does not drive
        // the WebContents sizing, so this needs to be done in addition to the above size
        // update.
        onViewportChanged();
    }

    /** Should be called for cleanup when the CompositorView instance is no longer used. */
    public void shutDown() {
        setTab(null);
        if (mApplicationBottomInsetSupplier != null) {
            assert mOnViewportInsetsChanged != null;
            mApplicationBottomInsetSupplier.removeObserver(mOnViewportInsetsChanged);
        }

        mCompositorView.shutDown();
        if (mLayoutManager != null) mLayoutManager.destroy();
        if (mOnscreenContentProvider != null) mOnscreenContentProvider.destroy();
        if (mContentView != null) {
            mContentView.removeOnHierarchyChangeListener(this);
        }
    }

    /** This is called when the native library are ready. */
    public void onNativeLibraryReady(
            WindowAndroid windowAndroid,
            TabContentManager tabContentManager,
            PrefService prefService) {
        mActivity = windowAndroid.getActivity().get();
        mCompositorView.initNativeCompositor(
                SysUtils.isLowEndDevice(), windowAndroid, tabContentManager);

        if (mControlContainer != null) {
            mCompositorView
                    .getResourceManager()
                    .getDynamicResourceLoader()
                    .registerResource(
                            R.id.control_container, mControlContainer.getToolbarResourceAdapter());
        }

        mPrefService = prefService;
    }

    /** Perform any initialization necessary for showing a reparented tab. */
    public void prepareForTabReparenting() {
        if (mHasDrawnOnce) return;

        // Set the background to white while we wait for the first swap of buffers. This gets
        // corrected inside the view.
        mCompositorView.setBackgroundColor(Color.WHITE);
    }

    @Override
    public ResourceManager getResourceManager() {
        return mCompositorView.getResourceManager();
    }

    /**
     * @return The {@link DynamicResourceLoader} for registering resources.
     */
    public DynamicResourceLoader getDynamicResourceLoader() {
        return mCompositorView.getResourceManager().getDynamicResourceLoader();
    }

    // TouchEventProvider implementation.

    @Override
    public void addTouchEventObserver(TouchEventObserver o) {
        mTouchEventObservers.addObserver(o);
    }

    @Override
    public void removeTouchEventObserver(TouchEventObserver o) {
        mTouchEventObservers.removeObserver(o);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        super.onInterceptTouchEvent(e);
        for (TouchEventObserver o : mTouchEventObservers) {
            if (o.onInterceptTouchEvent(e)) return true;
        }

        if (mLayoutManager == null) return false;

        int actionMasked = SPenSupport.convertSPenEventAction(e.getActionMasked());
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            mEventOffsetHandler.onInterceptTouchDownEvent(e);
        }
        return mLayoutManager.onInterceptMotionEvent(e, mIsKeyboardShowing, EventType.TOUCH);
    }

    @Override
    public boolean onTouchEvent(MotionEvent e) {
        super.onTouchEvent(e);
        for (TouchEventObserver o : mTouchEventObservers) {
            if (o.onTouchEvent(e)) return true;
        }

        boolean consumed = mLayoutManager != null && mLayoutManager.onTouchEvent(e);
        mEventOffsetHandler.onTouchEvent(e);
        return consumed;
    }

    private void updateIsInGesture(MotionEvent e) {
        int eventAction = e.getActionMasked();
        if (eventAction == MotionEvent.ACTION_DOWN
                || eventAction == MotionEvent.ACTION_POINTER_DOWN) {
            mInGesture = true;
        } else if (eventAction == MotionEvent.ACTION_CANCEL
                || eventAction == MotionEvent.ACTION_UP) {
            mInGesture = false;
            tryUpdateControlsAndWebContentsSizing();
        }
    }

    private void updateInMotion() {
        // TODO(crbug.com/40244051): Track fling as well.
        boolean inMotion = mContentViewScrolling;
        if (ChromeFeatureList.sSuppressToolbarCapturesAtGestureEnd.isEnabled()) {
            inMotion |= mNumGestureActiveTouches > 0;
        } else {
            inMotion |= mInGesture;
        }
        mInMotionSupplier.set(inMotion);
        if (mContentView != null) {
            mContentView.setDeferKeepScreenOnChanges(inMotion);
        }
    }

    /**
     * Aggregated supplier for whether the compositor's content is moving. Currently tracking in
     * touch event and in scroll event. Performance is critical while this supplier returns true,
     * and clients that have expensive operations may consider deferring until after the motion is
     * over.
     */
    public ObservableSupplier<Boolean> getInMotionSupplier() {
        return mInMotionSupplier;
    }

    @Override
    public boolean onInterceptHoverEvent(MotionEvent e) {
        mEventOffsetHandler.onInterceptHoverEvent(e);
        if (mLayoutManager == null) return super.onInterceptHoverEvent(e);
        return mLayoutManager.onInterceptMotionEvent(e, mIsKeyboardShowing, EventType.HOVER);
    }

    @Override
    public boolean onHoverEvent(MotionEvent e) {
        super.onHoverEvent(e);
        boolean consumed = mLayoutManager != null && mLayoutManager.onHoverEvent(e);
        mEventOffsetHandler.onHoverEvent(e);
        return consumed;
    }

    @Override
    public boolean dispatchHoverEvent(MotionEvent e) {
        if (mNodeProvider != null) {
            if (mNodeProvider.dispatchHoverEvent(e)) {
                return true;
            }
        }
        return super.dispatchHoverEvent(e);
    }

    @Override
    public boolean dispatchDragEvent(DragEvent e) {
        mEventOffsetHandler.onPreDispatchDragEvent(e.getAction(), 0.f, 0.f);
        if (UiAndroidFeatureMap.isEnabled(UiAndroidFeatureList.DRAG_DROP_FILES)) {
            if (e.getAction() == DragEvent.ACTION_DRAG_STARTED) {
                releaseDragAndDropPermissions();
            } else if (e.getAction() == DragEvent.ACTION_DROP) {
                mDragAndDropPermissions = mActivity.requestDragAndDropPermissions(e);
            }
        }
        boolean ret = super.dispatchDragEvent(e);
        mEventOffsetHandler.onPostDispatchDragEvent(e.getAction());
        return ret;
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {
        assert e != null : "The motion event dispatched shouldn't be null!";
        updateIsInGesture(e);
        for (TouchEventObserver o : mTouchEventObservers) {
            if (o.dispatchTouchEvent(e)) return true;
        }

        // This is where input events go from android through native to the web content. This
        // process is latency sensitive. Ideally observers that might be expensive, such as
        // notifying in motion, should be done after this.
        boolean handled = super.dispatchTouchEvent(e);

        updateInMotion();
        return handled;
    }

    /**
     * @return The {@link LayoutManagerImpl} associated with this view.
     */
    public LayoutManagerImpl getLayoutManager() {
        return mLayoutManager;
    }

    /**
     * @return The SurfaceView proxy used by the Compositor.
     */
    public CompositorView getCompositorView() {
        return mCompositorView;
    }

    /**
     * @return The active {@link android.view.SurfaceView} of the Compositor.
     */
    public View getActiveSurfaceView() {
        return mCompositorView.getActiveSurfaceView();
    }

    @VisibleForTesting
    Tab getCurrentTab() {
        if (mLayoutManager == null || mTabModelSelector == null) return null;
        Tab currentTab = mTabModelSelector.getCurrentTab();

        // If the tab model selector doesn't know of a current tab, use the last visible one.
        if (currentTab == null) currentTab = mTabVisible;

        return currentTab;
    }

    @VisibleForTesting
    ViewGroup getContentView() {
        Tab tab = getCurrentTab();
        return tab != null ? tab.getContentView() : null;
    }

    protected WebContents getWebContents() {
        Tab tab = getCurrentTab();
        return tab != null ? tab.getWebContents() : null;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (mTabModelSelector == null) return;

        for (TabModel tabModel : mTabModelSelector.getModels()) {
            for (int i = 0; i < tabModel.getCount(); ++i) {
                Tab tab = tabModel.getTabAt(i);
                if (tab == null) continue;
                updateWebContentsSize(tab);
            }
        }
    }

    /**
     * Ensures the tab-backed webContents' size is up to date.
     *
     * Using this view's current size, taking into account the current state of UI like the virtual
     * keyboard and browser controls and resizes as well as the virtual keyboard resizing mode,
     * updates the size of the given Tab's WebContents. If the given view isn't attached to the
     * Window, this method will force it to layout and use that size.
     *
     * @param tab {@link Tab} for which the size of the view is set.
     */
    @VisibleForTesting
    void updateWebContentsSize(Tab tab) {
        if (tab == null) return;

        WebContents webContents = tab.getWebContents();
        View view = tab.getContentView();
        if (webContents == null || view == null) return;

        Point viewportSize = getViewportSize();
        int width = viewportSize.x;
        int height = viewportSize.y;

        // The view size takes into account of the browser controls whose height should be
        // subtracted from the view if they are visible, therefore shrink Blink-side view size.
        // TODO(crbug.com/40767446): Centralize the logic for calculating bottom insets by
        // merging them into ApplicationBottomInsetSupplier.
        int controlsInsets = 0;
        if (mBrowserControlsManager != null) {
            int controlsMinHeight =
                    mBrowserControlsManager.getTopControlsMinHeight()
                            + mBrowserControlsManager.getBottomControlsMinHeight();
            int controlsHeight =
                    mBrowserControlsManager.getTopControlsHeight()
                            + mBrowserControlsManager.getBottomControlsHeight();
            controlsInsets = mControlsResizeView ? controlsHeight : controlsMinHeight;
        }

        int keyboardInset =
                mApplicationBottomInsetSupplier != null
                        ? mApplicationBottomInsetSupplier.get().webContentsHeightInset
                        : 0;

        int viewportInsets = controlsInsets + keyboardInset;

        if (isAttachedToWindow(view)) {
            webContents.setSize(width, height - viewportInsets);

            // Dispatch the geometrychange JavaScript event to the page.
            // TODO(bokan): This doesn't belong in updateWebContentsSize. Ideally the content/ layer
            // would listen to changes in keyboard state and dispatch this event itself.
            if (mVirtualKeyboardMode == VirtualKeyboardMode.OVERLAYS_CONTENT) {
                int keyboardHeight =
                        KeyboardVisibilityDelegate.getInstance()
                                .calculateTotalKeyboardHeight(this.getRootView());
                notifyVirtualKeyboardOverlayGeometryChangeEvent(width, keyboardHeight, webContents);
            }
        } else {
            // Need to call layout() for the following View if it is not attached to the view
            // hierarchy. Calling {@code view.onSizeChanged()} is dangerous because if the View has
            // a different size than the WebContents, it might think a future size update is a NOOP
            // and not call onSizeChanged() on the WebContents.
            view.measure(
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
            view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
            webContents.setSize(view.getWidth(), view.getHeight() - viewportInsets);
            requestRender();
        }
    }

    private static boolean isAttachedToWindow(View view) {
        return view != null && view.getWindowToken() != null;
    }

    @VirtualKeyboardMode.EnumType
    private int defaultVirtualKeyboardMode() {
        if (mPrefService.getBoolean(Pref.VIRTUAL_KEYBOARD_RESIZES_LAYOUT_BY_DEFAULT)) {
            return VirtualKeyboardMode.RESIZES_CONTENT;
        }
        return VirtualKeyboardMode.RESIZES_VISUAL;
    }

    /**
     * Notifies geometrychange event to JS.
     * @param w  Width of the view.
     * @param keyboardHeight Height of the keyboard.
     * @param webContents Active WebContent for which this event needs to be fired.
     */
    private void notifyVirtualKeyboardOverlayGeometryChangeEvent(
            int w, int keyboardHeight, WebContents webContents) {
        assert mVirtualKeyboardMode == VirtualKeyboardMode.OVERLAYS_CONTENT;

        boolean keyboardVisible = keyboardHeight > 0;
        if (!keyboardVisible && !mHasKeyboardGeometryChangeFired) {
            return;
        }

        mHasKeyboardGeometryChangeFired = keyboardVisible;
        Rect appRect = new Rect();
        getRootView().getWindowVisibleDisplayFrame(appRect);
        if (keyboardVisible) {
            // Fire geometrychange event to JS.
            // The assumption here is that the keyboard is docked at the bottom so we use the
            // root visible window frame's origin to calculate the position of the keyboard.
            notifyVirtualKeyboardOverlayRect(
                    webContents, appRect.left, appRect.top, w, keyboardHeight);
        } else {
            // Keyboard has hidden.
            notifyVirtualKeyboardOverlayRect(webContents, 0, 0, 0, 0);
        }
    }

    @Override
    public void onSurfaceResized(int width, int height) {
        View view = getContentView();
        WebContents webContents = getWebContents();
        if (view == null || webContents == null) return;
        onPhysicalBackingSizeChanged(webContents, width, height);
    }

    private void onPhysicalBackingSizeChanged(WebContents webContents, int width, int height) {
        if (mCompositorView != null) {
            mCompositorView.onPhysicalBackingSizeChanged(webContents, width, height);
        }
    }

    private void onControlsResizeViewChanged(WebContents webContents, boolean controlsResizeView) {
        if (webContents != null && mCompositorView != null) {
            mCompositorView.onControlsResizeViewChanged(webContents, controlsResizeView);
        }
    }

    /**
     * Fires geometrychange event to JS with the keyboard size.
     * @param webContents Active WebContent for which this event needs to be fired.
     * @param x When the keyboard is shown, it has the left position of the app's rect, else, 0.
     * @param y When the keyboard is shown, it has the top position of the app's rect, else, 0.
     * @param width  When the keyboard is shown, it has the width of the view, else, 0.
     * @param height The height of the keyboard.
     */
    @VisibleForTesting
    void notifyVirtualKeyboardOverlayRect(
            WebContents webContents, int x, int y, int width, int height) {
        if (mCompositorView != null) {
            mCompositorView.notifyVirtualKeyboardOverlayRect(webContents, x, y, width, height);
        }
    }

    /** Called whenever the host activity is started. */
    public void onStart() {
        if (mBrowserControlsManager != null) mBrowserControlsManager.addObserver(this);
        requestRender();
    }

    /** Called whenever the host activity is stopped. */
    public void onStop() {
        if (mBrowserControlsManager != null) mBrowserControlsManager.removeObserver(this);
    }

    @Override
    public void onControlsOffsetChanged(
            int topOffset,
            int topControlsMinHeightOffset,
            int bottomOffset,
            int bottomControlsMinHeightOffset,
            boolean needsAnimate,
            boolean isVisibilityForced) {
        onViewportChanged();

        // When scrolling browser controls in viz, don't produce new browser frames unless it's
        // forced with |needs_animate|
        boolean scrollingWithBciv =
                ChromeFeatureList.sBrowserControlsInViz.isEnabled()
                        && (mInGesture || mContentViewScrolling);
        if (needsAnimate && !scrollingWithBciv) {
            requestRender();
        }

        updateContentViewChildrenDimension();
    }

    @Override
    public void onBottomControlsHeightChanged(
            int bottomControlsHeight, int bottomControlsMinHeight) {
        if (mTabVisible == null) return;
        onBrowserControlsHeightChanged();
        updateWebContentsSize(getCurrentTab());
        onViewportChanged();
    }

    @Override
    public void onTopControlsHeightChanged(int topControlsHeight, int topControlsMinHeight) {
        if (mTabVisible == null) return;
        onBrowserControlsHeightChanged();
        updateWebContentsSize(getCurrentTab());
        onViewportChanged();
    }

    /**
     * Notify the {@link WebContents} of the browser controls height changes. Unlike
     * #updateWebContentsSize, this will make sure the renderer's properties are updated even if the
     * size didn't change.
     */
    private void onBrowserControlsHeightChanged() {
        final WebContents webContents = getWebContents();
        if (webContents == null) return;
        webContents.notifyBrowserControlsHeightChanged();
    }

    /**
     * Attempts to update browser controls sizing state and then synchronizes the WebContents size
     * based on the current viewport and insets. No-op if the user is currently scrolling or in a
     * gesture.
     */
    private void tryUpdateControlsAndWebContentsSizing() {
        if (mInGesture || mContentViewScrolling) return;
        boolean controlsResizeViewChanged = false;
        if (mBrowserControlsManager != null) {
            // Update content viewport size only if the browser controls are not moving, i.e. not
            // scrolling or animating.
            if (!BrowserControlsUtils.areBrowserControlsIdle(mBrowserControlsManager)) return;

            boolean controlsResizeView =
                    BrowserControlsUtils.controlsResizeView(mBrowserControlsManager);
            if (controlsResizeView != mControlsResizeView) {
                mControlsResizeView = controlsResizeView;
                controlsResizeViewChanged = true;
            }
        }
        // Reflect the changes that may have happened in in view/control size.
        updateWebContentsSize(getCurrentTab());
        if (controlsResizeViewChanged) {
            // Send this after updateWebContentsSize, so that RenderWidgetHost doesn't
            // SynchronizeVisualProperties in a partly-updated state.
            onControlsResizeViewChanged(getWebContents(), mControlsResizeView);
        }
    }

    // View.OnHierarchyChangeListener implementation

    @Override
    public void onChildViewRemoved(View parent, View child) {
        updateContentViewChildrenDimension();
    }

    @Override
    public void onChildViewAdded(View parent, View child) {
        updateContentViewChildrenDimension();
    }

    private void updateContentViewChildrenDimension() {
        TraceEvent.begin("CompositorViewHolder:updateContentViewChildrenDimension");
        ViewGroup view = getContentView();
        if (view != null) {
            assert mBrowserControlsManager != null;
            float topViewsTranslation = mBrowserControlsManager.getTopVisibleContentOffset();
            float bottomMargin =
                    BrowserControlsUtils.getBottomContentOffset(mBrowserControlsManager);
            applyMarginToFullscreenChildViews(view, topViewsTranslation, bottomMargin);
            tryUpdateControlsAndWebContentsSizing();
        }
        TraceEvent.end("CompositorViewHolder:updateContentViewChildrenDimension");
    }

    private static void applyMarginToFullscreenChildViews(
            ViewGroup contentView, float topMargin, float bottomMargin) {
        for (int i = 0; i < contentView.getChildCount(); i++) {
            View child = contentView.getChildAt(i);
            if (!(child.getLayoutParams() instanceof FrameLayout.LayoutParams)) continue;
            FrameLayout.LayoutParams layoutParams =
                    (FrameLayout.LayoutParams) child.getLayoutParams();

            if (layoutParams.height == LayoutParams.MATCH_PARENT
                    && (layoutParams.topMargin != (int) topMargin
                            || layoutParams.bottomMargin != (int) bottomMargin)) {
                layoutParams.topMargin = (int) topMargin;
                layoutParams.bottomMargin = (int) bottomMargin;
                ViewUtils.requestLayout(
                        child, "CompositorViewHolder.applyMarginToFullscreenChildViews");
                TraceEvent.instant("FullscreenManager:child.requestLayout()");
            }
        }
    }

    /** Sets the overlay mode. */
    public void setOverlayMode(boolean useOverlayMode) {
        if (mCompositorView != null) {
            mCompositorView.setOverlayVideoMode(useOverlayMode);
        }
    }

    private void onViewportChanged() {
        if (mLayoutManager != null) mLayoutManager.onViewportChanged();
    }

    /** To be called once a frame before commit. */
    @Override
    public void onCompositorLayout() {
        TraceEvent.begin("CompositorViewHolder:layout");
        if (mLayoutManager != null) {
            mLayoutManager.onUpdate();
            mCompositorView.finalizeLayers(mLayoutManager);
        }

        mDidSwapFrameCallbacks.addAll(mOnCompositorLayoutCallbacks);
        mOnCompositorLayoutCallbacks.clear();
        updateNeedsSwapBuffersCallback();

        TraceEvent.end("CompositorViewHolder:layout");
    }

    @Override
    public void getWindowViewport(RectF outRect) {
        Point viewportSize = getViewportSize();
        outRect.set(0, 0, viewportSize.x, viewportSize.y);
    }

    @Override
    public void getVisibleViewport(RectF outRect) {
        getWindowViewport(outRect);

        if (mApplicationBottomInsetSupplier != null) {
            outRect.bottom -= mApplicationBottomInsetSupplier.get().viewVisibleHeightInset;
        }

        // mApplicationBottomInsetSupplier doesn't include browser controls.
        if (mBrowserControlsManager != null) {
            // All of these values are in pixels.
            outRect.top += mBrowserControlsManager.getTopVisibleContentOffset();
            float bottomControlOffset = mBrowserControlsManager.getBottomControlOffset();
            outRect.bottom -= (getBottomControlsHeightPixels() - bottomControlOffset);
        }
    }

    @Override
    public void getViewportFullControls(RectF outRect) {
        getWindowViewport(outRect);

        if (mApplicationBottomInsetSupplier != null) {
            outRect.bottom -= mApplicationBottomInsetSupplier.get().viewVisibleHeightInset;
        }

        // mApplicationBottomInsetSupplier doesn't include browser controls.
        outRect.top += getTopControlsHeightPixels();
        outRect.bottom -= getBottomControlsHeightPixels();
    }

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

    @Override
    public void requestRender(Runnable onUpdateEffective) {
        if (onUpdateEffective != null) {
            mOnCompositorLayoutCallbacks.add(onUpdateEffective);
            updateNeedsSwapBuffersCallback();
        }
        mCompositorView.requestRender();
    }

    @Override
    public void didSwapFrame(int pendingFrameCount) {
        TraceEvent.instant("didSwapFrame");

        mHasDrawnOnce = true;

        mDidSwapBuffersCallbacks.addAll(mDidSwapFrameCallbacks);
        mDidSwapFrameCallbacks.clear();
        updateNeedsSwapBuffersCallback();
    }

    @Override
    public void didSwapBuffers(boolean swappedCurrentSize, int framesUntilHideBackground) {
        if (mSetBackgroundRunnable != null
                && mHasDrawnOnce
                && framesUntilHideBackground == 0
                && !mCanSetBackground) {
            // Remove temporary background if tab state is ready. Otherwise, mark that the
            // background can be removed and handle in TabModelSelectorObserver.
            if (!mDelayTempStripRemoval
                    || mTabModelSelector.isTabStateInitialized()
                    || mSetBackgroundTimedOut) {
                runSetBackgroundRunnable();
            } else {
                mCanSetBackground = true;
            }

            // If tab state is already initialized, record how long it took for the real tab strip
            // to be ready to be drawn.
            if (mTabStateInitializedTimestamp != 0) {
                RecordHistogram.recordTimesHistogram(
                        "Android.TabStrip.TimeToBufferSwapAfterInitializeTabState",
                        SystemClock.elapsedRealtime() - mTabStateInitializedTimestamp);
            } else {
                mBuffersSwappedTimestamp = SystemClock.elapsedRealtime();
            }
        }

        for (Runnable runnable : mDidSwapBuffersCallbacks) {
            runnable.run();
        }
        mDidSwapBuffersCallbacks.clear();
        updateNeedsSwapBuffersCallback();
    }

    private void runSetBackgroundRunnable() {
        // This runnable should only be run once.
        if (mSetBackgroundRunnable == null) return;

        new Handler().post(mSetBackgroundRunnable);
        mSetBackgroundRunnable = null;

        // Mark that we timed out if we remove the background before the tab state is initialized.
        // Called when the background is actually being removed, since if the timeout is reached,
        // but the second buffer swap happens after the tab state is initialized, we shouldn't
        // actually see any jank.
        RecordHistogram.recordBooleanHistogram(
                "Android.TabStrip.DelayTempStripRemovalTimedOut",
                !mTabModelSelector.isTabStateInitialized());
    }

    @VisibleForTesting
    void maybeInitializeSetBackgroundRunnableTimeout() {
        if (mDelayTempStripRemoval && !mFirstTabCreated) {
            mFirstTabCreated = true;
            new Handler()
                    .postDelayed(
                            () -> {
                                // If null, the background has already been removed before the
                                // timeout.
                                if (mSetBackgroundRunnable == null) return;

                                if (mCanSetBackground) {
                                    // If the background can be removed, remove it now.
                                    runSetBackgroundRunnable();
                                } else {
                                    // If the background cannot be removed, mark that we have timed
                                    // out, so that we can remove the background when the buffer
                                    // swaps.
                                    mSetBackgroundTimedOut = true;
                                }
                            },
                            mDelayTempStripRemovalTimeoutMs);
        }
    }

    @Override
    public void setContentOverlayVisibility(boolean show, boolean canBeFocusable) {
        if (show != mContentOverlayVisiblity || canBeFocusable != mCanBeFocusable) {
            mContentOverlayVisiblity = show;
            mCanBeFocusable = canBeFocusable;
            updateContentOverlayVisibility(mContentOverlayVisiblity);
        }
    }

    @Override
    public LayoutRenderHost getLayoutRenderHost() {
        return this;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mIsKeyboardShowing =
                KeyboardVisibilityDelegate.getInstance().isKeyboardShowing(getContext(), this);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) onViewportChanged();
        super.onLayout(changed, l, t, r, b);

        invalidateAccessibilityProvider();
    }

    @Override
    public void clearChildFocus(View child) {
        // Override this method so that the ViewRoot doesn't go looking for a new
        // view to take focus. It will find the URL Bar, focus it, then refocus this
        // later, causing a keyboard flicker.
    }

    @Override
    public BrowserControlsManager getBrowserControlsManager() {
        return mBrowserControlsManager;
    }

    @Override
    public FullscreenManager getFullscreenManager() {
        return mBrowserControlsManager.getFullscreenManager();
    }

    /**
     * Sets a browser controls manager.
     * @param manager A browser controls manager.
     */
    public void setBrowserControlsManager(BrowserControlsManager manager) {
        mBrowserControlsManager = manager;
        mBrowserControlsManager.addObserver(this);
        onViewportChanged();
    }

    public int getTopControlsHeightPixels() {
        return mBrowserControlsManager != null ? mBrowserControlsManager.getTopControlsHeight() : 0;
    }

    public int getBottomControlsHeightPixels() {
        return mBrowserControlsManager != null
                ? mBrowserControlsManager.getBottomControlsHeight()
                : 0;
    }

    /**
     * @return {@code true} if browser controls shrink Blink view's size.
     */
    public boolean controlsResizeView() {
        return mControlsResizeView;
    }

    /**
     * Sets the URL bar. This is needed so that the ContentViewHolder can find out
     * whether it can claim focus.
     */
    public void setUrlBar(View urlBar) {
        mUrlBar = urlBar;
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        // Removes the accessibility node provider from this view.
        if (mNodeProvider != null) {
            mAccessibilityView.setAccessibilityDelegate(null);
            mNodeProvider = null;
            removeView(mAccessibilityView);
            mAccessibilityView = null;
        }
    }

    @Override
    public void hideKeyboard(Runnable postHideTask) {
        // When this is called we actually want to hide the keyboard whatever owns it.
        // This includes hiding the keyboard, and dropping focus from the URL bar.
        // See http://crbug/236424
        // TODO(aberent) Find a better place to put this, possibly as part of a wider
        // redesign of focus control.
        if (mUrlBar != null && mUrlBar.isFocused()) mUrlBar.clearFocus();
        boolean wasVisible = false;
        if (hasFocus()) {
            KeyboardVisibilityDelegate keyboardVisibilityDelegate =
                    KeyboardVisibilityDelegate.getInstance();
            wasVisible = keyboardVisibilityDelegate.isKeyboardShowing(getContext(), this);
            if (wasVisible) {
                keyboardVisibilityDelegate.hideKeyboard(this);
            }
        }
        if (wasVisible) {
            mPostHideKeyboardTask = postHideTask;
        } else {
            postHideTask.run();
        }
    }

    /**
     * Sets the appropriate objects this class should represent.
     * @param tabModelSelector        The {@link TabModelSelector} this View should hold and
     *                                represent.
     * @param tabCreatorManager       The {@link TabCreatorManager} for this view.
     */
    public void onFinishNativeInitialization(
            TabModelSelector tabModelSelector, TabCreatorManager tabCreatorManager) {
        assert mLayoutManager != null;
        mLayoutManager.init(
                tabModelSelector,
                tabCreatorManager,
                mControlContainer,
                mCompositorView.getResourceManager().getDynamicResourceLoader(),
                mTopUiThemeColorProvider);

        mTabModelSelector = tabModelSelector;
        tabModelSelector.addObserver(
                new TabModelSelectorObserver() {
                    @Override
                    public void onChange() {
                        onContentChanged();
                    }

                    @Override
                    public void onTabStateInitialized() {
                        // Tab state is initialized, so remove background if we've not yet done so
                        // and a frame is ready.
                        if (mDelayTempStripRemoval
                                && mSetBackgroundRunnable != null
                                && mCanSetBackground) {
                            runSetBackgroundRunnable();
                        }

                        // If real tab strip is ready to be drawn, record how long it took for the
                        // tab state to be initialized.
                        if (mBuffersSwappedTimestamp != 0) {
                            RecordHistogram.recordTimesHistogram(
                                    "Android.TabStrip.TimeToInitializeTabStateAfterBufferSwap",
                                    SystemClock.elapsedRealtime() - mBuffersSwappedTimestamp);
                        } else {
                            mTabStateInitializedTimestamp = SystemClock.elapsedRealtime();
                        }
                    }

                    @Override
                    public void onNewTabCreated(Tab tab, @TabCreationState int creationState) {
                        initializeTab(tab);
                        maybeInitializeSetBackgroundRunnableTimeout();
                    }
                });

        onContentChanged();
        mNativeInitialized = true;
    }

    private void updateContentOverlayVisibility(boolean show) {
        if (mView == null) return;
        WebContents webContents = getWebContents();
        if (show) {
            if (mView != getCurrentTab().getView() || mView.getParent() == this) return;
            // During tab creation, we temporarily add the new tab's view to a FrameLayout to
            // measure and lay it out. This way we could show the animation in the stack view.
            // Therefore we should remove the view from that temporary FrameLayout here.
            UiUtils.removeViewFromParent(mView);

            if (webContents != null) {
                assert !webContents.isDestroyed();
                getContentView().setVisibility(View.VISIBLE);
                tryUpdateControlsAndWebContentsSizing();
            }

            // CompositorView always has index of 0.
            // TODO(crbug.com/40770763): Look into enforcing the z-order of the views.
            addView(mView, 1);

            setFocusable(false);
            setFocusableInTouchMode(false);

            // Claim focus for the new view unless the user is currently using the URL bar.
            if (mUrlBar == null || !mUrlBar.hasFocus()) mView.requestFocus();
        } else {
            if (mView.getParent() == this) {
                setFocusable(mCanBeFocusable);
                setFocusableInTouchMode(mCanBeFocusable);

                if (webContents != null && !webContents.isDestroyed()) {
                    getContentView().setVisibility(View.INVISIBLE);
                }
                removeView(mView);
            }
        }
    }

    @Override
    public void onContentChanged() {
        if (mTabModelSelector == null) {
            // Not yet initialized, onContentChanged() will eventually get called by
            // setTabModelSelector.
            return;
        }
        Tab tab = mTabModelSelector.getCurrentTab();
        setTab(tab);
    }

    @VisibleForTesting
    void onWillShowBrowserControls(boolean viewTransitionOptIn) {
        // TODO(bokan): Flag guarding potential new behavior
        // https://crbug.com/332331777.
        if (!viewTransitionOptIn && !ChromeFeatureList.sBrowserControlsEarlyResize.isEnabled()) {
            return;
        }

        // Let observers know the controls will be shown, resize the web content
        // immediately rather than waiting for the controls animation to finish. This
        // helps makes the resize more predictable, in particular, when capturing
        // snapshots of outgoing content for a view transition.
        if (mControlsResizeView) return;
        mControlsResizeView = true;
        updateWebContentsSize(getCurrentTab());
        onControlsResizeViewChanged(getWebContents(), mControlsResizeView);
    }

    private void setTab(Tab tab) {
        if (tab != null) {
            tab.loadIfNeeded(TabLoadIfNeededCaller.SET_TAB);
        }

        View newView = tab != null ? tab.getView() : null;
        if (mView == newView) return;

        // TODO(dtrainor): Look into changing this only if the views differ, but still parse the
        // WebContents list even if they're the same.
        updateContentOverlayVisibility(false);

        if (mTabVisible != tab) {
            // Reset the geometrychange event flag so it can fire on the current active tab.
            mHasKeyboardGeometryChangeFired = false;
            if (mTabVisible != null) mTabVisible.removeObserver(mTabObserver);
            if (tab != null) {
                tab.addObserver(mTabObserver);
                mCompositorView.onTabChanged();
            }
            updateViewStateListener(tab != null ? tab.getContentView() : null);
        }

        mTabVisible = tab;
        mView = newView;

        updateContentOverlayVisibility(mContentOverlayVisiblity);

        if (mTabVisible != null) initializeTab(mTabVisible);

        if (mOnscreenContentProvider == null) {
            mOnscreenContentProvider =
                    new OnscreenContentProvider(getContext(), this, getWebContents());
        } else {
            mOnscreenContentProvider.onWebContentsChanged(getWebContents());
        }

        releaseDragAndDropPermissions();
    }

    private void updateViewStateListener(ContentView newContentView) {
        if (mContentView != null) {
            mContentView.removeOnHierarchyChangeListener(this);
            mContentView.setDeferKeepScreenOnChanges(false);
            mContentView.setEventOffsetHandlerForDragDrop(null);
        }
        if (newContentView != null) {
            newContentView.addOnHierarchyChangeListener(this);
            newContentView.setEventOffsetHandlerForDragDrop(mEventOffsetHandler);
        }
        mContentView = newContentView;
    }

    @VisibleForTesting
    void updateVirtualKeyboardMode(@VirtualKeyboardMode.EnumType int newMode) {
        // UNSET means the author hasn't explicitly set a preference but the mode should have been
        // set to the default in that case.
        assert mVirtualKeyboardMode != VirtualKeyboardMode.UNSET;

        if (newMode == VirtualKeyboardMode.UNSET) {
            newMode = defaultVirtualKeyboardMode();
        }

        if (mVirtualKeyboardMode == newMode) return;

        mVirtualKeyboardMode = newMode;

        if (mApplicationBottomInsetSupplier != null) {
            mApplicationBottomInsetSupplier.setVirtualKeyboardMode(mVirtualKeyboardMode);
        }
    }

    /**
     * Sets the correct size for {@link View} on {@code tab} and sets the correct rendering
     * parameters on {@link WebContents} on {@code tab}.
     * @param tab The {@link Tab} to initialize.
     */
    private void initializeTab(Tab tab) {
        WebContents webContents = tab.getWebContents();
        if (webContents != null) {
            onPhysicalBackingSizeChanged(
                    webContents, mCompositorView.getWidth(), mCompositorView.getHeight());
            onControlsResizeViewChanged(webContents, mControlsResizeView);

            updateVirtualKeyboardMode(webContents.getVirtualKeyboardMode());
        } else if (tab.getView() != null) {
            updateVirtualKeyboardMode(VirtualKeyboardMode.UNSET);
        }

        if (tab.getView() == null) return;

        // Update WebContents' size only if the currently visible View is the ContentView. If
        // unattached, the ContentView will be sized here to ensure it stays in sync with
        // WebContents but other types of Views can just wait for layout as usual.
        if (tab.getView() != tab.getContentView()) return;

        updateWebContentsSize(tab);
    }

    @Override
    public void invalidateAccessibilityProvider() {
        if (mNodeProvider != null) {
            mNodeProvider.sendEventForVirtualView(
                    mNodeProvider.getAccessibilityFocusedVirtualViewId(),
                    AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
            mNodeProvider.invalidateRoot();
        }
    }

    // ChromeAccessibilityUtil.Observer

    @Override
    public void onAccessibilityModeChanged(boolean enabled) {
        // Instantiate and install the accessibility node provider on this view if necessary.
        // This overrides any hover event listeners or accessibility delegates
        // that may have been added elsewhere.
        assert mLayoutManager != null;
        if (enabled && (mNodeProvider == null)) {
            mAccessibilityView =
                    new View(getContext()) {
                        boolean mIsCheckingForVirtualViews;
                        final List<VirtualView> mVirtualViews = new ArrayList<>();

                        /**
                         * Checks if there are any a11y focusable VirtualViews. If there are, set the view
                         * to be View.IMPORTANT_FOR_ACCESSIBILITY_AUTO (and therefore return true). If there
                         * are not, set the view to be View.IMPORTANT_FOR_ACCESSIBILITY_NO (and therefore
                         * return false).
                         *
                         * @return Whether or not the view should be a11y focusable.
                         */
                        @Override
                        public boolean isImportantForAccessibility() {
                            if (mNativeInitialized && !mIsCheckingForVirtualViews) {
                                mIsCheckingForVirtualViews = true;
                                mVirtualViews.clear();
                                mLayoutManager.getVirtualViews(mVirtualViews);
                                int importantForAccessibility =
                                        mVirtualViews.size() == 0
                                                ? View.IMPORTANT_FOR_ACCESSIBILITY_NO
                                                : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
                                if (getImportantForAccessibility() != importantForAccessibility) {
                                    setImportantForAccessibility(importantForAccessibility);
                                    sendAccessibilityEvent(
                                            AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
                                }
                                mIsCheckingForVirtualViews = false;
                            }

                            return super.isImportantForAccessibility();
                        }
                    };
            addView(mAccessibilityView);
            mNodeProvider = new CompositorAccessibilityProvider(mAccessibilityView);
            ViewCompat.setAccessibilityDelegate(mAccessibilityView, mNodeProvider);
        }
    }

    // TabObscuringHandler.Observer

    @Override
    public void updateObscured(boolean obscureTabContent, boolean obscureToolbar) {
        setFocusable(!obscureTabContent);
    }

    /**
     * Class used to provide a virtual view hierarchy to the Accessibility
     * framework for this view and its contained items.
     * <p>
     * <strong>NOTE:</strong> This class is fully backwards compatible for
     * compilation, but will only provide touch exploration on devices running
     * Ice Cream Sandwich and above.
     * </p>
     */
    private class CompositorAccessibilityProvider extends ExploreByTouchHelper {
        private final float mDpToPx;
        List<VirtualView> mVirtualViews = new ArrayList<>();
        private final Rect mPlaceHolderRect = new Rect(0, 0, 1, 1);
        private static final String PLACE_HOLDER_STRING = "";
        private final RectF mTouchTarget = new RectF();
        private final Rect mPixelRect = new Rect();

        public CompositorAccessibilityProvider(View forView) {
            super(forView);
            mDpToPx = getContext().getResources().getDisplayMetrics().density;
        }

        @Override
        protected int getVirtualViewAt(float x, float y) {
            if (mVirtualViews == null) return INVALID_ID;
            for (int i = 0; i < mVirtualViews.size(); i++) {
                if (mVirtualViews.get(i).checkClickedOrHovered(x / mDpToPx, y / mDpToPx)) {
                    return i;
                }
            }
            return INVALID_ID;
        }

        @Override
        protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
            if (mLayoutManager == null) return;
            mVirtualViews.clear();
            mLayoutManager.getVirtualViews(mVirtualViews);
            for (int i = 0; i < mVirtualViews.size(); i++) {
                virtualViewIds.add(i);
            }
        }

        @Override
        protected boolean onPerformActionForVirtualView(
                int virtualViewId, int action, Bundle arguments) {
            switch (action) {
                case AccessibilityNodeInfoCompat.ACTION_CLICK:
                    mVirtualViews.get(virtualViewId).handleClick(LayoutManagerImpl.time());
                    return true;
            }

            return false;
        }

        @Override
        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
            if (mVirtualViews == null || mVirtualViews.size() <= virtualViewId) {
                // TODO(clholgat): Remove this work around when the Android bug is fixed.
                // crbug.com/420177
                event.setContentDescription(PLACE_HOLDER_STRING);
                return;
            }
            VirtualView view = mVirtualViews.get(virtualViewId);

            event.setContentDescription(view.getAccessibilityDescription());
            event.setClassName(CompositorViewHolder.class.getName());
        }

        @Override
        protected void onPopulateNodeForVirtualView(
                int virtualViewId, AccessibilityNodeInfoCompat node) {
            if (mVirtualViews == null || mVirtualViews.size() <= virtualViewId) {
                // TODO(clholgat): Remove this work around when the Android bug is fixed.
                // crbug.com/420177
                node.setBoundsInParent(mPlaceHolderRect);
                node.setContentDescription(PLACE_HOLDER_STRING);
                return;
            }
            VirtualView view = mVirtualViews.get(virtualViewId);
            view.getTouchTarget(mTouchTarget);

            node.setBoundsInParent(rectToPx(mTouchTarget));
            node.setContentDescription(view.getAccessibilityDescription());
            if (view.hasClickAction()) {
                node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
            }
            node.addAction(AccessibilityNodeInfoCompat.ACTION_FOCUS);
            if (view.hasLongClickAction()) {
                node.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
            }
        }

        private Rect rectToPx(RectF rect) {
            rect.roundOut(mPixelRect);
            mPixelRect.left = (int) (mPixelRect.left * mDpToPx);
            mPixelRect.top = (int) (mPixelRect.top * mDpToPx);
            mPixelRect.right = (int) (mPixelRect.right * mDpToPx);
            mPixelRect.bottom = (int) (mPixelRect.bottom * mDpToPx);

            // Don't let any zero sized rects through, they'll cause parent
            // size errors in L.
            if (mPixelRect.width() == 0) {
                mPixelRect.right = mPixelRect.left + 1;
            }
            if (mPixelRect.height() == 0) {
                mPixelRect.bottom = mPixelRect.top + 1;
            }
            return mPixelRect;
        }
    }

    // Should be called any time inputs used to compute `needsSwapCallback` changes.
    private void updateNeedsSwapBuffersCallback() {
        boolean needsSwapCallback =
                !mHasDrawnOnce
                        || !mOnCompositorLayoutCallbacks.isEmpty()
                        || !mDidSwapFrameCallbacks.isEmpty()
                        || !mDidSwapBuffersCallbacks.isEmpty();
        mCompositorView.setRenderHostNeedsDidSwapBuffersCallback(needsSwapCallback);
    }

    void setCompositorViewForTesting(CompositorView compositorView) {
        mCompositorView = compositorView;
    }

    @VirtualKeyboardMode.EnumType
    public int getVirtualKeyboardModeForTesting() {
        return mVirtualKeyboardMode;
    }

    /** Release any DragAndDropPermissions currently held. */
    private void releaseDragAndDropPermissions() {
        if (mDragAndDropPermissions != null) {
            mDragAndDropPermissions.release();
            mDragAndDropPermissions = null;
        }
    }
}