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

Commit f0fceb4c authored by Steve McKay's avatar Steve McKay
Browse files

Don't try to drag non-model-backed views.

Refactor DragStartListener to allow test confirmation of the fix.
Move DragShadowBuilder related code into DragShadowBuilder.Factory.
Further de-concrete-couple DragStartListener from handlers.
Give event handling names in DragStartListener a wee bit more explicit meaning.

Bug: 31350922
Change-Id: I5b4ff88b65c40d3ccadcda3338e547b2c5c6ac32
parent e6d61e3e
Loading
Loading
Loading
Loading
+17 −0
Original line number Diff line number Diff line
@@ -60,6 +60,18 @@ public final class Events {
        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.isOverItem()
                && e.isMouseEvent()
                && e.isActionMove()
                && e.isPrimaryButtonPressed();
    }

    /**
     * Whether or not the given keyCode represents a navigation keystroke (e.g. up, down, home).
     *
@@ -290,4 +302,9 @@ public final class Events {
                    .toString();
        }
    }

    @FunctionalInterface
    public interface EventHandler {
        boolean apply(InputEvent event);
    }
}
+18 −28
Original line number Diff line number Diff line
@@ -110,7 +110,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

import javax.annotation.Nullable;

@@ -188,7 +187,6 @@ public class DirectoryFragment extends Fragment
    private @Nullable ActionMode mActionMode;

    private DragHoverListener mDragHoverListener;
    private DragStartListener mDragStartListener;
    private MenuManager mMenuManager;

    private SortModel.UpdateListener mSortListener = (model, updateType) -> {
@@ -318,10 +316,26 @@ public class DirectoryFragment extends Fragment
                mSelectionMgr,
                mRecView);

        mTuner = getBaseActivity().createFragmentTuner();
        mMenuManager = getBaseActivity().getMenuManager();

        if (state.allowMultiple) {
            mBandController = new BandController(mRecView, mAdapter, mSelectionMgr);
        }

        DragStartListener mDragStartListener = mTuner.dragAndDropEnabled()
                ? DragStartListener.create(
                        mIconHelper,
                        getContext(),
                        mModel,
                        mSelectionMgr,
                        mClipper,
                        getDisplayState(),
                        this::getModelId,
                        mRecView::findChildViewUnder,
                        getContext().getDrawable(com.android.internal.R.drawable.ic_doc_generic))
                : DragStartListener.DUMMY;

        mInputHandler = new UserInputHandler<>(
                mSelectionMgr,
                mFocusManager,
@@ -331,36 +345,20 @@ public class DirectoryFragment extends Fragment
                this::onRightClick,
                (DocumentDetails doc) -> handleViewItem(doc.getModelId()), // activate handler
                (DocumentDetails ignored) -> onDeleteSelectedDocuments(), // delete handler
                this::onDragAndDrop,
                mDragStartListener::onTouchDragEvent,
                gestureSel::start);

        mDragStartListener = new DragStartListener(
                mIconHelper,
                getContext(),
                mModel,
                mSelectionMgr,
                mClipper,
                getDisplayState(),
                this::getModelId,
                mRecView::findChildViewUnder,
                getContext().getDrawable(com.android.internal.R.drawable.ic_doc_generic));


        new ListeningGestureDetector(
                this.getContext(),
                mRecView,
                mEmptyView,
                mDragStartListener,
                mDragStartListener::onMouseDragEvent,
                gestureSel,
                mInputHandler,
                mBandController);

        mSelectionMgr.addCallback(mSelectionModeListener);

        final BaseActivity activity = getBaseActivity();
        mTuner = activity.createFragmentTuner();
        mMenuManager = activity.getMenuManager();

        final ActivityManager am = (ActivityManager) context.getSystemService(
                Context.ACTIVITY_SERVICE);
        boolean svelte = am.isLowRamDevice() && (mType == TYPE_RECENT_OPEN);
@@ -1486,14 +1484,6 @@ public class DirectoryFragment extends Fragment
        }
    }

    private boolean onDragAndDrop(InputEvent event) {
        if (mTuner.dragAndDropEnabled()) {
            View childView = mRecView.findChildViewUnder(event.getX(), event.getY());
            return mDragStartListener.startDrag(childView);
        }
        return false;
    }

    private boolean canSelect(DocumentDetails doc) {
        return canSelect(doc.getModelId());
    }
+58 −0
Original line number Diff line number Diff line
@@ -27,6 +27,12 @@ import android.widget.ImageView;
import android.widget.TextView;

import com.android.documentsui.R;
import com.android.documentsui.Shared;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;

import java.util.List;
import java.util.function.Function;

final class DragShadowBuilder extends View.DragShadowBuilder {

@@ -65,4 +71,56 @@ final class DragShadowBuilder extends View.DragShadowBuilder {
        mShadowView.layout(r.left, r.top, r.right, r.bottom);
        mShadowView.draw(canvas);
    }

    /**
     * Provides a means of fully isolating the mechanics of building drag shadows (and builders)
     * in support of testing.
     */
    public static final class Factory implements Function<Selection, DragShadowBuilder> {

        private final Context mContext;
        private final IconHelper mIconHelper;
        private final Drawable mDefaultDragIcon;
        private Model mModel;

        public Factory(
                Context context, Model model, IconHelper iconHelper, Drawable defaultDragIcon) {
            mContext = context;
            mModel = model;
            mIconHelper = iconHelper;
            mDefaultDragIcon = defaultDragIcon;
        }

        @Override
        public DragShadowBuilder apply(Selection selection) {
            return new DragShadowBuilder(
                    mContext,
                    getDragTitle(selection),
                    getDragIcon(selection));
        }

        private Drawable getDragIcon(Selection selection) {
            if (selection.size() == 1) {
                DocumentInfo doc = getSingleSelectedDocument(selection);
                return mIconHelper.getDocumentIcon(mContext, doc);
            }
            return mDefaultDragIcon;
        }

        private String getDragTitle(Selection selection) {
            assert (!selection.isEmpty());
            if (selection.size() == 1) {
                DocumentInfo doc = getSingleSelectedDocument(selection);
                return doc.displayName;
            }
            return Shared.getQuantityString(mContext, R.plurals.elements_dragged, selection.size());
        }

        private DocumentInfo getSingleSelectedDocument(Selection selection) {
            assert (selection.size() == 1);
            final List<DocumentInfo> docs = mModel.getDocuments(selection);
            assert (docs.size() == 1);
            return docs.get(0);
        }
    }
}
+145 −97
Original line number Diff line number Diff line
@@ -16,22 +16,24 @@

