Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 093d5e77 authored by Steve McKay's avatar Steve McKay
Browse files

Fix erroneous band selection start.

Fix band selection to not start when crossing off of a grid item into empty space.
This CL also introduces a MotionEvent wrapper class, since MotionEvent
can't be used in tests.
Note that this CL works around several issues with b/23793622.

Bug: 23727363
Change-Id: I010a82db3363d99f2d804db2653a3a25d8cac940
parent 784c787e
Loading
Loading
Loading
Loading
+119 −0
Original line number Original line Diff line number Diff line
@@ -16,8 +16,11 @@


package com.android.documentsui;
package com.android.documentsui;


import android.graphics.Point;
import android.support.v7.widget.RecyclerView;
import android.view.KeyEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;


/**
/**
 * Utility code for dealing with MotionEvents.
 * Utility code for dealing with MotionEvents.
@@ -53,6 +56,20 @@ final class Events {
                || toolType == MotionEvent.TOOL_TYPE_STYLUS;
                || toolType == MotionEvent.TOOL_TYPE_STYLUS;
    }
    }


    /**
     * Returns true if event was triggered by a finger or stylus touch.
     */
    static boolean isActionDown(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_DOWN;
    }

    /**
     * Returns true if event was triggered by a finger or stylus touch.
     */
    static boolean isActionUp(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_UP;
    }

    /**
    /**
     * Returns true if the shift is pressed.
     * Returns true if the shift is pressed.
     */
     */
@@ -66,4 +83,106 @@ final class Events {
    static boolean hasShiftBit(int metaState) {
    static boolean hasShiftBit(int metaState) {
        return (metaState & KeyEvent.META_SHIFT_ON) != 0;
        return (metaState & KeyEvent.META_SHIFT_ON) != 0;
    }
    }

    /**
     * A facade over MotionEvent primarily designed to permit for unit testing
     * of related code.
     */
    interface InputEvent {
        boolean isMouseEvent();
        boolean isPrimaryButtonPressed();
        boolean isSecondaryButtonPressed();
        boolean isShiftKeyDown();

        /** Returns true if the action is the initial press of a mouse or touch. */
        boolean isActionDown();

        /** Returns true if the action is the final release of a mouse or touch. */
        boolean isActionUp();

        Point getOrigin();

        /** Returns true if the there is an item under the finger/cursor. */
        boolean isOverItem();

        /** Returns the adapter position of the item under the finger/cursor. */
        int getItemPosition();
    }

    static final class MotionInputEvent implements InputEvent {
        private final MotionEvent mEvent;
        private final RecyclerView mView;
        private final int mPosition;

        public MotionInputEvent(MotionEvent event, RecyclerView view) {
            mEvent = event;
            mView = view;
            View child = mView.findChildViewUnder(mEvent.getX(), mEvent.getY());
            mPosition = (child != null)
                    ? mView.getChildAdapterPosition(child)
                    : RecyclerView.NO_POSITION;
        }

        @Override
        public boolean isMouseEvent() {
            return Events.isMouseEvent(mEvent);
        }

        @Override
        public boolean isPrimaryButtonPressed() {
            return mEvent.isButtonPressed(MotionEvent.BUTTON_PRIMARY);
        }

        @Override
        public boolean isSecondaryButtonPressed() {
            return mEvent.isButtonPressed(MotionEvent.BUTTON_SECONDARY);
        }

        @Override
        public boolean isShiftKeyDown() {
            return Events.hasShiftBit(mEvent.getMetaState());
        }

        @Override
        public boolean isActionDown() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_DOWN;
        }

        @Override
        public boolean isActionUp() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_UP;
        }

        @Override
        public Point getOrigin() {
            return new Point((int) mEvent.getX(), (int) mEvent.getY());
        }

        @Override
        public boolean isOverItem() {
            return getItemPosition() != RecyclerView.NO_POSITION;
        }

        @Override
        public int getItemPosition() {
            return mPosition;
        }

        @Override
        public String toString() {
            return new StringBuilder()
                    .append("MotionInputEvent {")
                    .append("isMouseEvent=").append(isMouseEvent())
                    .append(" isPrimaryButtonPressed=").append(isPrimaryButtonPressed())
                    .append(" isSecondaryButtonPressed=").append(isSecondaryButtonPressed())
                    .append(" isShiftKeyDown=").append(isShiftKeyDown())
                    .append(" isActionDown=").append(isActionDown())
                    .append(" isActionUp=").append(isActionUp())
                    .append(" getOrigin=").append(getOrigin())
                    .append(" isOverItem=").append(isOverItem())
                    .append(" getItemPosition=").append(getItemPosition())
                    .append("}")
                    .toString();
        }
    }
}
}
+131 −114
Original line number Original line Diff line number Diff line
@@ -16,11 +16,16 @@


