chromium/chrome/android/java/src/org/chromium/chrome/browser/compositor/layouts/eventfilter/AreaMotionEventFilter.java

// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.compositor.layouts.eventfilter;

import android.content.Context;
import android.graphics.RectF;
import android.view.MotionEvent;

import androidx.annotation.VisibleForTesting;

/**
 * A {@link AreaMotionEventFilter} intercepts all events that start in a specific Rect on the
 * screen.
 */
public class AreaMotionEventFilter extends MotionEventFilter {
    private final RectF mTriggerRect = new RectF();

    /** Whether a down event has occurred inside of the specified area. */
    private boolean mHasDownEventInArea;

    /** Whether a hover enter or move event has occurred inside of the specified area. */
    private boolean mHasHoverEnterOrMoveEventInArea;

    /** Whether a hover exit event has occurred from the specified area. */
    private boolean mHoverExitedArea;

    /**
     * Creates a {@link AreaMotionEventFilter}.
     * @param context       The context to build the gesture handler under.
     * @param handler       The handler to be notified of gesture events.
     * @param triggerRect   The area that events should be stolen from in dp.
     */
    public AreaMotionEventFilter(Context context, MotionEventHandler handler, RectF triggerRect) {
        this(context, handler, triggerRect, true);
    }

    /**
     * Creates a {@link AreaMotionEventFilter}.
     * @param context       The context to build the gesture handler under.
     * @param handler       The handler to be notified of gesture events.
     * @param triggerRect   The area that events should be stolen from in dp.
     * @param autoOffset    Whether or not to offset touch events.
     */
    public AreaMotionEventFilter(
            Context context, MotionEventHandler handler, RectF triggerRect, boolean autoOffset) {
        super(context, handler, autoOffset);
        setEventArea(triggerRect);
    }

    /**
     * Creates a {@link AreaMotionEventFilter}.
     * @param context               The context to build the gesture handler under.
     * @param handler               The handler to be notified of gesture events.
     * @param triggerRect           The area that events should be stolen from in dp.
     * @param autoOffset            Whether or not to offset touch events.
     * @param useDefaultLongPress   Whether or not to use the default long press behavior.
     */
    public AreaMotionEventFilter(
            Context context,
            MotionEventHandler handler,
            RectF triggerRect,
            boolean autoOffset,
            boolean useDefaultLongPress) {
        super(context, handler, autoOffset, useDefaultLongPress);
        setEventArea(triggerRect);
    }

    /**
     * @param rect The area that events should be stolen from in dp.
     */
    public void setEventArea(RectF rect) {
        if (rect == null) {
            mTriggerRect.setEmpty();
        } else {
            mTriggerRect.set(rect);
        }
    }

    @Override
    public boolean onTouchEventInternal(MotionEvent e) {
        // If the action is up or down, consider it to be a new gesture.
        if (e.getActionMasked() == MotionEvent.ACTION_UP
                || e.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mHasDownEventInArea = false;
        }
        return super.onTouchEventInternal(e);
    }

    @Override
    public boolean onInterceptTouchEventInternal(MotionEvent e, boolean isKeyboardShowing) {
        // If the action is up or down, consider it to be a new gesture.
        if (e.getActionMasked() == MotionEvent.ACTION_UP
                || e.getActionMasked() == MotionEvent.ACTION_DOWN) {
            mHasDownEventInArea = false;
        }
        if (isMotionEventInArea(e)) {
            if (e.getActionMasked() == MotionEvent.ACTION_DOWN) {
                mHasDownEventInArea = true;
            } else if (!mHasDownEventInArea) {
                return false;
            }
            return super.onInterceptTouchEventInternal(e, isKeyboardShowing);
        }
        return false;
    }

    @Override
    public boolean onHoverEventInternal(MotionEvent e) {
        // |mHoverExitedArea| determines whether a hover event within the parent view in which
        // |mTriggerRect| is present causes a hover out of this rect; in this case, we will process
        // this action as an ACTION_HOVER_EXIT from the rect area.
        if (mHoverExitedArea) {
            mHoverExitedArea = false;
            MotionEvent exitEvent = MotionEvent.obtain(e);
            exitEvent.setAction(MotionEvent.ACTION_HOVER_EXIT);
            exitEvent.recycle();
            return super.onHoverEventInternal(exitEvent);
        }
        return super.onHoverEventInternal(e);
    }

    @Override
    public boolean onInterceptHoverEventInternal(MotionEvent e) {
        if (isMotionEventInArea(e)) {
            if (e.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
                    || e.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
                mHasHoverEnterOrMoveEventInArea = true;
                return super.onInterceptHoverEventInternal(e);
            } else if (e.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
                // A hover exit event is recorded inside the filter area when another screen
                // interaction through a gesture event occurs. In this case, we will give a chance
                // to the event filter to handle such an exit event if a previous hover entry/move
                // event was intercepted.
                if (mHasHoverEnterOrMoveEventInArea) {
                    mHasHoverEnterOrMoveEventInArea = false;
                    return super.onInterceptHoverEventInternal(e);
                }
            }
        } else {
            // If there was a previous hover into/within the rect, potentially handle hovering out
            // of this rect.
            if (mHasHoverEnterOrMoveEventInArea) {
                mHasHoverEnterOrMoveEventInArea = false;
                mHoverExitedArea = true;
                return super.onInterceptHoverEventInternal(e);
            }
        }
        return false;
    }

    /** Gets the motion event area rect for testing purposes. */
    public RectF getEventAreaForTesting() {
        return mTriggerRect;
    }

    @VisibleForTesting
    boolean isMotionEventInArea(MotionEvent e) {
        return mTriggerRect.contains(e.getX() * mPxToDp, e.getY() * mPxToDp);
    }

    boolean getHasHoverEnterOrMoveEventInAreaForTesting() {
        return mHasHoverEnterOrMoveEventInArea;
    }

    boolean getHoverExitedAreaForTesting() {
        return mHoverExitedArea;
    }
}