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

Commit 57c559c8 authored by Steve McKay's avatar Steve McKay
Browse files

Replace InputEvent with direct use of MotionEvent.

Bug: 64847011
Test: Passing.
Change-Id: Ib5e8044b8cbe49a04acaf67f17d689d813fa3bf8
parent 40c1131f
Loading
Loading
Loading
Loading
+62 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.documentsui.base;

import android.view.MotionEvent;

import com.android.documentsui.dirlist.DocumentDetails;

import javax.annotation.Nullable;

/**
 * Provides event handlers w/ access to details about documents details
 * view items Documents in the UI (RecyclerView).
 */
public interface EventDetailsLookup {

    /** @return true if there is an item under the finger/cursor. */
    boolean overItem(MotionEvent e);

    /**
     * @return true if there is a model backed item under the finger/cursor.
     * Resulting calls on the event instance should never return a null
     * DocumentDetails and DocumentDetails#hasModelId should always return true
     */
    boolean overModelItem(MotionEvent e);

    /**
     * @return true if the event is over an area that can be dragged via touch
     * or via mouse. List items have a white area that is not draggable.
     */
    boolean inItemDragRegion(MotionEvent e);

    /**
     * @return true if the event is in the "selection hot spot" region.
     * The hot spot region instantly selects in touch mode, vs launches.
     */
    boolean inItemSelectRegion(MotionEvent e);

    /**
     * @return the adapter position of the item under the finger/cursor.
     */
    int getItemPosition(MotionEvent e);


    /**
     * @return the DocumentDetails for the item under the event, or null.
     */
    @Nullable DocumentDetails getDocumentDetails(MotionEvent e);
}
+59 −354
Original line number Diff line number Diff line
@@ -16,403 +16,108 @@

package com.android.documentsui.base;

import static com.android.documentsui.base.Shared.DEBUG;

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

import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.DocumentHolder;

import javax.annotation.Nullable;

/**
 * Utility code for dealing with MotionEvents.
 */
public final class Events {

    /**
     * Returns true if event was triggered by a mouse.
     */
    public static boolean isMouseEvent(MotionEvent e) {
        int toolType = e.getToolType(0);
        return toolType == MotionEvent.TOOL_TYPE_MOUSE;
        return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
    }

    public static boolean isActionMove(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_MOVE;
    }

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

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

    /**
     * Returns true if the shift is pressed.
     */
    public boolean isShiftPressed(MotionEvent e) {
        return hasShiftBit(e.getMetaState());
    }

    /**
     * Returns true if the event is a mouse drag event.
     * @param e
     * @return
     */
    public static boolean isMouseDragEvent(InputEvent e) {
        return e.isMouseEvent()
                && e.isActionMove()
                && e.isPrimaryButtonPressed()
                && e.isOverDragHotspot();
    }

