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

Commit cf62a12c authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Replace InputEvent with direct use of MotionEvent."

parents 8f9741c6 57c559c8
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