Loading src/com/android/documentsui/Events.java +17 −0 Original line number Diff line number Diff line Loading @@ -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). * Loading Loading @@ -290,4 +302,9 @@ public final class Events { .toString(); } } @FunctionalInterface public interface EventHandler { boolean apply(InputEvent event); } } src/com/android/documentsui/dirlist/DirectoryFragment.java +18 −28 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) -> { Loading Loading @@ -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, Loading @@ -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); Loading Loading @@ -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()); } Loading src/com/android/documentsui/dirlist/DragShadowBuilder.java +58 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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); } } } src/com/android/documentsui/dirlist/DragStartListener.java +145 −97 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); Loading @@ -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); } } src/com/android/documentsui/dirlist/ListeningGestureDetector.java +6 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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(); Loading @@ -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); Loading Loading @@ -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 Loading
src/com/android/documentsui/Events.java +17 −0 Original line number Diff line number Diff line Loading @@ -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). * Loading Loading @@ -290,4 +302,9 @@ public final class Events { .toString(); } } @FunctionalInterface public interface EventHandler { boolean apply(InputEvent event); } }
src/com/android/documentsui/dirlist/DirectoryFragment.java +18 −28 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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) -> { Loading Loading @@ -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, Loading @@ -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); Loading Loading @@ -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()); } Loading
src/com/android/documentsui/dirlist/DragShadowBuilder.java +58 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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); } } }
src/com/android/documentsui/dirlist/DragStartListener.java +145 −97 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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); Loading @@ -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); } }
src/com/android/documentsui/dirlist/ListeningGestureDetector.java +6 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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(); Loading @@ -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); Loading Loading @@ -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