package com.android.documentsui.dirlist;

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

import android.content.ClipData;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import android.view.View;

import com.android.documentsui.Events;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.R;
import com.android.documentsui.Shared;
import com.android.documentsui.State;
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;

import java.util.List;
import java.util.function.Function;

import javax.annotation.Nullable;
@@ -41,81 +43,107 @@ import javax.annotation.Nullable;
 * direct call to {@code #startDrag(RecyclerView, View)} if explicit start is needed, such as long-
 * pressing on an item via touch. (e.g. {@link UserInputHandler#onLongPress(InputEvent)} via touch.)
 */
class DragStartListener {
interface DragStartListener {

    public static final DragStartListener DUMMY = new DragStartListener() {
        @Override
        public boolean onMouseDragEvent(InputEvent event) {
            return false;
        }
        @Override
        public boolean onTouchDragEvent(InputEvent event) {
            return false;
        }
    };

    boolean onMouseDragEvent(InputEvent event);
    boolean onTouchDragEvent(InputEvent event);

    @VisibleForTesting
    static class ActiveListener implements DragStartListener {

        private static String TAG = "DragStartListener";

    private final IconHelper mIconHelper;
    private final Context mContext;
    private final Model mModel;
    private final MultiSelectManager mSelectionMgr;
        private final State mState;
    private final Drawable mDefaultDragIcon;
    private final DocumentClipper mClipper;
    private final Function<View, String> mIdFinder;
        private final MultiSelectManager mSelectionMgr;
        private final ViewFinder mViewFinder;
        private final Function<View, String> mIdFinder;
        private final ClipDataFactory mClipFactory;
        private final Function<Selection, DragShadowBuilder> mShadowFactory;

    public DragStartListener(
            IconHelper iconHelper,
            Context context,
            Model model,
            MultiSelectManager selectionMgr,
            DocumentClipper clipper,
        // use DragStartListener.create
        @VisibleForTesting
        public ActiveListener(
                State state,
            Function<View, String> idFinder,
                MultiSelectManager selectionMgr,
                ViewFinder viewFinder,
            Drawable defaultDragIcon) {
        mIconHelper = iconHelper;
        mContext = context;
        mModel = model;
        mSelectionMgr = selectionMgr;
        mClipper = clipper;
                Function<View, String> idFinder,
                ClipDataFactory clipFactory,
                Function<Selection, DragShadowBuilder> shadowFactory) {

            mState = state;
        mIdFinder = idFinder;
            mSelectionMgr = selectionMgr;
            mViewFinder = viewFinder;
        mDefaultDragIcon = defaultDragIcon;
            mIdFinder = idFinder;
            mClipFactory = clipFactory;
            mShadowFactory = shadowFactory;
        }

    boolean onInterceptTouchEvent(InputEvent event) {
        if (isDragEvent(event)) {
            View child = mViewFinder.findView(event.getX(), event.getY());
            startDrag(child);
            return true;
        @Override
        public final boolean onMouseDragEvent(InputEvent event) {
            assert(Events.isMouseDragEvent(event));
            return startDrag(mViewFinder.findView(event.getX(), event.getY()));
        }
        return false;

        @Override
        public final boolean onTouchDragEvent(InputEvent event) {
            return startDrag(mViewFinder.findView(event.getX(), event.getY()));
        }

    boolean startDrag(View v) {
        /**
         * May be called externally when drag is initiated from other event handling code.
         */
        private final boolean startDrag(@Nullable View view) {

            if (view == null) {
                if (DEBUG) Log.d(TAG, "Ignoring drag event, null view.");
                return false;
            }

        if (v == null) {
            Log.d(TAG, "Ignoring drag event, null view");
            @Nullable String modelId = mIdFinder.apply(view);
            if (modelId == null) {
                if (DEBUG) Log.d(TAG, "Ignoring drag on view not represented in model.");
                return false;
            }

        final Selection selection = new Selection();
        String modelId = mIdFinder.apply(v);
        if (modelId != null && !mSelectionMgr.getSelection().contains(modelId)) {

            Selection selection = new Selection();

            // User can drag an unselected item. Ideally if CTRL key was pressed
            // we'd extend the selection, if not, the selection would be cleared.
            // Buuuuuut, there's an impedance mismatch between event-handling policies,
            // and drag and drop. So we only initiate drag of a single item when
            // drag starts on an item that is unselected. This behavior
            // would look like a bug, if it were not for the implicitly coupled
            // behavior where we clear the selection in the UI (finish action mode)
            // in DirectoryFragment#onDragStart.
            if (!mSelectionMgr.getSelection().contains(modelId)) {
                selection.add(modelId);
            } else {
                mSelectionMgr.getSelection(selection);
            }

        DocumentInfo currentDir = mState.stack.peek();
        ClipData clipData = mClipper.getClipDataForDocuments(
                mModel::getItemUri,
                selection,
                FileOperationService.OPERATION_COPY);
            // NOTE: Preparation of the ClipData object can require a lot of time
            // and ideally should be done in the background. Unfortunately
            // the current code layout and framework assumptions don't support
            // this. So for now, we could end up doing a bunch of i/o on main thread.
        v.startDragAndDrop(
                clipData,
                new DragShadowBuilder(
                        mContext,
                        getDragTitle(selection),
                        getDragIcon(selection)),
                currentDir,
            startDragAndDrop(
                    view,
                    mClipFactory.create(
                            selection,
                            FileOperationService.OPERATION_COPY),
                    mShadowFactory.apply(selection),
                    mState.stack.peek(),
                    View.DRAG_FLAG_GLOBAL
                            | View.DRAG_FLAG_GLOBAL_URI_READ
                            | View.DRAG_FLAG_GLOBAL_URI_WRITE);
@@ -123,36 +151,56 @@ class DragStartListener {
            return true;
        }

    public boolean isDragEvent(InputEvent e) {
        return e.isOverItem() && e.isMouseEvent() && e.isActionMove() && e.isPrimaryButtonPressed();
    }

    private Drawable getDragIcon(Selection selection) {
        if (selection.size() == 1) {
            DocumentInfo doc = getSingleSelectedDocument(selection);
            return mIconHelper.getDocumentIcon(mContext, doc);
        /**
         * This exists as a testing workaround since {@link View#startDragAndDrop} is final.
         */
        @VisibleForTesting
        void startDragAndDrop(
                View view,
                ClipData data,
                DragShadowBuilder shadowBuilder,
                DocumentInfo currentDirectory,
                int flags) {

            view.startDragAndDrop(data, shadowBuilder, currentDirectory, flags);
        }
        return mDefaultDragIcon;
    }

    private String getDragTitle(Selection selection) {
        assert (!selection.isEmpty());
        if (selection.size() == 1) {
            DocumentInfo doc = getSingleSelectedDocument(selection);
            return doc.displayName;
        }
        return Shared.getQuantityString(mContext, R.plurals.elements_dragged, selection.size());
    }
    public static DragStartListener create(
            IconHelper iconHelper,
            Context context,
            Model model,
            MultiSelectManager selectionMgr,
            DocumentClipper clipper,
            State state,
            Function<View, String> idFinder,
            ViewFinder viewFinder,
            Drawable defaultDragIcon) {

    private DocumentInfo getSingleSelectedDocument(Selection selection) {
        assert (selection.size() == 1);
        final List<DocumentInfo> docs = mModel.getDocuments(selection);
        assert (docs.size() == 1);
        return docs.get(0);
        DragShadowBuilder.Factory shadowFactory =
                new DragShadowBuilder.Factory(context, model, iconHelper, defaultDragIcon);

        return new ActiveListener(
                state,
                selectionMgr,
                viewFinder,
                idFinder,
                (Selection selection, @OpType int operationType) -> {
                    return clipper.getClipDataForDocuments(
                            model::getItemUri,
                            selection,
                            FileOperationService.OPERATION_COPY);
                },
                shadowFactory);
    }

    @FunctionalInterface
    interface ViewFinder {
        @Nullable View findView(float x, float y);
    }

    @FunctionalInterface
    interface ClipDataFactory {
        ClipData create(Selection selection, @OpType int operationType);
    }
}
+6 −5
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ import android.view.View;
import android.view.View.OnTouchListener;

import com.android.documentsui.Events;
import com.android.documentsui.Events.EventHandler;
import com.android.documentsui.Events.InputEvent;
import com.android.documentsui.Events.MotionInputEvent;

@@ -36,7 +37,7 @@ final class ListeningGestureDetector extends GestureDetector
        implements OnItemTouchListener, OnTouchListener {

    private final GestureSelector mGestureSelector;
    private final DragStartListener mDragListener;
    private final EventHandler mMouseDragListener;
    private final BandController mBandController;
    private final MouseDelegate mMouseDelegate = new MouseDelegate();
    private final TouchDelegate mTouchDelegate = new TouchDelegate();
@@ -45,12 +46,12 @@ final class ListeningGestureDetector extends GestureDetector
            Context context,
            RecyclerView recView,
            View emptyView,
            DragStartListener dragListener,
            EventHandler mouseDragListener,
            GestureSelector gestureSelector,
            UserInputHandler<? extends InputEvent> handler,
            @Nullable BandController bandController) {
        super(context, handler);
        mDragListener = dragListener;
        mMouseDragListener = mouseDragListener;
        mGestureSelector = gestureSelector;
        mBandController = bandController;
        recView.addOnItemTouchListener(this);
@@ -95,8 +96,8 @@ final class ListeningGestureDetector extends GestureDetector

    private class MouseDelegate {
        boolean onInterceptTouchEvent(InputEvent e) {
            if (mDragListener.isDragEvent(e)) {
                return mDragListener.onInterceptTouchEvent(e);
            if (Events.isMouseDragEvent(e)) {
                return mMouseDragListener.apply(e);
            } else if (mBandController != null &&
                    (mBandController.shouldStart(e) || mBandController.shouldStop(e))) {
                return mBandController.onInterceptTouchEvent(e);
Loading