package com.android.documentsui;
package com.android.documentsui;


import static com.android.documentsui.Events.isMouseEvent;
import static com.android.documentsui.Shared.DEBUG;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkNotNull;
import static com.android.internal.util.Preconditions.checkState;
import static com.android.internal.util.Preconditions.checkState;


import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.Adapter;
import android.support.v7.widget.RecyclerView.Adapter;
@@ -36,10 +41,9 @@ import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View;
import android.graphics.Point;

import android.graphics.Rect;
import com.android.documentsui.Events.InputEvent;
import android.graphics.drawable.Drawable;
import com.android.documentsui.Events.MotionInputEvent;
import android.support.annotation.VisibleForTesting;


import java.util.ArrayList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collections;
@@ -59,7 +63,6 @@ public final class MultiSelectManager {
    public static final int MODE_SINGLE = 1;
    public static final int MODE_SINGLE = 1;


    private static final String TAG = "MultiSelectManager";
    private static final String TAG = "MultiSelectManager";
    private static final boolean DEBUG = false;


    private final Selection mSelection = new Selection();
    private final Selection mSelection = new Selection();


@@ -72,7 +75,7 @@ public final class MultiSelectManager {
    private Adapter<?> mAdapter;
    private Adapter<?> mAdapter;
    private MultiSelectHelper mHelper;
    private MultiSelectHelper mHelper;
    private boolean mSingleSelect;
    private boolean mSingleSelect;
    private BandSelectManager mBandSelectManager;
    private BandSelectManager mBandManager;


    /**
    /**
     * @param recyclerView
     * @param recyclerView
@@ -89,17 +92,19 @@ public final class MultiSelectManager {
                new RuntimeRecyclerViewHelper(recyclerView),
                new RuntimeRecyclerViewHelper(recyclerView),
                mode);
                mode);


        mBandSelectManager = new BandSelectManager((RuntimeRecyclerViewHelper) mHelper);
        mBandManager = new BandSelectManager((RuntimeRecyclerViewHelper) mHelper);


        GestureDetector.SimpleOnGestureListener listener =
        GestureDetector.SimpleOnGestureListener listener =
                new GestureDetector.SimpleOnGestureListener() {
                new GestureDetector.SimpleOnGestureListener() {
                    @Override
                    @Override
                    public boolean onSingleTapUp(MotionEvent e) {
                    public boolean onSingleTapUp(MotionEvent e) {
                        return MultiSelectManager.this.onSingleTapUp(e);
                        return MultiSelectManager.this.onSingleTapUp(
                                new MotionInputEvent(e, recyclerView));
                    }
                    }
                    @Override
                    @Override
                    public void onLongPress(MotionEvent e) {
                    public void onLongPress(MotionEvent e) {
                        MultiSelectManager.this.onLongPress(e);
                        MultiSelectManager.this.onLongPress(
                                new MotionInputEvent(e, recyclerView));
                    }
                    }
                };
                };


@@ -112,17 +117,39 @@ public final class MultiSelectManager {


        recyclerView.addOnItemTouchListener(
        recyclerView.addOnItemTouchListener(
                new RecyclerView.OnItemTouchListener() {
                new RecyclerView.OnItemTouchListener() {
                    @Override
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
                        detector.onTouchEvent(e);
                        detector.onTouchEvent(e);


                        // Only intercept the event if it was a mouse-based band selection.
                        // b/23793622 notes the fact that we *never* receiver ACTION_DOWN
                        return isMouseEvent(e) && (mBandSelectManager.mIsActive ||
                        // events in onTouchEvent. Where it not for this issue, we'd
                                e.getActionMasked() != MotionEvent.ACTION_UP);
                        // push start handling down into handleInputEvent.
                        if (mBandManager.shouldStart(e)) {
                            // endBandSelect is handled in handleInputEvent.
                            mBandManager.startBandSelect(
                                    new Point((int) e.getX(), (int) e.getY()));
                        } else if (mBandManager.isActive()
                                && Events.isMouseEvent(e)
                                && Events.isActionUp(e)) {
                            // Same issue here w b/23793622. The ACTION_UP event
                            // is only evert dispatched to onTouchEvent when
                            // there is some associated motion. If a user taps
                            // mouse, but doesn't move, then band select gets
                            // started BUT not ended. Causing phantom
                            // bands to appear when the user later clicks to start
                            // band select.
                            mBandManager.handleInputEvent(
                                    new MotionInputEvent(e, recyclerView));
                        }
                        return mBandManager.isActive();
                    }
                    }

                    @Override
                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                    public void onTouchEvent(RecyclerView rv, MotionEvent e) {
                        checkState(isMouseEvent(e));
                        mBandManager.handleInputEvent(
                        mBandSelectManager.processMotionEvent(e);
                                new MotionInputEvent(e, recyclerView));
                    }
                    }
                    @Override
                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
                    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {}
                });
                });
    }
    }
@@ -251,7 +278,7 @@ public final class MultiSelectManager {
    }
    }


    public void handleLayoutChanged() {
    public void handleLayoutChanged() {
        mBandSelectManager.handleLayoutChanged();
        mBandManager.handleLayoutChanged();
    }
    }


    /**
    /**
@@ -275,70 +302,36 @@ public final class MultiSelectManager {
        }
        }
    }
    }


    private void onLongPress(MotionEvent e) {
    @VisibleForTesting
    void onLongPress(InputEvent input) {
        if (DEBUG) Log.d(TAG, "Handling long press event.");
        if (DEBUG) Log.d(TAG, "Handling long press event.");


        int position = mHelper.findEventPosition(e);
        if (!input.isOverItem()) {
        if (position == RecyclerView.NO_POSITION) {
            if (DEBUG) Log.i(TAG, "Cannot handle tap. No adapter position available.");
            if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
        }
        }


        onLongPress(position, e.getMetaState());
        handleAdapterEvent(input);
    }
    }


    /**
     * TODO: Roll this back into {@link #onLongPress(MotionEvent)} once MotionEvent
     * can be mocked.
     *
     * @param position
     * @param metaState as returned from {@link MotionEvent#getMetaState()}.
     * @hide
     */
    @VisibleForTesting
    @VisibleForTesting
    void onLongPress(int position, int metaState) {
    boolean onSingleTapUp(InputEvent input) {
        if (position == RecyclerView.NO_POSITION) {
        if (DEBUG) Log.d(TAG, "Processing tap event.");
            if (DEBUG) Log.i(TAG, "View is null. Cannot handle tap event.");
        }

        handlePositionChanged(position, metaState);
    }

    /**
     * @param e
     * @return true if the event was consumed.
     */
    private boolean onSingleTapUp(MotionEvent e) {
        if (DEBUG) Log.d(TAG, "Handling tap event.");
        return onSingleTapUp(mHelper.findEventPosition(e), e.getMetaState(), e.getToolType(0));
    }

    /**
     * TODO: Roll this into {@link #onSingleTapUp(MotionEvent)} once MotionEvent
     * can be mocked.
     *
     * @param position
     * @param metaState as returned from {@link MotionEvent#getMetaState()}.
     * @param toolType
     * @return true if the event was consumed.
     * @hide
     */
    @VisibleForTesting
    boolean onSingleTapUp(int position, int metaState, int toolType) {
        if (mSelection.isEmpty()) {
        if (mSelection.isEmpty()) {
            // if this is a mouse click on an item, start selection mode.
            // if this is a mouse click on an item, start selection mode.
            if (position != RecyclerView.NO_POSITION && Events.isMouseType(toolType)) {
            // TODO:  && input.isPrimaryButtonPressed(), but it is returning false.
                toggleSelection(position);
            if (input.isOverItem() && input.isMouseEvent()) {
                toggleSelection(input.getItemPosition());
            }
            }
            return false;
            return false;
        }
        }


        if (position == RecyclerView.NO_POSITION) {
        if (!input.isOverItem()) {
            if (DEBUG) Log.d(TAG, "View is null. Canceling selection.");
            if (DEBUG) Log.d(TAG, "Activity has no position. Canceling selection.");
            clearSelection();
            clearSelection();
            return false;
            return false;
        }
        }


        handlePositionChanged(position, metaState);
        handleAdapterEvent(input);
        return true;
        return true;
    }
    }


@@ -347,15 +340,15 @@ public final class MultiSelectManager {
     * held down, this performs a range select; otherwise, it simply toggles the item's selection
     * held down, this performs a range select; otherwise, it simply toggles the item's selection
     * state.
     * state.
     */
     */
    private void handlePositionChanged(int position, int metaState) {
    private void handleAdapterEvent(InputEvent input) {
        if (Events.hasShiftBit(metaState) && mRanger != null) {
        if (mRanger != null && input.isShiftKeyDown()) {
            mRanger.snapSelection(position);
            mRanger.snapSelection(input.getItemPosition());


            // We're being lazy here notifying even when something might not have changed.
            // We're being lazy here notifying even when something might not have changed.
            // To make this more correct, we'd need to update the Ranger class to return
            // To make this more correct, we'd need to update the Ranger class to return
            // information about what has changed.
            // information about what has changed.
            notifySelectionChanged();
            notifySelectionChanged();
        } else if (toggleSelection(position)) {
        } else if (toggleSelection(input.getItemPosition())) {
            notifySelectionChanged();
            notifySelectionChanged();
        }
        }
    }
    }
@@ -1175,12 +1168,12 @@ public final class MultiSelectManager {
        private static final int NOT_SET = -1;
        private static final int NOT_SET = -1;


        private final BandManagerHelper mHelper;
        private final BandManagerHelper mHelper;
        private final Runnable mModelBuilder;


        private boolean mIsActive;
        @Nullable private Rect mBounds;
        private Point mOrigin;
        @Nullable private Point mCurrentPosition;
        private Point mPointer;
        @Nullable private Point mOrigin;
        private Rect mBounds;
        @Nullable private BandSelectModel mModel;
        private BandSelectModel mModel;


        // The time at which the current band selection-induced scroll began. If no scroll is in
        // The time at which the current band selection-induced scroll began. If no scroll is in
        // progress, the value is NOT_SET.
        // progress, the value is NOT_SET.
@@ -1188,11 +1181,21 @@ public final class MultiSelectManager {
        private final Runnable mViewScroller = new ViewScroller();
        private final Runnable mViewScroller = new ViewScroller();


        public <T extends BandManagerHelper & BandModelHelper>
        public <T extends BandManagerHelper & BandModelHelper>
                BandSelectManager(T helper) {
                BandSelectManager(final T helper) {
            mHelper = helper;
            mHelper = helper;
            mHelper.addOnScrollListener(this);
            mHelper.addOnScrollListener(this);

            mModelBuilder = new Runnable() {
                @Override
                public void run() {
                    mModel = new BandSelectModel(helper);
                    mModel = new BandSelectModel(helper);
            mModel.addOnSelectionChangedListener(this);
                    mModel.addOnSelectionChangedListener(BandSelectManager.this);
                }
            };
        }

        private boolean isActive() {
            return mModel != null;
        }
        }


        /**
        /**
@@ -1200,39 +1203,48 @@ public final class MultiSelectManager {
         * a new model which will track the new layout.
         * a new model which will track the new layout.
         */
         */
        public void handleLayoutChanged() {
        public void handleLayoutChanged() {
            if (mModel != null) {
                mModel.removeOnSelectionChangedListener(this);
                mModel.removeOnSelectionChangedListener(this);
                mModel.stopListening();
                mModel.stopListening();


            mModel = new BandSelectModel((RuntimeRecyclerViewHelper) mHelper);
                // build a new model, all fresh and happy.
            mModel.addOnSelectionChangedListener(this);
                mModelBuilder.run();
            }
        }

        boolean shouldStart(MotionEvent e) {
            return !isActive()
                    && Events.isMouseEvent(e)  // a mouse
                    && Events.isActionDown(e)  // the initial button press
                    && mHelper.findEventPosition(e) == RecyclerView.NO_ID;  // in empty space
        }

        boolean shouldStop(InputEvent input) {
            return isActive()
                    && input.isMouseEvent()
                    && input.isActionUp();
        }
        }


        /**
        /**
         * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
         * Processes a MotionEvent by starting, ending, or resizing the band select overlay.
         * @param e
         * @param input
         */
         */
        private void processMotionEvent(MotionEvent e) {
        private void handleInputEvent(InputEvent input) {
            if (!isMouseEvent(e)) {
            checkArgument(input.isMouseEvent());
                return;
            }


            if (mIsActive && e.getActionMasked() == MotionEvent.ACTION_UP) {
            if (shouldStop(input)) {
                endBandSelect();
                mBandManager.endBandSelect();
                return;
                return;
            }
            }


            mPointer = new Point((int) e.getX(), (int) e.getY());
            // We shouldn't get any events in this method when band select is not active,
            if (!mIsActive) {
            // but it turns some guests show up late to the party.
                // Only start a band select if the pointer is in margins between items, not
            if (!isActive()) {
                // actually within an item's bounds.
                if (mHelper.findEventPosition(e) != RecyclerView.NO_POSITION) {
                return;
                return;
            }
            }
                startBandSelect();
            } else {
                mModel.resizeSelection(mPointer);
            }


            mCurrentPosition = input.getOrigin();
            mModel.resizeSelection(input.getOrigin());
            scrollViewIfNecessary();
            scrollViewIfNecessary();
            resizeBandSelectRectangle();
            resizeBandSelectRectangle();
        }
        }
@@ -1240,12 +1252,11 @@ public final class MultiSelectManager {
        /**
        /**
         * Starts band select by adding the drawable to the RecyclerView's overlay.
         * Starts band select by adding the drawable to the RecyclerView's overlay.
         */
         */
        private void startBandSelect() {
        private void startBandSelect(Point origin) {
            if (DEBUG) {
            if (DEBUG) Log.d(TAG, "Starting band select @ " + origin);
                Log.d(TAG, "Starting band select from (" + mPointer.x + "," + mPointer.y + ").");

            }
            mOrigin = origin;
            mIsActive = true;
            mModelBuilder.run();  // Creates a new selection model.
            mOrigin = new Point(mPointer.x, mPointer.y);
            mModel.startSelection(mOrigin);
            mModel.startSelection(mOrigin);
        }
        }


@@ -1263,10 +1274,10 @@ public final class MultiSelectManager {
         * two opposite corners of the selection.
         * two opposite corners of the selection.
         */
         */
        private void resizeBandSelectRectangle() {
        private void resizeBandSelectRectangle() {
            mBounds = new Rect(Math.min(mOrigin.x, mPointer.x),
            mBounds = new Rect(Math.min(mOrigin.x, mCurrentPosition.x),
                    Math.min(mOrigin.y, mPointer.y),
                    Math.min(mOrigin.y, mCurrentPosition.y),
                    Math.max(mOrigin.x, mPointer.x),
                    Math.max(mOrigin.x, mCurrentPosition.x),
                    Math.max(mOrigin.y, mPointer.y));
                    Math.max(mOrigin.y, mCurrentPosition.y));
            mHelper.drawBand(mBounds);
            mHelper.drawBand(mBounds);
        }
        }


@@ -1275,14 +1286,20 @@ public final class MultiSelectManager {
         */
         */
        private void endBandSelect() {
        private void endBandSelect() {
            if (DEBUG) Log.d(TAG, "Ending band select.");
            if (DEBUG) Log.d(TAG, "Ending band select.");
            mIsActive = false;

            mHelper.hideBand();
            mHelper.hideBand();
            mSelection.applyProvisionalSelection();
            mSelection.applyProvisionalSelection();
            mModel.endSelection();
            mModel.endSelection();
            int firstSelected = mModel.getPositionNearestOrigin();
            int firstSelected = mModel.getPositionNearestOrigin();
            if (firstSelected != BandSelectModel.NOT_SET) {
            if (!mSelection.contains(firstSelected)) {
                Log.w(TAG, "First selected by band is NOT in selection!");
                // Sadly this is really happening. Need to figure out what's going on.
            } else if (firstSelected != BandSelectModel.NOT_SET) {
                setSelectionFocusBegin(firstSelected);
                setSelectionFocusBegin(firstSelected);
            }
            }

            mModel = null;
            mOrigin = null;
        }
        }


        @Override
        @Override
@@ -1311,13 +1328,13 @@ public final class MultiSelectManager {
                // that one additional pixel is added here so that the view still scrolls when the
                // that one additional pixel is added here so that the view still scrolls when the
                // pointer is exactly at the top or bottom.
                // pointer is exactly at the top or bottom.
                int pixelsPastView = 0;
                int pixelsPastView = 0;
                if (mPointer.y <= 0) {
                if (mCurrentPosition.y <= 0) {
                    pixelsPastView = mPointer.y - 1;
                    pixelsPastView = mCurrentPosition.y - 1;
                } else if (mPointer.y >= mHelper.getHeight() - 1) {
                } else if (mCurrentPosition.y >= mHelper.getHeight() - 1) {
                    pixelsPastView = mPointer.y - mHelper.getHeight() + 1;
                    pixelsPastView = mCurrentPosition.y - mHelper.getHeight() + 1;
                }
                }


                if (!mIsActive || pixelsPastView == 0) {
                if (!isActive() || pixelsPastView == 0) {
                    // If band selection is inactive, or if it is active but not at the edge of the
                    // If band selection is inactive, or if it is active but not at the edge of the
                    // view, no scrolling is necessary.
                    // view, no scrolling is necessary.
                    mScrollStartTime = NOT_SET;
                    mScrollStartTime = NOT_SET;
@@ -1403,7 +1420,7 @@ public final class MultiSelectManager {


        @Override
        @Override
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
        public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
            if (!mIsActive) {
            if (!isActive()) {
                return;
                return;
            }
            }


+1 −0
Original line number Original line Diff line number Diff line
@@ -22,6 +22,7 @@ import android.content.Context;
 * @hide
 * @hide
 */
 */
public final class Shared {
public final class Shared {
    public static final boolean DEBUG = false;
    public static final String TAG = "Documents";
    public static final String TAG = "Documents";


    /**
    /**
+27 −24

File changed.

Preview size limit exceeded, changes collapsed.

+90 −0
Original line number Original line Diff line number Diff line
package com.android.documentsui;

import android.graphics.Point;
import android.support.v7.widget.RecyclerView;

class TestInputEvent implements Events.InputEvent {

    public boolean mouseEvent;
    public boolean primaryButtonPressed;
    public boolean secondaryButtonPressed;
    public boolean shiftKeyDow;
    public boolean actionDown;
    public boolean actionUp;
    public Point location;
    public int position = Integer.MIN_VALUE;

    public TestInputEvent() {}

    public TestInputEvent(int position) {
        this.position = position;
    }

    @Override
    public boolean isMouseEvent() {
        return mouseEvent;
    }

    @Override
    public boolean isPrimaryButtonPressed() {
        return primaryButtonPressed;
    }

    @Override
    public boolean isSecondaryButtonPressed() {
        return secondaryButtonPressed;
    }

    @Override
    public boolean isShiftKeyDown() {
        return shiftKeyDow;
    }

    @Override
    public boolean isActionDown() {
        return actionDown;
    }

    @Override
    public boolean isActionUp() {
        return actionUp;
    }

    @Override
    public Point getOrigin() {
        return location;
    }

    @Override
    public boolean isOverItem() {
        return position != Integer.MIN_VALUE && position != RecyclerView.NO_POSITION;
    }

    @Override
    public int getItemPosition() {
        return position;
    }

    public static TestInputEvent tap(int position) {
        return new TestInputEvent(position);
    }

    public static TestInputEvent shiftTap(int position) {
        TestInputEvent e = new TestInputEvent(position);
        e.shiftKeyDow = true;
        return e;
    }

    public static TestInputEvent click(int position) {
        TestInputEvent e = new TestInputEvent(position);
        e.mouseEvent = true;
        return e;
    }

    public static TestInputEvent shiftClick(int position) {
        TestInputEvent e = new TestInputEvent(position);
        e.mouseEvent = true;
        e.shiftKeyDow = true;
        return e;
    }
}