    /**
     * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home).
     *
     * @param keyCode
     * @return
     */
    public static boolean isNavigationKeyCode(int keyCode) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_LEFT:
            case KeyEvent.KEYCODE_DPAD_RIGHT:
            case KeyEvent.KEYCODE_MOVE_HOME:
            case KeyEvent.KEYCODE_MOVE_END:
            case KeyEvent.KEYCODE_PAGE_UP:
            case KeyEvent.KEYCODE_PAGE_DOWN:
                return true;
            default:
                return false;
        }
    }


    /**
     * Returns true if the "SHIFT" bit is set.
     */
    public static boolean hasShiftBit(int metaState) {
        return (metaState & KeyEvent.META_SHIFT_ON) != 0;
    }

    public static boolean hasCtrlBit(int metaState) {
        return (metaState & KeyEvent.META_CTRL_ON) != 0;
    }

    public static boolean hasAltBit(int metaState) {
        return (metaState & KeyEvent.META_ALT_ON) != 0;
    }

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

        /** 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();

        /**
         * Returns true when the action is the initial press of a non-primary (ex. second finger)
         * pointer.
         * See {@link MotionEvent#ACTION_POINTER_DOWN}.
         */
        boolean isMultiPointerActionDown();

        /**
         * Returns true when the action is the final of a non-primary (ex. second finger)
         * pointer.
         * * See {@link MotionEvent#ACTION_POINTER_UP}.
         */
        boolean isMultiPointerActionUp();

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

        /** Returns true if the action is cancel. */
        boolean isActionCancel();

        // Eliminate the checked Exception from Autoclosable.
        @Override
        public void close();

        Point getOrigin();
        float getX();
        float getY();
        float getRawX();
        float getRawY();
        int getPointerCount();

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

        /**
         * Returns true if there is a model backed item under the finger/cursor.
         * Resulting calls on the event instance should never return a null
         * DocumentDetails and DocumentDetails#hasModelId should always return true
         */
        boolean isOverModelItem();

        /**
         * Returns true if the event is over an area that can be dragged via touch.
         * List items have a white area that is not draggable.
         */
        boolean isOverDragHotspot();

        /**
         * Returns true if the event is a two/three-finger scroll on touchpad.
         */
        boolean isTouchpadScroll();

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

        boolean isOverDocIcon();

        /** Returns the DocumentDetails for the item under the event, or null. */
        @Nullable DocumentDetails getDocumentDetails();
    }

    public static final class MotionInputEvent implements InputEvent {
        private static final String TAG = "MotionInputEvent";

        private static final int UNSET_POSITION = RecyclerView.NO_POSITION - 1;

        private static final Pools.SimplePool<MotionInputEvent> sPool = new Pools.SimplePool<>(1);

        private MotionEvent mEvent;
        private @Nullable RecyclerView mRecView;

        private int mPosition = UNSET_POSITION;
        private @Nullable DocumentDetails mDocDetails;

        private MotionInputEvent() {
            if (DEBUG) Log.i(TAG, "Created a new instance.");
        }

        public static MotionInputEvent obtain(MotionEvent event, RecyclerView view) {
            Shared.checkMainLoop();

            MotionInputEvent instance = sPool.acquire();
            instance = (instance != null ? instance : new MotionInputEvent());

            instance.mEvent = event;
            instance.mRecView = view;

            return instance;
        }

        public void recycle() {
            Shared.checkMainLoop();

            mEvent = null;
            mRecView = null;
            mPosition = UNSET_POSITION;
            mDocDetails = null;

            boolean released = sPool.release(this);
            // This assert is used to guarantee we won't generate too many instances that can't be
            // held in the pool, which indicates our pool size is too small.
            //
            // Right now one instance is enough because we expect all instances are only used in
            // main thread.
            assert(released);
        }

        @Override
        public void close() {
            recycle();
        }

        @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 isTertiaryButtonPressed() {
            return mEvent.isButtonPressed(MotionEvent.BUTTON_TERTIARY);
        }

        @Override
        public boolean isAltKeyDown() {
            return Events.hasAltBit(mEvent.getMetaState());
        }

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

        @Override
        public boolean isCtrlKeyDown() {
            return Events.hasCtrlBit(mEvent.getMetaState());
        }

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

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

        @Override
        public boolean isMultiPointerActionDown() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
    public static boolean isMultiPointerActionDown(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
    }

        @Override
        public boolean isMultiPointerActionUp() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
    public static boolean isMultiPointerActionUp(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
    }


        @Override
        public boolean isActionMove() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_MOVE;
        }

        @Override
        public boolean isActionCancel() {
            return mEvent.getActionMasked() == MotionEvent.ACTION_CANCEL;
    public static boolean isActionCancel(MotionEvent e) {
        return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
    }

        @Override
        public Point getOrigin() {
            return new Point((int) mEvent.getX(), (int) mEvent.getY());
    public static boolean isPrimaryButtonPressed(MotionEvent e) {
        return e.isButtonPressed(MotionEvent.BUTTON_PRIMARY);
    }

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

        @Override
        public float getY() {
            return mEvent.getY();
    public static boolean isTertiaryButtonPressed(MotionEvent e) {
        return e.isButtonPressed(MotionEvent.BUTTON_TERTIARY);
    }

        @Override
        public float getRawX() {
            return mEvent.getRawX();
    public static boolean isCtrlKeyPressed(MotionEvent e) {
        return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
    }

        @Override
        public float getRawY() {
            return mEvent.getRawY();
    public static boolean isAltKeyPressed(MotionEvent e) {
        return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
    }

        @Override
        public int getPointerCount() {
            return mEvent.getPointerCount();
    public static boolean isShiftKeyPressed(MotionEvent e) {
        return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
    }

        @Override
        public boolean isTouchpadScroll() {
    public static boolean isTouchpadScroll(MotionEvent e) {
        // Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
        // returned.
            return isMouseEvent() && isActionMove() && mEvent.getButtonState() == 0;
        }

        @Override
        public boolean isOverDragHotspot() {
            return isOverItem() && getDocumentDetails().isInDragHotspot(this);
        return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
    }

        @Override
        public boolean isOverItem() {
            return getItemPosition() != RecyclerView.NO_POSITION;
    private static boolean hasBit(int metaState, int bit) {
        return (metaState & bit) != 0;
    }

        @Override
        public boolean isOverDocIcon() {
            return isOverItem() && getDocumentDetails().isOverDocIcon(this);
    public static Point getOrigin(MotionEvent e) {
        return new Point((int) e.getX(), (int) e.getY());
    }

        @Override
        public boolean isOverModelItem() {
            return isOverItem() && getDocumentDetails().hasModelId();
        }

        @Override
        public int getItemPosition() {
            if (mPosition == UNSET_POSITION) {
                View child = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY());
                mPosition = (child != null)
                        ? mRecView.getChildAdapterPosition(child)
                        : RecyclerView.NO_POSITION;
            }
            return mPosition;
        }

        @Override
        public @Nullable DocumentDetails getDocumentDetails() {
            if (mDocDetails == null) {
                View childView = mRecView.findChildViewUnder(mEvent.getX(), mEvent.getY());
                mDocDetails = (childView != null)
                    ? (DocumentHolder) mRecView.getChildViewHolder(childView)
                    : null;
            }
            if (isOverItem()) {
                assert(mDocDetails != null);
    /**
     * @return true if keyCode is a known navigation code (e.g. up, down, home).
     */
    public static boolean isNavigationKeyCode(int keyCode) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_DPAD_UP:
            case KeyEvent.KEYCODE_DPAD_DOWN:
            case KeyEvent.KEYCODE_DPAD_LEFT:
            case KeyEvent.KEYCODE_DPAD_RIGHT:
            case KeyEvent.KEYCODE_MOVE_HOME:
            case KeyEvent.KEYCODE_MOVE_END:
            case KeyEvent.KEYCODE_PAGE_UP:
            case KeyEvent.KEYCODE_PAGE_DOWN:
                return true;
            default:
                return false;
        }
            return mDocDetails;
    }

        @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(" isAltKeyDown=").append(isAltKeyDown())
                    .append(" action(decoded)=").append(
                            MotionEvent.actionToString(mEvent.getActionMasked()))
                    .append(" getOrigin=").append(getOrigin())
                    .append(" isOverItem=").append(isOverItem())
                    .append(" getItemPosition=").append(getItemPosition())
                    .append(" getDocumentDetails=").append(getDocumentDetails())
                    .append(" getPointerCount=").append(getPointerCount())
                    .append("}")
                    .toString();
        }
    /**
     * Returns true if the event is a mouse drag event.
     * @param e
     * @return
     */
    public static boolean isMouseDragEvent(MotionEvent e) {
        return isMouseEvent(e)
                && isActionMove(e)
                && isPrimaryButtonPressed(e);
    }
}
+38 −12
Original line number Diff line number Diff line
@@ -75,10 +75,9 @@ import com.android.documentsui.ThumbnailCache;
import com.android.documentsui.base.DocumentFilters;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.EventDetailsLookup;
import com.android.documentsui.base.EventHandler;
import com.android.documentsui.base.EventListener;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.base.Events.MotionInputEvent;
import com.android.documentsui.base.Features;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
@@ -93,6 +92,8 @@ import com.android.documentsui.selection.Selection;
import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.selection.SelectionManager.SelectionPredicate;
import com.android.documentsui.selection.addons.BandSelector;
import com.android.documentsui.selection.addons.BandSelector.BandPredicate;
import com.android.documentsui.selection.addons.BandSelector.SelectionHost;
import com.android.documentsui.selection.addons.ContentLock;
import com.android.documentsui.selection.addons.GestureSelector;
import com.android.documentsui.services.FileOperation;
@@ -157,8 +158,9 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
    @ContentScoped
    private ActionModeController mActionModeController;

    private EventDetailsLookup mDetailsLookup;
    private SelectionMetadata mSelectionMetadata;
    private UserInputHandler<InputEvent> mInputHandler;
    private UserInputHandler mInputHandler;
    private @Nullable BandSelector mBandController;
    private @Nullable DragHoverListener mDragHoverListener;
    private IconHelper mIconHelper;
@@ -353,13 +355,28 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                        (View child) -> onAccessibilityClick(child)));
        mSelectionMetadata = new SelectionMetadata(mModel::getItem);
        mSelectionMgr.addEventListener(mSelectionMetadata);
        mDetailsLookup = new RuntimeEventDetailsLookup(mRecView);

        GestureSelector gestureSel =
                GestureSelector.create(mSelectionMgr, mRecView, mContentLock);

        if (mState.allowMultiple) {
            BandPredicate bandPredicate = new BandPredicate() {
                @Override
                public boolean canInitiate(MotionEvent e) {
                    View view = mRecView.findChildViewUnder(e.getX(), e.getY());
                    if (view instanceof DocumentDetails) {
                        return ((DocumentDetails) view).inDragRegion(e);
                    }
                    return true;
                }
            };

            SelectionHost host = BandSelector.createHost(
                    mRecView, R.drawable.band_select_overlay, bandPredicate);
            mBandController = new BandSelector(
                    mRecView,
                    host,
                    mAdapter,
                    mAdapter,  // stableIds provider.
                    mSelectionMgr,
                    selectionPredicate,
@@ -376,20 +393,26 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                        mSelectionMgr,
                        mSelectionMetadata,
                        mState,
                        mDetailsLookup,
                        this::getModelId,
                        mRecView::findChildViewUnder,
                        DocumentsApplication.getDragAndDropManager(mActivity))
                : DragStartListener.DUMMY;

        EventHandler<InputEvent> gestureHandler = mState.allowMultiple
                ? gestureSel::start
        EventHandler<MotionEvent> gestureHandler = mState.allowMultiple
                ? new EventHandler<MotionEvent>() {
                    @Override
                    public boolean accept(MotionEvent event) {
                        return gestureSel.start();
                    }
                }
                : EventHandler.createStub(false);

        mInputHandler = new UserInputHandler<>(
        mInputHandler = new UserInputHandler(
                mActions,
                mFocusManager,
                mSelectionMgr,
                (MotionEvent t) -> MotionInputEvent.obtain(t, mRecView),
                mDetailsLookup,
                this::canSelect,
                this::onContextMenuClick,
                mDragStartListener::onTouchDragEvent,
@@ -397,8 +420,8 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                () -> mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS));

        new ListeningGestureDetector(
                mInjector.features,
                this.getContext(),
                mInjector.features,
                mRecView,
                mDragStartListener::onMouseDragEvent,
                mRefreshLayout::setEnabled,
@@ -509,11 +532,14 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
                jobId);
    }

    protected boolean onContextMenuClick(InputEvent e) {
    // TODO: Move to UserInputHander.
    protected boolean onContextMenuClick(MotionEvent e) {
        final View v;
        final float x, y;
        if (e.isOverModelItem()) {
            DocumentHolder doc = (DocumentHolder) e.getDocumentDetails();
        if (mDetailsLookup.overModelItem(e)) {
            // Oooo. Naughty. This is test hostile code, since it makes assumptions
            // about the document details being a holder.
            DocumentHolder doc = (DocumentHolder) mDetailsLookup.getDocumentDetails(e);

            v = doc.itemView;
            x = e.getX() - v.getLeft();
+3 −10
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@

package com.android.documentsui.dirlist;

import com.android.documentsui.base.Events.InputEvent;
import android.view.MotionEvent;

/**
 * Interface providing a loose coupling between DocumentHolder.
@@ -25,13 +25,6 @@ public interface DocumentDetails {
    boolean hasModelId();
    String getModelId();
    int getAdapterPosition();
    boolean isInSelectionHotspot(InputEvent event);
    boolean isInDragHotspot(InputEvent event);

    /**
     * Given a mouse input event, this method does a hit-test to see if the cursor
     * is currently positioned over the document icon or checkbox in the case where
     * the document is selected.
     */
    boolean isOverDocIcon(InputEvent event);
    boolean inDragRegion(MotionEvent event);
    boolean inSelectRegion(MotionEvent event);
}
+3 −21
Original line number Diff line number Diff line
@@ -16,25 +16,19 @@

package com.android.documentsui.dirlist;

import android.annotation.ColorInt;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.os.Build;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.FrameLayout;
import android.widget.ImageView;

import com.android.documentsui.R;
import com.android.documentsui.base.DebugFlags;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.base.Shared;
import com.android.documentsui.ui.DocumentDebugInfo;

@@ -128,24 +122,12 @@ public abstract class DocumentHolder
    }

    @Override
    public boolean isInSelectionHotspot(InputEvent event) {
        // Do everything in global coordinates - it makes things simpler.
        int[] coords = new int[2];
        mSelectionHotspot.getLocationOnScreen(coords);
        Rect rect = new Rect(coords[0], coords[1], coords[0] + mSelectionHotspot.getWidth(),
                coords[1] + mSelectionHotspot.getHeight());

        // If the tap occurred within the icon rect, consider it a selection.
        return rect.contains((int) event.getRawX(), (int) event.getRawY());
    }

    @Override
    public boolean isInDragHotspot(InputEvent event) {
    public boolean inDragRegion(MotionEvent event) {
        return false;
    }

    @Override
    public boolean isOverDocIcon(InputEvent event) {
    public boolean inSelectRegion(MotionEvent event) {
        return false;
    }

Loading