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

import android.app.Activity;
import android.content.Context;
import android.graphics.RectF;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.Supplier;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanelManager.PanelPriority;
import org.chromium.chrome.browser.compositor.layouts.Layout;
import org.chromium.chrome.browser.compositor.layouts.LayoutManagerImpl;
import org.chromium.chrome.browser.compositor.layouts.SceneChangeObserver;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.MotionEventHandler;
import org.chromium.chrome.browser.compositor.layouts.eventfilter.OverlayPanelEventFilter;
import org.chromium.chrome.browser.layouts.EventFilter;
import org.chromium.chrome.browser.layouts.SceneOverlay;
import org.chromium.chrome.browser.layouts.components.VirtualView;
import org.chromium.chrome.browser.layouts.scene_layer.SceneOverlayLayer;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.ScrollDirection;
import org.chromium.components.browser_ui.widget.gesture.SwipeGestureListener.SwipeHandler;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.resources.ResourceManager;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/** Controls the Overlay Panel. */
public class OverlayPanel extends OverlayPanelAnimation
        implements ActivityStateListener,
                SwipeHandler,
                MotionEventHandler,
                OverlayPanelContentFactory,
                SceneOverlay {
    /** The delay after which the hide progress will be hidden. */
    private static final long HIDE_PROGRESS_BAR_DELAY_MS = 1000 / 60 * 4;

    /** State of the Overlay Panel. */
    @IntDef({
        PanelState.UNDEFINED,
        PanelState.CLOSED,
        PanelState.PEEKED,
        PanelState.EXPANDED,
        PanelState.MAXIMIZED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface PanelState {
        // Values can't have gaps and should be numerated from 0.
        // Values CLOSED - MAXIMIZED are sorted and show next states.
        // TODO(pedrosimonetti): consider removing the UNDEFINED state
        int UNDEFINED = 0;
        int CLOSED = 1;
        int PEEKED = 2;
        int EXPANDED = 3;
        int MAXIMIZED = 4;
        int NUM_ENTRIES = 5;
    }

    /** The reason for a change in the Overlay Panel's state. */
    @IntDef({
        StateChangeReason.UNKNOWN,
        StateChangeReason.RESET,
        StateChangeReason.BACK_PRESS,
        StateChangeReason.TEXT_SELECT_TAP,
        StateChangeReason.TEXT_SELECT_LONG_PRESS,
        StateChangeReason.INVALID_SELECTION,
        StateChangeReason.CLEARED_SELECTION,
        StateChangeReason.BASE_PAGE_TAP,
        StateChangeReason.BASE_PAGE_SCROLL,
        StateChangeReason.SEARCH_BAR_TAP,
        StateChangeReason.SERP_NAVIGATION,
        StateChangeReason.TAB_PROMOTION,
        StateChangeReason.CLICK,
        StateChangeReason.SWIPE,
        StateChangeReason.FLING,
        StateChangeReason.OPTIN,
        StateChangeReason.OPTOUT,
        StateChangeReason.CLOSE_BUTTON,
        StateChangeReason.PANEL_SUPPRESS,
        StateChangeReason.PANEL_UNSUPPRESS,
        StateChangeReason.TAP_SUPPRESS,
        StateChangeReason.NAVIGATION
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface StateChangeReason {
        int UNKNOWN = 0;
        int RESET = 1;
        int BACK_PRESS = 2;
        int TEXT_SELECT_TAP = 3;
        int TEXT_SELECT_LONG_PRESS = 4;
        int INVALID_SELECTION = 5;
        int CLEARED_SELECTION = 6;
        int BASE_PAGE_TAP = 7;
        int BASE_PAGE_SCROLL = 8;
        int SEARCH_BAR_TAP = 9;
        int SERP_NAVIGATION = 10;
        int TAB_PROMOTION = 11;
        int CLICK = 12;
        int SWIPE = 13;
        int FLING = 14;
        int OPTIN = 15;
        int OPTOUT = 16;
        int CLOSE_BUTTON = 17;
        int PANEL_SUPPRESS = 18;
        int PANEL_UNSUPPRESS = 19;
        int TAP_SUPPRESS = 20;
        int NAVIGATION = 21;
        // Always update MAX_VALUE to match the last StateChangeReason in the list.
        int MAX_VALUE = 21;
    }

    /** A layout manager for tracking changes in the active layout. */
    private final LayoutManagerImpl mLayoutManager;

    /** The observer that reacts to scene-change events. */
    private final SceneChangeObserver mSceneChangeObserver;

    /** Used to measure the browser controls. */
    private final BrowserControlsStateProvider mBrowserControlsStateProvider;

    /** The {@link WindowAndroid} for the current activity.  */
    private final WindowAndroid mWindowAndroid;

    /** The {@link Profile} associated with this Panel. */
    private final Profile mProfile;

    /**
     * The {@link CompositorViewHolder}, used as an anchor view. Also injected into other classes.
     */
    private final ViewGroup mCompositorViewHolder;

    /** Supplies the current activity {@link Tab}. */
    private final Supplier<Tab> mCurrentTabSupplier;

    /** OverlayPanel manager handle for notifications of opening and closing. */
    protected final OverlayPanelManager mPanelManager;

    /** The activity this panel is in. */
    protected Activity mActivity;

    /** The initial height of the Overlay Panel. */
    private float mInitialPanelHeight;

    /** The initial location of a touch on the panel */
    private float mInitialPanelTouchY;

    /** Whether a touch gesture has been detected. */
    private boolean mHasDetectedTouchGesture;

    /** The EventFilter that this panel uses. */
    protected EventFilter mEventFilter;

    /** That factory that creates OverlayPanelContents. */
    private OverlayPanelContentFactory mContentFactory;

    /** Container for content the panel will show. */
    private OverlayPanelContent mContent;

    /** If the base page text controls have been cleared. */
    private boolean mDidClearTextControls;

    /** If the panel should be ignoring swipe events (for compatibility mode). */
    private boolean mIgnoreSwipeEvents;

    /** This is used to make sure there is one show request to one close request. */
    private boolean mPanelShown;

    /** Sequence number for peekPanel() calls to distinguish stale from fresh. */
    private int mLastPeekSequence;

    /**
     * Cache the viewport width and height of the screen to filter SceneOverlay#onSizeChanged
     * events.
     */
    private float mViewportWidth;

    private float mViewportHeight;

    // ============================================================================================
    // Constructor
    // ============================================================================================

    /**
     * @param context The current Android {@link Context}.
     * @param layoutManager A {@link LayoutManagerImpl} for observing changes in the active layout.
     * @param panelManager The {@link OverlayPanelManager} responsible for showing panels.
     * @param browserControlsStateProvider The {@link BrowserControlsStateProvider} for measuring
     *     controls.
     * @param windowAndroid The {@link WindowAndroid} for the current activity.
     * @param profile The Profile this OverlayPanel is associated with.
     * @param compositorViewHolder The {@link CompositorViewHolder}
     * @param toolbarHeightDp The height of the toolbar in dp.
     * @param currentTabSupplier Supplies the current {@link Tab}.
     */
    public OverlayPanel(
            @NonNull Context context,
            @NonNull LayoutManagerImpl layoutManager,
            @NonNull OverlayPanelManager panelManager,
            @NonNull BrowserControlsStateProvider browserControlsStateProvider,
            @NonNull WindowAndroid windowAndroid,
            @NonNull Profile profile,
            @NonNull ViewGroup compositorViewHolder,
            float toolbarHeightDp,
            @NonNull Supplier<Tab> currentTabSupplier) {
        super(context, layoutManager, toolbarHeightDp);
        mLayoutManager = layoutManager;
        mContentFactory = this;
        mBrowserControlsStateProvider = browserControlsStateProvider;
        mWindowAndroid = windowAndroid;
        mProfile = profile;
        mCompositorViewHolder = compositorViewHolder;
        mCurrentTabSupplier = currentTabSupplier;

        mPanelManager = panelManager;
        mPanelManager.registerPanel(this);
        mEventFilter = new OverlayPanelEventFilter(mContext, this);

        mSceneChangeObserver =
                new SceneChangeObserver() {
                    @Override
                    public void onSceneChange(Layout layout) {
                        closePanel(StateChangeReason.UNKNOWN, false);
                    }
                };

        mLayoutManager.addSceneChangeObserver(mSceneChangeObserver);
    }

    /** Return the {@link WindowAndroid} this panel is associated with. */
    protected WindowAndroid getWindowAndroid() {
        return mWindowAndroid;
    }

    /** Return the {@link Profile} this panel is associated with. */
    protected Profile getProfile() {
        return mProfile;
    }

    /**
     * Return the {@link ViewGroup} corresponding to the CompositorViewHolder of the Activity
     * containing this OverlayPanel.
     */
    protected ViewGroup getCompositorViewHolder() {
        return mCompositorViewHolder;
    }

    /** Return the {@link Tab} supplier for the Activity containing this OverlayPanel. */
    protected Supplier<Tab> getCurrentTabSupplier() {
        return mCurrentTabSupplier;
    }

    /** Destroy the native components associated with this panel's content. */
    public void destroy() {
        closePanel(StateChangeReason.UNKNOWN, false);
        mLayoutManager.removeSceneChangeObserver(mSceneChangeObserver);
        ApplicationStatus.unregisterActivityStateListener(this);
    }

    /**
     * Destroy the components associated with this panel. This should be overridden by panel
     * implementations to destroy text views and other elements.
     */
    protected void destroyComponents() {
        destroyOverlayPanelContent();
    }

    @Override
    protected void onClosed(@StateChangeReason int reason) {
        mPanelShown = false;
        setBasePageTextControlsVisibility(true);
        destroyComponents();
        mPanelManager.notifyPanelClosed(this, reason);
    }

    // ============================================================================================
    // General API
    // ============================================================================================

    /**
     * @return True if the panel is in the PEEKED state.
     */
    public boolean isPeeking() {
        return doesPanelHeightMatchState(PanelState.PEEKED);
    }

    /**
     * @return Whether the Panel is in its expanded state.
     */
    public boolean isExpanded() {
        return doesPanelHeightMatchState(PanelState.EXPANDED);
    }

    @Override
    public void closePanel(@StateChangeReason int reason, boolean animate) {
        // If the panel hasn't peeked, then it shouldn't need to close.
        if (!mPanelShown) return;

        super.closePanel(reason, animate);
    }

    /**
     * Request that this panel be shown.
     * @param reason The reason the panel is being shown.
     */
    public void requestPanelShow(@StateChangeReason int reason) {
        if (mPanelShown) return;

        if (mPanelManager != null) {
            mPanelManager.requestPanelShow(this, reason);
        }
    }

    public int getLastPeekSequence() {
        return mLastPeekSequence;
    }

    @Override
    public void peekPanel(@StateChangeReason int reason) {
        // TODO(mdjones): This is making a protected API public and should be removed. Animation
        // should only be controlled by the OverlayPanelManager.

        // Since the OverlayPanelManager can show panels without requestPanelShow being called, the
        // flag for the panel being shown should be set to true here.
        mPanelShown = true;
        ++mLastPeekSequence;
        super.peekPanel(reason);
    }

    /**
     * @param url The URL that the panel should load.
     */
    public void loadUrlInPanel(String url) {
        getOverlayPanelContent().loadUrl(url, true);
    }

    /**
     * @param url The URL that the panel should load.
     * @param shouldLoadImmediately If the URL should be loaded immediately when this method is
     *                              called.
     */
    public void loadUrlInPanel(String url, boolean shouldLoadImmediately) {
        getOverlayPanelContent().loadUrl(url, shouldLoadImmediately);
    }

    /**
     * @return True if a URL has been loaded in the panel's current WebContents.
     */
    public boolean isProcessingPendingNavigation() {
        return mContent != null && mContent.isProcessingPendingNavigation();
    }

    /**
     * @param activity The {@link Activity} associated with the panel.
     */
    public void setActivity(Activity activity) {
        mActivity = activity;
        if (mActivity != null) {
            ApplicationStatus.registerStateListenerForActivity(this, mActivity);
        } else {
            throw new RuntimeException("Activity provided to OverlayPanel cannot be null!");
        }
    }

    /**
     * Notify the panel's content that it has been touched.
     * @param x The X position of the touch in dp.
     */
    public void notifyBarTouched(float x) {
        if (!isCoordinateInsideCloseButton(x)) {
            getOverlayPanelContent().showContent();
        }
    }

    /**
     * Acknowledges that there was a touch in the search content view, though no immediate action
     * needs to be taken. This should be overridden by child classes.
     * TODO(mdjones): Get a better name for this.
     */
    public void onTouchSearchContentViewAck() {}

    /**
     * Get a panel's display priority. This has a default to MEDIUM and should be overridden by
     * child classes.
     * @return The panel's display priority.
     */
    public @PanelPriority int getPriority() {
        return PanelPriority.MEDIUM;
    }

    /**
     * @return True if a panel can be suppressed. This should be overridden by each panel.
     */
    public boolean canBeSuppressed() {
        return false;
    }

    /**
     * @return The absolute amount in DP that the browser controls have shifted off screen.
     */
    protected float getBrowserControlsOffsetDp() {
        if (mActivity == null) return 0.0f;
        return -mBrowserControlsStateProvider.getTopControlOffset() * mPxToDp;
    }

    /**
     * Set the visibility of the base page text selection controls. This will also attempt to
     * remove focus from the base page to clear any open controls.
     * @param visible If the text controls are visible.
     */
    protected void setBasePageTextControlsVisibility(boolean visible) {
        if (mActivity == null) return;
        if (!mCurrentTabSupplier.hasValue()) return;

        WebContents baseWebContents = mCurrentTabSupplier.get().getWebContents();
        if (baseWebContents == null) return;

        // If the panel does not have focus or isn't open, return.
        if (isPanelOpened() && mDidClearTextControls && !visible) return;
        if (!isPanelOpened() && !mDidClearTextControls && visible) return;

        mDidClearTextControls = !visible;

        SelectionPopupController spc = SelectionPopupController.fromWebContents(baseWebContents);
        if (!visible) spc.setPreserveSelectionOnNextLossOfFocus(true);
        updateFocus(baseWebContents, visible);

        spc.updateTextSelectionUI(visible);
    }

    // Claim or lose focus of the content view of the base WebContents. This keeps
    // the state of the text selected for overlay in consistent way.
    private static void updateFocus(WebContents baseWebContents, boolean focusBaseView) {
        View baseContentView =
                baseWebContents.getViewAndroidDelegate() != null
                        ? baseWebContents.getViewAndroidDelegate().getContainerView()
                        : null;
        if (baseContentView == null) return;

        if (focusBaseView) {
            baseContentView.requestFocus();
        } else {
            baseContentView.clearFocus();
        }
    }

    /**
     * Returns whether the panel has been activated -- asked to show.  It may not yet be physically
     * showing due animation.  Use {@link #isShowing} instead to determine if the panel is
     * physically visible.
     * @return Whether the panel is showing or about to show.
     */
    public boolean isActive() {
        return mPanelShown;
    }

    // ============================================================================================
    // ActivityStateListener
    // ============================================================================================

    @Override
    public void onActivityStateChange(Activity activity, int newState) {
        boolean isMultiWindowMode = MultiWindowUtils.getInstance().isInMultiWindowMode(mActivity);

        // In multi-window mode the activity that was interacted with last is resumed and
        // all others are paused. We should not close Contextual Search in this case,
        // because the activity may be visible even though it is paused.
        if (isMultiWindowMode
                && (newState == ActivityState.PAUSED || newState == ActivityState.RESUMED)) {
            return;
        }

        if (newState == ActivityState.RESUMED
                || newState == ActivityState.STOPPED
                || newState == ActivityState.DESTROYED) {
            closePanel(StateChangeReason.UNKNOWN, false);
        }
    }

    // ============================================================================================
    // Content
    // ============================================================================================

    /**
     * @return True if the panel's content view is showing.
     */
    public boolean isContentShowing() {
        return mContent != null && mContent.isContentShowing();
    }

    /**
     * @return The WebContents that this panel currently holds.
     */
    public WebContents getWebContents() {
        return mContent != null ? mContent.getWebContents() : null;
    }

    /**
     * @return The container view that this panel currently holds.
     */
    public ViewGroup getContainerView() {
        return mContent != null ? mContent.getContainerView() : null;
    }

    /**
     * Call this when a loadUrl request has failed to notify the panel that the WebContents can
     * be reused.  See crbug.com/682953 for details.
     */
    public void onLoadUrlFailed() {
        if (mContent != null) mContent.onLoadUrlFailed();
    }

    /** Progress observer progress indicator animation for a panel. */
    public class PanelProgressObserver extends OverlayPanelContentProgressObserver {
        @Override
        public void onProgressBarStarted() {
            setProgressBarCompletion(0);
            setProgressBarVisible(true);
            requestUpdate();
        }

        @Override
        public void onProgressBarUpdated(float progress) {
            setProgressBarCompletion(progress);
            requestUpdate();
        }

        @Override
        public void onProgressBarFinished() {
            // Hides the Progress Bar after a delay to make sure it is rendered for at least
            // a few frames, otherwise its completion won't be visually noticeable.
            new Handler()
                    .postDelayed(
                            new Runnable() {
                                @Override
                                public void run() {
                                    setProgressBarVisible(false);
                                    requestUpdate();
                                }
                            },
                            HIDE_PROGRESS_BAR_DELAY_MS);
        }
    }

    /**
     * Create a new OverlayPanelContent object. This can be overridden for tests.
     *
     * @return A new OverlayPanelContent object.
     */
    @Override
    public OverlayPanelContent createNewOverlayPanelContent() {
        return new OverlayPanelContent(
                new OverlayPanelContentDelegate(),
                new OverlayPanelContentProgressObserver(),
                mActivity,
                mProfile,
                getBarHeight(),
                mCompositorViewHolder,
                mWindowAndroid,
                mCurrentTabSupplier);
    }

    /** Add any other objects that depend on the OverlayPanelContent having already been created. */
    private OverlayPanelContent createNewOverlayPanelContentInternal() {
        OverlayPanelContent content = mContentFactory.createNewOverlayPanelContent();
        content.setContentViewSize(
                getContentViewWidthPx(), getContentViewHeightPx(), isFullWidthSizePanel());
        return content;
    }

    /**
     * @return A new OverlayPanelContent if the instance was null or the existing one.
     */
    protected OverlayPanelContent getOverlayPanelContent() {
        // Only create the content when necessary
        if (mContent == null) {
            mContent = createNewOverlayPanelContentInternal();
        }
        return mContent;
    }

    /** Destroy the native components of the OverlayPanelContent. */
    protected void destroyOverlayPanelContent() {
        // It is possible that an OverlayPanelContent was never created for this panel.
        if (mContent != null) {
            mContent.destroy();
            mContent = null;
        }
    }

    /**
     * Updates the browser controls state for the base tab.  As these values are set at the renderer
     * level, there is potential for this impacting other tabs that might share the same
     * process. See {@link BrowserControlsState.update(Tab, tab, int current, boolean animate)}
     * @param current The desired current state for the controls.  Pass
     *                {@link BrowserControlsState#BOTH} to preserve the current position.
     * @param animate Whether the controls should animate to the specified ending condition or
     *                should jump immediately.
     */
    public void updateBrowserControlsState(int current, boolean animate) {
        TabBrowserControlsConstraintsHelper.update(mCurrentTabSupplier.get(), current, animate);
    }

    /** Sets the top control state based on the internals of the panel. */
    public void updateBrowserControlsState() {
        if (mContent == null) return;

        // Consider the ContentView height to be fullscreen, and inform the system that
        // the Toolbar is always visible (from the Compositor's perspective), even though
        // the Toolbar and Base Page might be offset outside the screen. This means the
        // renderer will consider the ContentView height to be the fullscreen height
        // minus the Toolbar height.
        //
        // This is necessary to fix the bugs: crbug.com/510205 and crbug.com/510206
        mContent.updateBrowserControlsState(isFullWidthSizePanel());
    }

    /**
     * Remove the last entry from history provided it is in a given time frame.
     * @param historyUrl The URL to remove.
     * @param urlTimeMs The time that the URL was visited.
     */
    public void removeLastHistoryEntry(String historyUrl, long urlTimeMs) {
        if (mContent == null) return;
        // Expose OverlayPanelContent method.
        mContent.removeLastHistoryEntry(historyUrl, urlTimeMs);
    }

    /**
     * @return The vertical scroll position of the content.
     */
    public float getContentVerticalScroll() {
        return mContent != null ? mContent.getContentVerticalScroll() : 0.0f;
    }

    // ============================================================================================
    // OverlayPanelBase methods.
    // ============================================================================================

    @Override
    protected void onHeightAnimationFinished() {
        super.onHeightAnimationFinished();

        if (getPanelState() == PanelState.PEEKED || getPanelState() == PanelState.CLOSED) {
            setBasePageTextControlsVisibility(true);
        } else {
            setBasePageTextControlsVisibility(false);
        }
        if (mContent != null) {
            mContent.setPanelTopOffset((int) ((mViewportHeight - getHeight()) / mPxToDp));
        }
    }

    // ============================================================================================
    // Layout Integration
    // ============================================================================================

    /** Updates the Panel so it preserves its state when the orientation changes. */
    protected void updatePanelForOrientationChange() {
        resizePanelToState(getPanelState(), StateChangeReason.UNKNOWN);
    }

    // ============================================================================================
    // Generic Event Handling
    // ============================================================================================

    /** Handles the beginning of the swipe gesture. */
    public void handleSwipeStart() {
        cancelHeightAnimation();

        mHasDetectedTouchGesture = false;
        mInitialPanelHeight = getHeight();
    }

    /**
     * Handles the movement of the swipe gesture.
     *
     * @param ty The movement's total displacement in dps.
     */
    public void handleSwipeMove(float ty) {
        if (mContent != null && ty > 0 && getPanelState() == PanelState.MAXIMIZED) {
            // Resets the Content View scroll position when swiping the Panel down
            // after being maximized.
            mContent.resetContentViewScroll();
        }

        // Negative ty value means an upward movement so subtracting ty means expanding the panel.
        setClampedPanelHeight(mInitialPanelHeight - ty);
        requestUpdate();
    }

    /** Handles the end of the swipe gesture. */
    public void handleSwipeEnd() {
        // This method will be called after handleFling() and handleClick()
        // methods because we also need to track down the onUpOrCancel()
        // action from the Layout. Therefore the animation to the nearest
        // PanelState should only happen when no other gesture has been
        // detected.
        if (!mHasDetectedTouchGesture) {
            mHasDetectedTouchGesture = true;
            animateToNearestState();
        }
    }

    /**
     * Handles the fling gesture.
     *
     * @param velocity The velocity of the gesture in dps per second.
     */
    public void handleFling(float velocity) {
        mHasDetectedTouchGesture = true;
        animateToProjectedState(velocity);
    }

    /**
     * @param x The x coordinate in dp.
     * @return Whether the given |x| coordinate is inside the close button.
     */
    protected boolean isCoordinateInsideCloseButton(float x) {
        if (LocalizationUtils.isLayoutRtl()) {
            return x <= (getCloseIconX() + getCloseIconDimension() + mButtonPaddingDps);
        } else {
            return x >= (getCloseIconX() - mButtonPaddingDps);
        }
    }

    /**
     * @param x The x coordinate in dp.
     * @return Whether the given |x| coordinate is inside the open-in-new-tab button.
     */
    protected boolean isCoordinateInsideOpenTabButton(float x) {
        // Calculation is the same for RTL: within the button plus padding.
        return getOpenTabIconX() - mButtonPaddingDps <= x
                && x <= getOpenTabIconX() + getOpenTabIconDimension() + mButtonPaddingDps;
    }

    /**
     * Handles the click gesture.
     *
     * @param x The x coordinate of the gesture.
     * @param y The y coordinate of the gesture.
     */
    public void handleClick(float x, float y) {
        mHasDetectedTouchGesture = true;
        if (isCoordinateInsideBasePage(x, y)) {
            closePanel(StateChangeReason.BASE_PAGE_TAP, true);
        } else if (isCoordinateInsideBar(x, y) && !onInterceptBarClick()) {
            handleBarClick(x, y);
        }
    }

    /**
     * Handles the click gesture specifically on the bar.
     *
     * @param x The x coordinate of the gesture.
     * @param y The y coordinate of the gesture.
     */
    protected void handleBarClick(float x, float y) {
        if (isPeeking()) {
            expandPanel(StateChangeReason.SEARCH_BAR_TAP);
        }
    }

    /**
     * Allows the click on the bar to be intercepted.
     * @return True if the click on the bar was intercepted by this function.
     */
    protected boolean onInterceptBarClick() {
        return false;
    }

    /**
     * If the panel is intercepting the initial bar swipe event. This should be overridden per
     * panel.
     * @return True if the panel intercepted the initial bar swipe.
     */
    public boolean onInterceptBarSwipe() {
        return false;
    }

    // ============================================================================================
    // Gesture Event helpers
    // ============================================================================================

    /**
     * @param x The x coordinate in dp.
     * @param y The y coordinate in dp.
     * @return Whether the given coordinate is inside the bar area of the overlay.
     */
    public boolean isCoordinateInsideBar(float x, float y) {
        return isCoordinateInsideOverlayPanel(x, y)
                && y >= getOffsetY()
                && y <= (getOffsetY() + getBarContainerHeight());
    }

    /**
     * @param x The x coordinate in dp.
     * @param y The y coordinate in dp.
     * @return Whether the given coordinate is inside the Overlay Content View area.
     */
    public boolean isCoordinateInsideContent(float x, float y) {
        return isCoordinateInsideOverlayPanel(x, y) && y > getContentY();
    }

    /**
     * @return The horizontal offset of the Overlay Content View in dp.
     */
    public float getContentX() {
        return getOffsetX();
    }

    /**
     * @return The vertical offset of the Overlay Content View in dp.
     */
    public float getContentY() {
        return getOffsetY() + getBarContainerHeight();
    }

    /**
     * @param x The x coordinate in dp.
     * @param y The y coordinate in dp.
     * @return Whether the given coordinate is inside the Overlay Panel area.
     */
    public boolean isCoordinateInsideOverlayPanel(float x, float y) {
        return y >= getOffsetY()
                && y <= (getOffsetY() + getHeight())
                && x >= getOffsetX()
                && x <= (getOffsetX() + getWidth());
    }

    /**
     * @param x The x coordinate in dp.
     * @param y The y coordinate in dp.
     * @return Whether the given coordinate is inside the Base Page area.
     */
    private boolean isCoordinateInsideBasePage(float x, float y) {
        return !isCoordinateInsideOverlayPanel(x, y);
    }

    @VisibleForTesting
    public void setOverlayPanelContentFactory(OverlayPanelContentFactory factory) {
        mContentFactory = factory;
    }

    // ============================================================================================
    // GestureHandler and SwipeHandler implementation.
    // ============================================================================================

    @Override
    public void onDown(float x, float y, boolean fromMouse, int buttons) {
        mInitialPanelTouchY = y;
        handleSwipeStart();
    }

    @Override
    public void drag(float x, float y, float deltaX, float deltaY, float tx, float ty) {
        handleSwipeMove(y - mInitialPanelTouchY);
    }

    @Override
    public void onUpOrCancel() {
        handleSwipeEnd();
    }

    @Override
    public void fling(float x, float y, float velocityX, float velocityY) {
        handleFling(velocityY);
    }

    @Override
    public void click(float x, float y, boolean fromMouse, int buttons) {
        handleClick(x, y);
    }

    @Override
    public void onLongPress(float x, float y) {}

    @Override
    public void onPinch(float x0, float y0, float x1, float y1, boolean firstEvent) {}

    @Override
    public void onHoverEnter(float x, float y) {}

    @Override
    public void onHoverMove(float x, float y) {}

    @Override
    public void onHoverExit() {}

    // SwipeHandler implementation.

    @Override
    public void onSwipeStarted(@ScrollDirection int direction, MotionEvent ev) {
        if (onInterceptBarSwipe()) {
            mIgnoreSwipeEvents = true;
            return;
        }
        handleSwipeStart();
    }

    @Override
    public void onSwipeUpdated(MotionEvent current, float tx, float ty, float dx, float dy) {
        if (mIgnoreSwipeEvents) return;
        handleSwipeMove(ty * mPxToDp);
    }

    @Override
    public void onSwipeFinished() {
        if (mIgnoreSwipeEvents) {
            mIgnoreSwipeEvents = false;
            return;
        }
        handleSwipeEnd();
    }

    @Override
    public void onFling(
            @ScrollDirection int direction,
            MotionEvent current,
            float tx,
            float ty,
            float vx,
            float vy) {
        if (mIgnoreSwipeEvents) return;
        handleFling(vy * mPxToDp);
    }

    @Override
    public boolean isSwipeEnabled(@ScrollDirection int direction) {
        return direction == ScrollDirection.UP && isShowing();
    }

    // Other event handlers.

    /**
     * The user has performed a down event and has not performed a move or up yet. This event is
     * commonly used to provide visual feedback to the user to let them know that their action has
     * been recognized.
     * See {@link GestureDetector.SimpleOnGestureListener#onShowPress()}.
     * @param x The x coordinate in dp.
     * @param y The y coordinate in dp.
     */
    public void onShowPress(float x, float y) {}

    // ============================================================================================
    // SceneOverlay implementation.
    // ============================================================================================

    @Override
    public SceneOverlayLayer getUpdatedSceneOverlayTree(
            RectF viewport, RectF visibleViewport, ResourceManager resourceManager, float yOffset) {
        return null;
    }

    @Override
    public boolean isSceneOverlayTreeShowing() {
        return isShowing();
    }

    @Override
    public EventFilter getEventFilter() {
        return mEventFilter;
    }

    @Override
    public void onSizeChanged(
            float width, float height, float visibleViewportOffsetY, int orientation) {
        // Filter events that don't change the viewport width or height.
        if (height != mViewportHeight || width != mViewportWidth) {
            // We only care if the orientation is changing or we're shifting in/out of multi-window.
            // In either case the screen's viewport width or height will certainly change.
            mViewportWidth = width;
            mViewportHeight = height;

            onLayoutChanged(width, height, visibleViewportOffsetY);
            resizePanelContentView();
        }
    }

    /** Resize the panel's ContentView. Apply adjusted bar size to the height. */
    protected void resizePanelContentView() {
        if (!isShowing()) return;

        OverlayPanelContent panelContent = getOverlayPanelContent();

        // Device could have been rotated before panel webcontent creation. Update content size.
        panelContent.setContentViewSize(
                getContentViewWidthPx(), getContentViewHeightPx(), isFullWidthSizePanel());
        panelContent.resizePanelContentView();
    }

    @Override
    public void getVirtualViews(List<VirtualView> views) {
        // TODO(mdjones): Add views for accessibility.
    }

    @Override
    public boolean handlesTabCreating() {
        // If the panel is not opened, do not handle tab creating.
        if (!isPanelOpened()) return false;
        // Updates BrowserControls' State so the Toolbar becomes visible.
        // TODO(pedrosimonetti): The transition when promoting to a new tab is only smooth
        // if the SearchContentView's vertical scroll position is zero. Otherwise the
        // ContentView will appear to jump in the screen. Coordinate with @dtrainor to solve
        // this problem.
        updateBrowserControlsState(BrowserControlsState.BOTH, false);
        return true;
    }

    @Override
    public boolean shouldHideAndroidBrowserControls() {
        return isPanelOpened() && mCanHideAndroidBrowserControls;
    }

    @Override
    public boolean updateOverlay(long time, long dt) {
        if (isPanelOpened()) setBasePageTextControlsVisibility(false);
        return true;
    }

    @Override
    public boolean onBackPressed() {
        if (!isShowing()) return false;
        closePanel(StateChangeReason.BACK_PRESS, true);
        return true;
    }

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

    @Override
    public @BackPressResult int handleBackPress() {
        return onBackPressed() ? BackPressResult.SUCCESS : BackPressResult.FAILURE;
    }
}