Allow user control move/copy during drag and drop.

* Refactor some shared drag and drop logic into one single place.
* Add a workaround for updating badges across windows.
* Add unit tests for DragAndDropManager

Bug: 29581353
Change-Id: I2fcf950194457501e35e1bbc2e00ab68d7962666
diff --git a/src/com/android/documentsui/AbstractDragHost.java b/src/com/android/documentsui/AbstractDragHost.java
new file mode 100644
index 0000000..a0d13a9
--- /dev/null
+++ b/src/com/android/documentsui/AbstractDragHost.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import android.annotation.CallSuper;
+import android.view.View;
+
+import com.android.documentsui.services.FileOperationService;
+
+/**
+ * Provides common functionality for a {@link ItemDragListener.DragHost}.
+ */
+public abstract class AbstractDragHost implements ItemDragListener.DragHost {
+
+    protected DragAndDropManager mDragAndDropManager;
+
+    public AbstractDragHost(DragAndDropManager dragAndDropManager) {
+        mDragAndDropManager = dragAndDropManager;
+    }
+
+    @CallSuper
+    @Override
+    public void onDragExited(View v) {
+        mDragAndDropManager.resetState(v);
+    }
+
+    @CallSuper
+    @Override
+    public void onDragEnded() {
+        mDragAndDropManager.dragEnded();
+    }
+}
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index cd3b57e..ded4621 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -443,11 +443,6 @@
         return mState;
     }
 
-    public DragShadowBuilder getShadowBuilder() {
-        throw new UnsupportedOperationException(
-                "Drag and drop not supported, can't get shadow builder");
-    }
-
     /**
      * Set internal storage visible based on explicit user action.
      */
@@ -578,6 +573,9 @@
         if (event.getAction() == KeyEvent.ACTION_DOWN) {
             mInjector.debugHelper.debugCheck(event.getDownTime(), event.getKeyCode());
         }
+
+        DocumentsApplication.getDragAndDropManager(this).onKeyEvent(event);
+
         return super.dispatchKeyEvent(event);
     }
 
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index ee466d4..e0b2559 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -40,6 +40,7 @@
     private ThumbnailCache mThumbnailCache;
     private ClipStorage mClipStore;
     private DocumentClipper mClipper;
+    private DragAndDropManager mDragAndDropManager;
 
     public static ProvidersCache getProvidersCache(Context context) {
         return ((DocumentsApplication) context.getApplicationContext()).mProviders;
@@ -69,6 +70,10 @@
         return ((DocumentsApplication) context.getApplicationContext()).mClipStore;
     }
 
+    public static DragAndDropManager getDragAndDropManager(Context context) {
+        return ((DocumentsApplication) context.getApplicationContext()).mDragAndDropManager;
+    }
+
     @Override
     public void onCreate() {
         super.onCreate();
@@ -86,6 +91,8 @@
                 getSharedPreferences(ClipStorage.PREF_NAME, 0));
         mClipper = DocumentClipper.create(this, mClipStore);
 
+        mDragAndDropManager = DragAndDropManager.create(this, mClipper);
+
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
diff --git a/src/com/android/documentsui/DragAndDropHelper.java b/src/com/android/documentsui/DragAndDropHelper.java
deleted file mode 100644
index 1b634c3..0000000
--- a/src/com/android/documentsui/DragAndDropHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright (C) 2016 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;
-
-import static com.android.documentsui.base.Shared.DEBUG;
-
-import android.util.Log;
-
-import com.android.documentsui.base.DocumentInfo;
-
-import java.util.List;
-
-/**
- * A helper class for drag and drop operations
- */
-public final class DragAndDropHelper {
-
-    private static final String TAG = "DragAndDropHelper";
-
-    private DragAndDropHelper() {}
-
-    /**
-     * Helper method to see whether an item can be dropped/copied into a particular destination.
-     * Don't copy from the cwd into a provided list of prohibited directories. (ie. into cwd, into a
-     * selected directory). Note: this currently doesn't work for multi-window drag, because
-     * localState isn't carried over from one process to another.
-     */
-    public static boolean canCopyTo(Object dragLocalState, DocumentInfo dst) {
-        if (dragLocalState == null || !(dragLocalState instanceof List<?>)) {
-            if (DEBUG) Log.d(TAG, "Invalid local state object. Will allow copy.");
-            return true;
-        }
-        List<?> src = (List<?>) dragLocalState;
-        if (src.contains(dst)) {
-            if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
-            return false;
-        }
-        return true;
-    }
-}
diff --git a/src/com/android/documentsui/DragAndDropManager.java b/src/com/android/documentsui/DragAndDropManager.java
new file mode 100644
index 0000000..9262ac7
--- /dev/null
+++ b/src/com/android/documentsui/DragAndDropManager.java
@@ -0,0 +1,467 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.ClipData;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.support.annotation.VisibleForTesting;
+import android.view.DragEvent;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.clipping.DocumentClipper;
+import com.android.documentsui.dirlist.IconHelper;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.services.FileOperations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Manager that tracks control key state, calculates the default file operation (move or copy)
+ * when user drops, and updates drag shadow state.
+ */
+public interface DragAndDropManager {
+
+    @IntDef({ STATE_NOT_ALLOWED, STATE_UNKNOWN, STATE_MOVE, STATE_COPY })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface State {}
+    int STATE_UNKNOWN = 0;
+    int STATE_NOT_ALLOWED = 1;
+    int STATE_MOVE = 2;
+    int STATE_COPY = 3;
+
+    /**
+     * Intercepts and handles a {@link KeyEvent}. Used to track the state of Ctrl key state.
+     */
+    void onKeyEvent(KeyEvent event);
+
+    /**
+     * Starts a drag and drop.
+     *
+     * @param v the view which
+     *          {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)} will be
+     *          called.
+     * @param parent {@link DocumentInfo} of the container of srcs
+     * @param srcs documents that are dragged
+     * @param root the root in which documents being dragged are
+     * @param invalidDest destinations that don't accept this drag and drop
+     * @param iconHelper used to load document icons
+     */
+    void startDrag(
+            View v,
+            DocumentInfo parent,
+            List<DocumentInfo> srcs,
+            RootInfo root,
+            List<Uri> invalidDest,
+            IconHelper iconHelper);
+
+    /**
+     * Checks whether the document can be spring opened.
+     * @param root the root in which the document is
+     * @param doc the document to check
+     * @return true if policy allows spring opening it; false otherwise
+     */
+    boolean canSpringOpen(RootInfo root, DocumentInfo doc);
+
+    /**
+     * Updates the state to {@link #STATE_NOT_ALLOWED} without any further checks. This is used when
+     * the UI component that handles the drag event already has enough information to disallow
+     * dropping by itself.
+     *
+     * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
+     */
+    void updateStateToNotAllowed(View v);
+
+    /**
+     * Updates the state according to the destination passed.
+     * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
+     * @param destRoot the root of the destination document.
+     * @param destDoc the destination document. Can be null if this is TBD. Must be a folder.
+     * @return the new state. Can be any state in {@link State}.
+     */
+    @State int updateState(
+            View v, RootInfo destRoot, @Nullable DocumentInfo destDoc);
+
+    /**
+     * Resets state back to {@link #STATE_UNKNOWN}. This is used when user drags items leaving a UI
+     * component.
+     * @param v the view which {@link View#updateDragShadow(View.DragShadowBuilder)} will be called.
+     */
+    void resetState(View v);
+
+    /**
+     * Drops items onto the a root.
+     *
+     * @param clipData the clip data that contains sources information.
+     * @param localState used to determine if this is a multi-window drag and drop.
+     * @param destRoot the target root
+     * @param actions {@link ActionHandler} used to load root document.
+     * @param callback callback called when file operation is rejected or scheduled.
+     * @return true if target accepts this drop; false otherwise
+     */
+    boolean drop(ClipData clipData, Object localState, RootInfo destRoot, ActionHandler actions,
+            FileOperations.Callback callback);
+
+    /**
+     * Drops items onto the target.
+     *
+     * @param clipData the clip data that contains sources information.
+     * @param localState used to determine if this is a multi-window drag and drop.
+     * @param dstStack the document stack pointing to the destination folder.
+     * @param callback callback called when file operation is rejected or scheduled.
+     * @return true if target accepts this drop; false otherwise
+     */
+    boolean drop(ClipData clipData, Object localState, DocumentStack dstStack,
+            FileOperations.Callback callback);
+
+    /**
+     * Called when drag and drop ended.
+     *
+     * This can be called multiple times as multiple {@link View.OnDragListener} might delegate
+     * {@link DragEvent#ACTION_DRAG_ENDED} events to this class so any work inside needs to be
+     * idempotent.
+     */
+    void dragEnded();
+
+    static DragAndDropManager create(Context context, DocumentClipper clipper) {
+        return new RuntimeDragAndDropManager(context, clipper);
+    }
+
+    class RuntimeDragAndDropManager implements DragAndDropManager {
+        private static final String SRC_ROOT_KEY = "dragAndDropMgr:srcRoot";
+
+        private final Context mContext;
+        private final DocumentClipper mClipper;
+        private final DragShadowBuilder mShadowBuilder;
+        private final Drawable mDefaultShadowIcon;
+
+        private @State int mState = STATE_UNKNOWN;
+
+        // Key events info. This is used to derive state when user drags items into a view to derive
+        // type of file operations.
+        private boolean mIsCtrlPressed;
+
+        // Drag events info. These are used to derive state and update drag shadow when user changes
+        // Ctrl key state.
+        private View mView;
+        private List<Uri> mInvalidDest;
+        private ClipData mClipData;
+        private RootInfo mDestRoot;
+        private DocumentInfo mDestDoc;
+
+        private RuntimeDragAndDropManager(Context context, DocumentClipper clipper) {
+            this(
+                    context.getApplicationContext(),
+                    clipper,
+                    new DragShadowBuilder(context),
+                    context.getDrawable(R.drawable.ic_doc_generic));
+        }
+
+        @VisibleForTesting
+        RuntimeDragAndDropManager(Context context, DocumentClipper clipper,
+                DragShadowBuilder builder, Drawable defaultShadowIcon) {
+            mContext = context;
+            mClipper = clipper;
+            mShadowBuilder = builder;
+            mDefaultShadowIcon = defaultShadowIcon;
+        }
+
+        @Override
+        public void onKeyEvent(KeyEvent event) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_CTRL_LEFT:
+                case KeyEvent.KEYCODE_CTRL_RIGHT:
+                    adjustCtrlKeyCount(event);
+            }
+        }
+
+        private void adjustCtrlKeyCount(KeyEvent event) {
+            assert(event.getKeyCode() == KeyEvent.KEYCODE_CTRL_LEFT
+                    || event.getKeyCode() == KeyEvent.KEYCODE_CTRL_RIGHT);
+
+            mIsCtrlPressed = event.isCtrlPressed();
+
+            // There is an ongoing drag and drop if mView is not null.
+            if (mView != null) {
+                // There is no need to update the state if current state is unknown or not allowed.
+                if (mState == STATE_COPY || mState == STATE_MOVE) {
+                    updateState(mView, mDestRoot, mDestDoc);
+                }
+            }
+        }
+
+        @Override
+        public void startDrag(
+                View v,
+                DocumentInfo parent,
+                List<DocumentInfo> srcs,
+                RootInfo root,
+                List<Uri> invalidDest,
+                IconHelper iconHelper) {
+
+            mView = v;
+            mInvalidDest = invalidDest;
+
+            List<Uri> uris = new ArrayList<>(srcs.size());
+            for (DocumentInfo doc : srcs) {
+                uris.add(doc.derivedUri);
+            }
+            mClipData = mClipper.getClipDataForDocuments(
+                            uris, FileOperationService.OPERATION_UNKNOWN, parent);
+            mClipData.getDescription().getExtras()
+                    .putString(SRC_ROOT_KEY, root.getUri().toString());
+
+            updateShadow(srcs, iconHelper);
+
+            startDragAndDrop(
+                    v,
+                    mClipData,
+                    mShadowBuilder,
+                    this, // Used to detect multi-window drag and drop
+                    View.DRAG_FLAG_GLOBAL
+                            | View.DRAG_FLAG_OPAQUE
+                            | View.DRAG_FLAG_GLOBAL_URI_READ
+                            | View.DRAG_FLAG_GLOBAL_URI_WRITE);
+        }
+
+        private void updateShadow(List<DocumentInfo> srcs, IconHelper iconHelper) {
+            final String title;
+            final Drawable icon;
+
+            final int size = srcs.size();
+            if (size == 1) {
+                DocumentInfo doc = srcs.get(0);
+                title = doc.displayName;
+                icon = iconHelper.getDocumentIcon(mContext, doc);
+            } else {
+                title = mContext.getResources()
+                        .getQuantityString(R.plurals.elements_dragged, size, size);
+                icon = mDefaultShadowIcon;
+            }
+
+            mShadowBuilder.updateTitle(title);
+            mShadowBuilder.updateIcon(icon);
+
+            mShadowBuilder.onStateUpdated(STATE_UNKNOWN);
+        }
+
+        /**
+         * A workaround of that
+         * {@link View#startDragAndDrop(ClipData, View.DragShadowBuilder, Object, int)} is final.
+         */
+        @VisibleForTesting
+        void startDragAndDrop(View v, ClipData clipData, DragShadowBuilder builder,
+                Object localState, int flags) {
+            v.startDragAndDrop(clipData, builder, localState, flags);
+        }
+
+        @Override
+        public boolean canSpringOpen(RootInfo root, DocumentInfo doc) {
+            return isValidDestination(root, doc.derivedUri);
+        }
+
+        @Override
+        public void updateStateToNotAllowed(View v) {
+            mView = v;
+            updateState(STATE_NOT_ALLOWED);
+        }
+
+        @Override
+        public @State int updateState(
+                View v, RootInfo destRoot, @Nullable DocumentInfo destDoc) {
+
+            mView = v;
+            mDestRoot = destRoot;
+            mDestDoc = destDoc;
+
+            if (!destRoot.supportsCreate()) {
+                updateState(STATE_NOT_ALLOWED);
+                return STATE_NOT_ALLOWED;
+            }
+
+            if (destDoc == null) {
+                updateState(STATE_UNKNOWN);
+                return STATE_UNKNOWN;
+            }
+
+            assert(destDoc.isDirectory());
+
+            if (!destDoc.isCreateSupported() || mInvalidDest.contains(destDoc.derivedUri)) {
+                updateState(STATE_NOT_ALLOWED);
+                return STATE_NOT_ALLOWED;
+            }
+
+            @State int state;
+            final @OpType int opType = calculateOpType(mClipData, destRoot);
+            switch (opType) {
+                case FileOperationService.OPERATION_COPY:
+                    state = STATE_COPY;
+                    break;
+                case FileOperationService.OPERATION_MOVE:
+                    state = STATE_MOVE;
+                    break;
+                default:
+                    // Should never happen
+                    throw new IllegalStateException("Unknown opType: " + opType);
+            }
+
+            updateState(state);
+            return state;
+        }
+
+        @Override
+        public void resetState(View v) {
+            mView = v;
+
+            updateState(STATE_UNKNOWN);
+        }
+
+        private void updateState(@State int state) {
+            mState = state;
+
+            mShadowBuilder.onStateUpdated(state);
+            updateDragShadow(mView);
+        }
+
+        /**
+         * A workaround of that {@link View#updateDragShadow(View.DragShadowBuilder)} is final.
+         */
+        @VisibleForTesting
+        void updateDragShadow(View v) {
+            v.updateDragShadow(mShadowBuilder);
+        }
+
+        @Override
+        public boolean drop(ClipData clipData, Object localState, RootInfo destRoot,
+                ActionHandler action, FileOperations.Callback callback) {
+
+            final Uri rootDocUri =
+                    DocumentsContract.buildDocumentUri(destRoot.authority, destRoot.documentId);
+            if (!isValidDestination(destRoot, rootDocUri)) {
+                return false;
+            }
+
+            action.getRootDocument(
+                    destRoot,
+                    TimeoutTask.DEFAULT_TIMEOUT,
+                    (DocumentInfo doc) -> {
+                        dropOnRootDocument(clipData, localState, destRoot, doc, callback);
+                    });
+
+            return true;
+        }
+
+        private void dropOnRootDocument(ClipData clipData, Object localState, RootInfo destRoot,
+                @Nullable DocumentInfo destRootDoc, FileOperations.Callback callback) {
+            if (destRootDoc == null) {
+                callback.onOperationResult(
+                        FileOperations.Callback.STATUS_FAILED,
+                        calculateOpType(clipData, destRoot),
+                        0);
+            } else {
+                dropChecked(
+                        clipData, localState, new DocumentStack(destRoot, destRootDoc), callback);
+            }
+        }
+
+        @Override
+        public boolean drop(ClipData clipData, Object localState, DocumentStack dstStack,
+                FileOperations.Callback callback) {
+
+            if (!canCopyTo(dstStack)) {
+                return false;
+            }
+
+            dropChecked(clipData, localState, dstStack, callback);
+            return true;
+        }
+
+        private void dropChecked(ClipData clipData, Object localState, DocumentStack dstStack,
+                FileOperations.Callback callback) {
+
+            // Recognize multi-window drag and drop based on the fact that localState is not
+            // carried between processes. It will stop working when the localsState behavior
+            // is changed. The info about window should be passed in the localState then.
+            // The localState could also be null for copying from Recents in single window
+            // mode, but Recents doesn't offer this functionality (no directories).
+            Metrics.logUserAction(mContext,
+                    localState == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+                            : Metrics.USER_ACTION_DRAG_N_DROP);
+
+            mClipper.copyFromClipData(
+                    dstStack,
+                    clipData,
+                    calculateOpType(clipData, dstStack.getRoot()),
+                    callback);
+        }
+
+        @Override
+        public void dragEnded() {
+            // Multiple drag listeners might delegate drag ended event to this method, so anything
+            // in this method needs to be idempotent. Otherwise we need to designate one listener
+            // that always exists and only let it notify us when drag ended, which will further
+            // complicate code and introduce one more coupling. This is a Android framework
+            // limitation.
+
+            mView = null;
+            mInvalidDest = null;
+            mClipData = null;
+            mDestDoc = null;
+            mDestRoot = null;
+        }
+
+        private @OpType int calculateOpType(ClipData clipData, RootInfo destRoot) {
+            final String srcRootUri = clipData.getDescription().getExtras().getString(SRC_ROOT_KEY);
+            final String destRootUri = destRoot.getUri().toString();
+
+            assert(srcRootUri != null);
+            assert(destRootUri != null);
+
+            if (srcRootUri.equals(destRootUri)) {
+                return mIsCtrlPressed
+                        ? FileOperationService.OPERATION_COPY
+                        : FileOperationService.OPERATION_MOVE;
+            } else {
+                return mIsCtrlPressed
+                        ? FileOperationService.OPERATION_MOVE
+                        : FileOperationService.OPERATION_COPY;
+            }
+        }
+
+        private boolean canCopyTo(DocumentStack dstStack) {
+            final RootInfo root = dstStack.getRoot();
+            final DocumentInfo dst = dstStack.peek();
+            return isValidDestination(root, dst.derivedUri);
+        }
+
+        private boolean isValidDestination(RootInfo root, Uri dstUri) {
+            return root.supportsCreate()  && !mInvalidDest.contains(dstUri);
+        }
+    }
+}
diff --git a/src/com/android/documentsui/DragShadowBuilder.java b/src/com/android/documentsui/DragShadowBuilder.java
index 3ba09d0..10a0106 100644
--- a/src/com/android/documentsui/DragShadowBuilder.java
+++ b/src/com/android/documentsui/DragShadowBuilder.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import com.android.documentsui.DragAndDropManager.State;
+
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -27,15 +29,7 @@
 import android.view.View;
 import android.widget.TextView;
 
-import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.Shared;
-import com.android.documentsui.dirlist.IconHelper;
-import com.android.documentsui.selection.Selection;
-
-import java.util.List;
-import java.util.function.Function;
-
-public final class DragShadowBuilder extends View.DragShadowBuilder {
+class DragShadowBuilder extends View.DragShadowBuilder {
 
     private final View mShadowView;
     private final TextView mTitle;
@@ -46,7 +40,7 @@
     private int mPadding;
     private Paint paint;
 
-    public DragShadowBuilder(Context context) {
+    DragShadowBuilder(Context context) {
         mWidth = context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
         mHeight = context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
         mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_radius);
@@ -93,76 +87,15 @@
         mShadowView.draw(canvas);
     }
 
-    public void updateTitle(String title) {
+    void updateTitle(String title) {
         mTitle.setText(title);
     }
 
-    public void updateIcon(Drawable icon) {
+    void updateIcon(Drawable icon) {
         mIcon.updateIcon(icon);
     }
 
-    public void resetBackground() {
-        mIcon.setDropHovered(false);
-        mIcon.setEnabled(false);
-    }
-
-    public void setAppearDroppable(boolean droppable) {
-        mIcon.setDropHovered(true);
-        mIcon.setDroppable(droppable);
-    }
-
-    /**
-     * Provides a means of fully isolating the mechanics of building drag shadows (and builders)
-     * in support of testing.
-     */
-    public static final class Updater implements Function<Selection, DragShadowBuilder> {
-
-        private final Context mContext;
-        private final IconHelper mIconHelper;
-        private final Drawable mDefaultDragIcon;
-        private final Model mModel;
-        private final DragShadowBuilder mShadowBuilder;
-
-        public Updater(
-                Context context, DragShadowBuilder shadowBuilder, Model model,
-                IconHelper iconHelper, Drawable defaultDragIcon) {
-            mContext = context;
-            mShadowBuilder = shadowBuilder;
-            mModel = model;
-            mIconHelper = iconHelper;
-            mDefaultDragIcon = defaultDragIcon;
-        }
-
-        @Override
-        public DragShadowBuilder apply(Selection selection) {
-            mShadowBuilder.updateTitle(getDragTitle(selection));
-            mShadowBuilder.updateIcon(getDragIcon(selection));
-
-            return mShadowBuilder;
-        }
-
-        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);
-        }
+    void onStateUpdated(@State int state) {
+        mIcon.updateState(state);
     }
 }
diff --git a/src/com/android/documentsui/DrawerController.java b/src/com/android/documentsui/DrawerController.java
index 9292157..d8c679a 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -131,7 +131,7 @@
         }
 
         @Override
-        public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
+        public void setDropTargetHighlight(View v, boolean highlight) {
             assert (v.getId() == R.id.drawer_edge);
 
             @ColorRes int id = highlight ? R.color.item_doc_background_selected :
@@ -140,12 +140,12 @@
         }
 
         @Override
-        public void onDragEntered(View v, Object localState) {
+        public void onDragEntered(View v) {
             // do nothing; let drawer only open for onViewHovered
         }
 
         @Override
-        public void onDragExited(View v, Object localState) {
+        public void onDragExited(View v) {
             // do nothing
         }
 
@@ -157,6 +157,11 @@
         }
 
         @Override
+        public void onDragEnded() {
+            // do nothing
+        }
+
+        @Override
         public void setOpen(boolean open) {
             if (open) {
                 mLayout.openDrawer(mDrawer);
diff --git a/src/com/android/documentsui/DropBadgeView.java b/src/com/android/documentsui/DropBadgeView.java
index 4bf3f74..6f71d01 100644
--- a/src/com/android/documentsui/DropBadgeView.java
+++ b/src/com/android/documentsui/DropBadgeView.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import com.android.documentsui.DragAndDropManager.State;
+
 import android.content.Context;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.LayerDrawable;
@@ -27,11 +29,10 @@
  * Provides a way to encapsulate droppable badge toggling logic into a single class.
  */
 public final class DropBadgeView extends ImageView {
-    private static final int[] STATE_DROPPABLE = {R.attr.state_droppable};
-    private static final int[] STATE_DROP_HOVERED = {R.attr.state_drop_hovered};
+    private static final int[] STATE_REJECT_DROP = { R.attr.state_reject_drop };
+    private static final int[] STATE_COPY = { R.attr.state_copy };
 
-    private boolean mDroppable = false;
-    private boolean mDropHovered = false;
+    private @State int mState;
     private LayerDrawable mBackground;
 
     public DropBadgeView(Context context, AttributeSet attrs) {
@@ -60,30 +61,27 @@
 
     @Override
     public int[] onCreateDrawableState(int extraSpace) {
-        final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
+        // STATE_REJECT_DROP and STATE_COPY can't exist at the same time.
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
 
-        if (mDroppable) {
-            mergeDrawableStates(drawableState, STATE_DROPPABLE);
-        }
-
-        if (mDropHovered) {
-            mergeDrawableStates(drawableState, STATE_DROP_HOVERED);
+        switch (mState) {
+            case DragAndDropManager.STATE_NOT_ALLOWED:
+                mergeDrawableStates(drawableState, STATE_REJECT_DROP);
+                break;
+            case DragAndDropManager.STATE_COPY:
+                mergeDrawableStates(drawableState, STATE_COPY);
+                break;
         }
 
         return drawableState;
     }
 
-    public void setDroppable(boolean droppable) {
-        mDroppable = droppable;
+    void updateState(@State int state) {
+        mState = state;
         refreshDrawableState();
     }
 
-    public void setDropHovered(boolean hovered) {
-        mDropHovered = hovered;
-        refreshDrawableState();
-    }
-
-    public void updateIcon(Drawable icon) {
+    void updateIcon(Drawable icon) {
         mBackground.setDrawable(0, icon);
     }
 }
\ No newline at end of file
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index d6410c8..2a3b82d 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -146,7 +146,7 @@
     }
 
     @Override
-    public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
+    public void setDropTargetHighlight(View v, boolean highlight) {
         RecyclerView.ViewHolder vh = getChildViewHolder(v);
         if (vh instanceof BreadcrumbHolder) {
             ((BreadcrumbHolder) vh).setHighlighted(highlight);
@@ -154,12 +154,12 @@
     }
 
     @Override
-    public void onDragEntered(View v, Object localState) {
+    public void onDragEntered(View v) {
         // do nothing
     }
 
     @Override
-    public void onDragExited(View v, Object localState) {
+    public void onDragExited(View v) {
         // do nothing
     }
 
@@ -171,6 +171,11 @@
         }
     }
 
+    @Override
+    public void onDragEnded() {
+        // do nothing
+    }
+
     private void onSingleTapUp(MotionEvent e) {
         View itemView = findChildViewUnder(e.getX(), e.getY());
         int pos = getChildAdapterPosition(itemView);
diff --git a/src/com/android/documentsui/Injector.java b/src/com/android/documentsui/Injector.java
index 387a9ab..aa3d43c 100644
--- a/src/com/android/documentsui/Injector.java
+++ b/src/com/android/documentsui/Injector.java
@@ -53,6 +53,8 @@
     public DialogController dialogs;
     public SearchViewManager searchManager;
 
+    public final DebugHelper debugHelper;
+
     @ContentScoped
     public ActionModeController actionModeController;
 
@@ -67,8 +69,6 @@
 
     private final Model mModel;
 
-    public final DebugHelper debugHelper;
-
     // must be initialized before calling super.onCreate because prefs
     // are used in State initialization.
     public Injector(
diff --git a/src/com/android/documentsui/ItemDragListener.java b/src/com/android/documentsui/ItemDragListener.java
index 783f6d4..cee5e91 100644
--- a/src/com/android/documentsui/ItemDragListener.java
+++ b/src/com/android/documentsui/ItemDragListener.java
@@ -73,9 +73,11 @@
                 handleLocationEvent(v, event.getX(), event.getY());
                 return true;
             case DragEvent.ACTION_DRAG_EXITED:
-                mDragHost.onDragExited(v, event.getLocalState());
-                // fall through
+                mDragHost.onDragExited(v);
+                handleExitedEndedEvent(v, event);
+                return true;
             case DragEvent.ACTION_DRAG_ENDED:
+                mDragHost.onDragEnded();
                 handleExitedEndedEvent(v, event);
                 return true;
             case DragEvent.ACTION_DROP:
@@ -86,9 +88,9 @@
     }
 
     private void handleEnteredEvent(View v, DragEvent event) {
-        mDragHost.onDragEntered(v, event.getLocalState());
+        mDragHost.onDragEntered(v);
         @Nullable TimerTask task = createOpenTask(v, event);
-        mDragHost.setDropTargetHighlight(v, event.getLocalState(), true);
+        mDragHost.setDropTargetHighlight(v, true);
         if (task == null) {
             return;
         }
@@ -104,7 +106,7 @@
     }
 
     private void handleExitedEndedEvent(View v, DragEvent event) {
-        mDragHost.setDropTargetHighlight(v, event.getLocalState(), false);
+        mDragHost.setDropTargetHighlight(v, false);
         TimerTask task = (TimerTask) v.getTag(R.id.drag_hovering_tag);
         if (task != null) {
             task.cancel();
@@ -160,11 +162,14 @@
 
         /**
          * Highlights/unhighlights the view to visually indicate this view is being hovered.
+         *
+         * Called after {@link #onDragEntered(View)}, {@link #onDragExited(View)}
+         * or {@link #onDragEnded()}.
+         *
          * @param v the view being hovered
-         * @param localState the Local state object  given by DragEvent
          * @param highlight true if highlight the view; false if unhighlight it
          */
-        void setDropTargetHighlight(View v, Object localState, boolean highlight);
+        void setDropTargetHighlight(View v, boolean highlight);
 
         /**
          * Notifies hovering timeout has elapsed
@@ -175,15 +180,18 @@
         /**
          * Notifies right away when drag shadow enters the view
          * @param v the view which drop shadow just entered
-         * @param localState the Local state object given by DragEvent
          */
-        void onDragEntered(View v, Object localState);
+        void onDragEntered(View v);
 
         /**
          * Notifies right away when drag shadow exits the view
          * @param v the view which drop shadow just exited
-         * @param localState the Local state object given by DragEvent
          */
-        void onDragExited(View v, Object localState);
+        void onDragExited(View v);
+
+        /**
+         * Notifies when the drag and drop has ended.
+         */
+        void onDragEnded();
     }
 }
diff --git a/src/com/android/documentsui/base/Shared.java b/src/com/android/documentsui/base/Shared.java
index 0e24eda..de42ab4 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -34,6 +34,7 @@
 import android.view.WindowManager;
 
 import com.android.documentsui.R;
+import com.android.documentsui.ui.MessageBuilder;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -119,7 +120,7 @@
     }
 
     /**
-     * @deprecated use {@ link MessageBuilder#getQuantityString}
+     * @deprecated use {@link MessageBuilder#getQuantityString}
      */
     @Deprecated
     public static final String getQuantityString(Context context, @PluralsRes int resourceId, int quantity) {
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index e462dde..3777d7a 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -29,6 +29,7 @@
 import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
 
+import java.util.List;
 import java.util.function.Function;
 
 public interface DocumentClipper {
@@ -41,14 +42,18 @@
     }
 
     boolean hasItemsToPaste();
-    @OpType int getOpType(ClipData data);
 
     /**
      * Returns {@link ClipData} representing the selection, or null if selection is empty,
      * or cannot be converted.
      */
-    ClipData getClipDataForDocuments(
-        Function<String, Uri> uriBuilder, Selection selection, @OpType int opType);
+    ClipData getClipDataForDocuments(Function<String, Uri> uriBuilder, Selection selection,
+            @OpType int opType);
+
+    /**
+     * Returns {@link ClipData} representing the list of {@link Uri}, or null if the list is empty.
+     */
+    ClipData getClipDataForDocuments(List<Uri> uris, @OpType int opType, DocumentInfo parent);
 
     /**
      * Puts {@code ClipData} in a primary clipboard, describing a copy operation
@@ -68,7 +73,7 @@
      * @param destination destination document.
      * @param docStack the document stack to the destination folder (not including the destination
      *                 folder)
-     * @param callback callback to notify when operation finishes.
+     * @param callback callback to notify when operation is scheduled or rejected.
      */
     void copyFromClipboard(
             DocumentInfo destination,
@@ -80,39 +85,41 @@
      * returned from {@link ClipboardManager#getPrimaryClip()}.
      *
      * @param docStack the document stack to the destination folder,
-     * @param callback callback to notify when operation finishes.
+     * @param callback callback to notify when operation is scheduled or rejected.
      */
     void copyFromClipboard(
             DocumentStack docStack,
             FileOperations.Callback callback);
 
     /**
-     * Copied documents from given clip data to a root directory.
-     * @param root the root which root directory to copy to
-     * @param destination the root directory
-     * @param clipData the clipData to copy from
-     * @param callback callback to notify when operation finishes
-     */
-    void copyFromClipData(
-            final RootInfo root,
-            final DocumentInfo destination,
-            final ClipData clipData,
-            final FileOperations.Callback callback);
-
-    /**
      * Copies documents from given clip data to a folder.
      *
      * @param destination destination folder
      * @param docStack the document stack to the destination folder (not including the destination
      *                 folder)
      * @param clipData the clipData to copy from
-     * @param callback callback to notify when operation finishes
+     * @param callback callback to notify when operation is scheduled or rejected.
      */
     void copyFromClipData(
-            final DocumentInfo destination,
-            final DocumentStack docStack,
-            final ClipData clipData,
-            final FileOperations.Callback callback);
+            DocumentInfo destination,
+            DocumentStack docStack,
+            ClipData clipData,
+            FileOperations.Callback callback);
+
+    /**
+     * Copies documents from given clip data to a folder, ignoring the op type in clip data.
+     *
+     * @param dstStack the document stack to the destination folder, including the destination
+     *                 folder.
+     * @param clipData the clipData to copy from
+     * @param opType the operation type
+     * @param callback callback to notify when operation is scheduled or rejected.
+     */
+    void copyFromClipData(
+            DocumentStack dstStack,
+            ClipData clipData,
+            @OpType int opType,
+            FileOperations.Callback callback);
 
     /**
      * Copies documents from given clip data to a folder.
@@ -120,10 +127,10 @@
      * @param dstStack the document stack to the destination folder, including the destination
      *            folder.
      * @param clipData the clipData to copy from
-     * @param callback callback to notify when operation finishes
+     * @param callback callback to notify when operation is scheduled or rejected.
      */
     void copyFromClipData(
-            final DocumentStack dstStack,
-            final ClipData clipData,
-            final FileOperations.Callback callback);
+            DocumentStack dstStack,
+            ClipData clipData,
+            FileOperations.Callback callback);
 }
diff --git a/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java b/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
index 8b837d3..012d3fb 100644
--- a/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
+++ b/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
@@ -52,8 +52,8 @@
 final class RuntimeDocumentClipper implements DocumentClipper {
 
     private static final String TAG = "DocumentClipper";
-    private static final String SRC_PARENT_KEY = "srcParent";
-    private static final String OP_TYPE_KEY = "opType";
+    private static final String SRC_PARENT_KEY = "clipper:srcParent";
+    private static final String OP_TYPE_KEY = "clipper:opType";
 
     private final Context mContext;
     private final ClipStore mClipStore;
@@ -99,19 +99,35 @@
             return null;
         }
 
-        return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
-                ? createJumboClipData(uriBuilder, selection, opType)
-                : createStandardClipData(uriBuilder, selection, opType);
+        final List<Uri> uris = new ArrayList<>(selection.size());
+        for (String id : selection) {
+            uris.add(uriBuilder.apply(id));
+        }
+        return getClipDataForDocuments(uris, opType);
+    }
+
+    @Override
+    public ClipData getClipDataForDocuments(
+            List<Uri> uris, @OpType int opType, DocumentInfo parent) {
+        ClipData clipData = getClipDataForDocuments(uris, opType);
+        clipData.getDescription().getExtras().putString(
+                SRC_PARENT_KEY, parent.derivedUri.toString());
+        return clipData;
+    }
+
+    private ClipData getClipDataForDocuments(List<Uri> uris, @OpType int opType) {
+        return (uris.size() > Shared.MAX_DOCS_IN_INTENT)
+                ? createJumboClipData(uris, opType)
+                : createStandardClipData(uris, opType);
     }
 
     /**
      * Returns ClipData representing the selection.
      */
-    private ClipData createStandardClipData(
-            Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+    private ClipData createStandardClipData(List<Uri> uris, @OpType int opType) {
 
-        assert(!selection.isEmpty());
-        assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
+        assert(!uris.isEmpty());
+        assert(uris.size() <= Shared.MAX_DOCS_IN_INTENT);
 
         final ContentResolver resolver = mContext.getContentResolver();
         final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
@@ -120,9 +136,7 @@
         PersistableBundle bundle = new PersistableBundle();
         bundle.putInt(OP_TYPE_KEY, opType);
 
-        for (String id : selection) {
-            assert(id != null);
-            Uri uri = uriBuilder.apply(id);
+        for (Uri uri : uris) {
             DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
             clipItems.add(new ClipData.Item(uri));
         }
@@ -138,36 +152,29 @@
     /**
      * Returns ClipData representing the list of docs
      */
-    private ClipData createJumboClipData(
-            Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+    private ClipData createJumboClipData(List<Uri> uris, @OpType int opType) {
 
-        assert(!selection.isEmpty());
-        assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
+        assert(!uris.isEmpty());
+        assert(uris.size() > Shared.MAX_DOCS_IN_INTENT);
 
-        final List<Uri> uris = new ArrayList<>(selection.size());
-
-        final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
+        final int capacity = Math.min(uris.size(), Shared.MAX_DOCS_IN_INTENT);
         final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
 
         // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
         final ContentResolver resolver = mContext.getContentResolver();
         final Set<String> clipTypes = new HashSet<>();
         int docCount = 0;
-        for (String id : selection) {
-            assert(id != null);
-            Uri uri = uriBuilder.apply(id);
+        for (Uri uri : uris) {
             if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
                 DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
                 clipItems.add(new ClipData.Item(uri));
             }
-
-            uris.add(uri);
         }
 
         // Prepare metadata
         PersistableBundle bundle = new PersistableBundle();
         bundle.putInt(OP_TYPE_KEY, opType);
-        bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
+        bundle.putInt(OP_JUMBO_SELECTION_SIZE, uris.size());
 
         // Persists clip items and gets the slot they were saved under.
         int tag = mClipStore.persistUris(uris);
@@ -226,20 +233,10 @@
 
     @Override
     public void copyFromClipData(
-            final RootInfo root,
-            final DocumentInfo destination,
-            final @Nullable ClipData clipData,
-            final FileOperations.Callback callback) {
-        DocumentStack dstStack = new DocumentStack(root, destination);
-        copyFromClipData(dstStack, clipData, callback);
-    }
-
-    @Override
-    public void copyFromClipData(
-            final DocumentInfo destination,
-            final DocumentStack docStack,
-            final @Nullable ClipData clipData,
-            final FileOperations.Callback callback) {
+            DocumentInfo destination,
+            DocumentStack docStack,
+            @Nullable ClipData clipData,
+            FileOperations.Callback callback) {
 
         DocumentStack dstStack = new DocumentStack(docStack, destination);
         copyFromClipData(dstStack, clipData, callback);
@@ -247,9 +244,20 @@
 
     @Override
     public void copyFromClipData(
-            final DocumentStack dstStack,
-            final @Nullable ClipData clipData,
-            final FileOperations.Callback callback) {
+            DocumentStack dstStack,
+            ClipData clipData,
+            @OpType int opType,
+            FileOperations.Callback callback) {
+
+        clipData.getDescription().getExtras().putInt(OP_TYPE_KEY, opType);
+        copyFromClipData(dstStack, clipData, callback);
+    }
+
+    @Override
+    public void copyFromClipData(
+            DocumentStack dstStack,
+            @Nullable ClipData clipData,
+            FileOperations.Callback callback) {
 
         if (clipData == null) {
             Log.i(TAG, "Received null clipData. Ignoring.");
@@ -302,8 +310,7 @@
         return dest != null && dest.isDirectory() && dest.isCreateSupported();
     }
 
-    @Override
-    public @OpType int getOpType(ClipData data) {
+    private @OpType int getOpType(ClipData data) {
         PersistableBundle bundle = data.getDescription().getExtras();
         return getOpType(bundle);
     }
diff --git a/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
index 591e402..949d51e 100644
--- a/src/com/android/documentsui/dirlist/DirectoryDragListener.java
+++ b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -19,6 +19,7 @@
 import android.view.DragEvent;
 import android.view.View;
 
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.ItemDragListener;
 
 import java.util.TimerTask;
@@ -27,6 +28,7 @@
 
 class DirectoryDragListener extends ItemDragListener<DragHost<?>> {
 
+
     DirectoryDragListener(com.android.documentsui.dirlist.DragHost<?> host) {
         super(host);
     }
@@ -54,7 +56,7 @@
 
     @Override
     public @Nullable TimerTask createOpenTask(final View v, DragEvent event) {
-        return mDragHost.canCopyTo(event.getLocalState(), v)
+        return mDragHost.canSpringOpen(v)
                 ? super.createOpenTask(v, event) : null;
     }
 }
\ No newline at end of file
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 0170b3f..cf4b6a2 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -66,6 +66,7 @@
 import com.android.documentsui.BaseActivity.RetainedState;
 import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.Injector.ContentScoped;
@@ -236,7 +237,7 @@
             DirectoryDragListener listener = new DirectoryDragListener(
                     new DragHost<>(
                             mActivity,
-                            mActivity.getShadowBuilder(),
+                            DocumentsApplication.getDragAndDropManager(mActivity),
                             mInjector.selectionMgr,
                             mInjector.actions,
                             mActivity.getDisplayState(),
@@ -245,8 +246,7 @@
                                 return getModelId(v) != null;
                             },
                             this::getDocumentHolder,
-                            this::getDestination,
-                            mClipper
+                            this::getDestination
                     ));
             mDragHoverListener = DragHoverListener.create(listener, mRecView);
         }
@@ -349,15 +349,12 @@
         DragStartListener mDragStartListener = mInjector.config.dragAndDropEnabled()
                 ? DragStartListener.create(
                         mIconHelper,
-                        mActivity,
                         mModel,
                         mSelectionMgr,
-                        mClipper,
                         mState,
                         this::getModelId,
                         mRecView::findChildViewUnder,
-                        getContext().getDrawable(R.drawable.ic_doc_generic),
-                        mActivity.getShadowBuilder())
+                        DocumentsApplication.getDragAndDropManager(mActivity))
                 : DragStartListener.DUMMY;
 
         EventHandler<InputEvent> gestureHandler = mState.allowMultiple
diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java
index 3015d42..7c6c6a8 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -121,7 +121,6 @@
 
     /**
      * Highlights the associated item view to indicate it's droppable.
-     * @param highlighted
      */
     public void setDroppableHighlight(boolean droppable) {
         // If item is already selected, its highlight should not be changed.
diff --git a/src/com/android/documentsui/dirlist/DragHost.java b/src/com/android/documentsui/dirlist/DragHost.java
index 0ab3994..0391431 100644
--- a/src/com/android/documentsui/dirlist/DragHost.java
+++ b/src/com/android/documentsui/dirlist/DragHost.java
@@ -22,17 +22,14 @@
 import android.view.View;
 
 import com.android.documentsui.AbstractActionHandler;
+import com.android.documentsui.AbstractDragHost;
 import com.android.documentsui.ActionHandler;
-import com.android.documentsui.DragAndDropHelper;
-import com.android.documentsui.DragShadowBuilder;
-import com.android.documentsui.ItemDragListener;
-import com.android.documentsui.Metrics;
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
 import com.android.documentsui.base.Lookup;
 import com.android.documentsui.base.State;
-import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.ui.DialogController;
 
 import java.util.function.Predicate;
@@ -40,11 +37,9 @@
 /**
  * Drag host for items in {@link DirectoryFragment}.
  */
-class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
-        implements ItemDragListener.DragHost {
+class DragHost<T extends Activity & AbstractActionHandler.CommonAddons> extends AbstractDragHost {
 
     private final T mActivity;
-    private final DragShadowBuilder mShadowBuilder;
     private final SelectionManager mSelectionMgr;
     private final ActionHandler mActions;
     private final State mState;
@@ -52,21 +47,20 @@
     private final Predicate<View> mIsDocumentView;
     private final Lookup<View, DocumentHolder> mHolderLookup;
     private final Lookup<View, DocumentInfo> mDestinationLookup;
-    private final DocumentClipper mClipper;
 
     DragHost(
             T activity,
-            DragShadowBuilder shadowBuilder,
+            DragAndDropManager dragAndDropManager,
             SelectionManager selectionMgr,
             ActionHandler actions,
             State state,
             DialogController dialogs,
             Predicate<View> isDocumentView,
             Lookup<View, DocumentHolder> holderLookup,
-            Lookup<View, DocumentInfo> destinationLookup,
-            DocumentClipper clipper) {
+            Lookup<View, DocumentInfo> destinationLookup) {
+        super(dragAndDropManager);
+
         mActivity = activity;
-        mShadowBuilder = shadowBuilder;
         mSelectionMgr = selectionMgr;
         mActions = actions;
         mState = state;
@@ -74,7 +68,6 @@
         mIsDocumentView = isDocumentView;
         mHolderLookup = holderLookup;
         mDestinationLookup = destinationLookup;
-        mClipper = clipper;
     }
 
     void dragStopped(boolean result) {
@@ -89,7 +82,7 @@
     }
 
     @Override
-    public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
+    public void setDropTargetHighlight(View v, boolean highlight) {
         // Note: use exact comparison - this code is searching for views which are children of
         // the RecyclerView instance in the UI.
         if (mIsDocumentView.test(v)) {
@@ -98,7 +91,7 @@
                 if (!highlight) {
                     holder.resetDropHighlight();
                 } else {
-                    holder.setDroppableHighlight(canCopyTo(localState, v));
+                    holder.setDroppableHighlight(canSpringOpen(v));
                 }
             }
         }
@@ -113,16 +106,14 @@
     }
 
     @Override
-    public void onDragEntered(View v, Object localState) {
+    public void onDragEntered(View v) {
         mActivity.setRootsDrawerOpen(false);
-        mShadowBuilder.setAppearDroppable(canCopyTo(localState, v));
-        v.updateDragShadow(mShadowBuilder);
+        mDragAndDropManager.updateState(v, mState.stack.getRoot(), mDestinationLookup.lookup(v));
     }
 
     @Override
-    public void onDragExited(View v, Object localState) {
-        mShadowBuilder.resetBackground();
-        v.updateDragShadow(mShadowBuilder);
+    public void onDragExited(View v) {
+        super.onDragExited(v);
         if (mIsDocumentView.test(v)) {
             DocumentHolder holder = mHolderLookup.lookup(v);
             if (holder != null) {
@@ -131,45 +122,23 @@
         }
     }
 
+    boolean canSpringOpen(View v) {
+        DocumentInfo doc = mDestinationLookup.lookup(v);
+        return (doc != null) && mDragAndDropManager.canSpringOpen(mState.stack.getRoot(), doc);
+    }
+
     boolean handleDropEvent(View v, DragEvent event) {
         mActivity.setRootsDrawerOpen(false);
 
         ClipData clipData = event.getClipData();
         assert (clipData != null);
 
-        assert(mClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
-
-        if (!canCopyTo(event.getLocalState(), v)) {
-            return false;
-        }
-
-        // Recognize multi-window drag and drop based on the fact that localState is not
-        // carried between processes. It will stop working when the localsState behavior
-        // is changed. The info about window should be passed in the localState then.
-        // The localState could also be null for copying from Recents in single window
-        // mode, but Recents doesn't offer this functionality (no directories).
-        Metrics.logUserAction(mActivity,
-                event.getLocalState() == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
-                        : Metrics.USER_ACTION_DRAG_N_DROP);
-
         DocumentInfo dst = mDestinationLookup.lookup(v);
         // If destination is already at top of stack, no need to pass it in
-        if (dst.equals(mState.stack.peek())) {
-            mClipper.copyFromClipData(
-                    mState.stack,
-                    clipData,
-                    mDialogs::showFileOperationStatus);
-        } else {
-            mClipper.copyFromClipData(
-                    dst,
-                    mState.stack,
-                    clipData,
-                    mDialogs::showFileOperationStatus);
-        }
-        return true;
-    }
-
-    boolean canCopyTo(Object localState, View v) {
-        return DragAndDropHelper.canCopyTo(localState, mDestinationLookup.lookup(v));
+        DocumentStack dstStack = dst.equals(mState.stack.peek())
+                ? mState.stack
+                : new DocumentStack(mState.stack, dst);
+        return mDragAndDropManager.drop(event.getClipData(), event.getLocalState(), dstStack,
+                mDialogs::showFileOperationStatus);
     }
 }
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index a0b0f64..a35d1c1 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -18,25 +18,21 @@
 
 import static com.android.documentsui.base.Shared.DEBUG;
 
-import android.content.ClipData;
-import android.content.Context;
-import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 import android.view.View;
 
-import com.android.documentsui.DragShadowBuilder;
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.Model;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Events;
 import com.android.documentsui.base.Events.InputEvent;
 import com.android.documentsui.base.State;
-import com.android.documentsui.clipping.DocumentClipper;
 import com.android.documentsui.selection.Selection;
 import com.android.documentsui.selection.SelectionManager;
-import com.android.documentsui.services.FileOperationService;
-import com.android.documentsui.services.FileOperationService.OpType;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
 
@@ -49,7 +45,7 @@
  */
 interface DragStartListener {
 
-    public static final DragStartListener DUMMY = new DragStartListener() {
+    static final DragStartListener DUMMY = new DragStartListener() {
         @Override
         public boolean onMouseDragEvent(InputEvent event) {
             return false;
@@ -64,36 +60,36 @@
     boolean onTouchDragEvent(InputEvent event);
 
     @VisibleForTesting
-    static class ActiveListener implements DragStartListener {
+    class ActiveListener implements DragStartListener {
 
         private static String TAG = "DragStartListener";
 
+        private final IconHelper mIconHelper;
         private final State mState;
         private final SelectionManager mSelectionMgr;
         private final ViewFinder mViewFinder;
         private final Function<View, String> mIdFinder;
-        private final ClipDataFactory mClipFactory;
-        private final Function<Selection, DragShadowBuilder> mShadowFactory;
-        private Function<Selection, List<DocumentInfo>> mDocsConverter;
+        private final Function<Selection, List<DocumentInfo>> mDocsConverter;
+        private final DragAndDropManager mDragAndDropManager;
 
         // use DragStartListener.create
         @VisibleForTesting
         public ActiveListener(
+                IconHelper iconHelper,
                 State state,
                 SelectionManager selectionMgr,
                 ViewFinder viewFinder,
                 Function<View, String> idFinder,
                 Function<Selection, List<DocumentInfo>> docsConverter,
-                ClipDataFactory clipFactory,
-                Function<Selection, DragShadowBuilder> shadowFactory) {
+                DragAndDropManager dragAndDropManager) {
 
+            mIconHelper = iconHelper;
             mState = state;
             mSelectionMgr = selectionMgr;
             mViewFinder = viewFinder;
             mIdFinder = idFinder;
             mDocsConverter = docsConverter;
-            mClipFactory = clipFactory;
-            mShadowFactory = shadowFactory;
+            mDragAndDropManager = dragAndDropManager;
         }
 
         @Override
@@ -110,7 +106,7 @@
         /**
          * May be called externally when drag is initiated from other event handling code.
          */
-        private final boolean startDrag(@Nullable View view, InputEvent event) {
+        private boolean startDrag(@Nullable View view, InputEvent event) {
 
             if (view == null) {
                 if (DEBUG) Log.d(TAG, "Ignoring drag event, null view.");
@@ -125,23 +121,17 @@
 
             Selection selection = getSelectionToBeCopied(modelId, event);
 
-            final List<DocumentInfo> invalidDest = mDocsConverter.apply(selection);
-            invalidDest.add(mState.stack.peek());
-            // 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.
-            startDragAndDrop(
-                    view,
-                    mClipFactory.create(
-                            selection,
-                            FileOperationService.OPERATION_COPY),
-                    mShadowFactory.apply(selection),
-                    invalidDest,
-                    View.DRAG_FLAG_GLOBAL
-                            | View.DRAG_FLAG_OPAQUE
-                            | View.DRAG_FLAG_GLOBAL_URI_READ
-                            | View.DRAG_FLAG_GLOBAL_URI_WRITE);
+            final List<DocumentInfo> srcs = mDocsConverter.apply(selection);
+
+            final DocumentInfo parent = mState.stack.peek();
+            final List<Uri> invalidDest = new ArrayList<>(srcs.size() + 1);
+            for (DocumentInfo doc : srcs) {
+                invalidDest.add(doc.derivedUri);
+            }
+            invalidDest.add(parent.derivedUri);
+
+            mDragAndDropManager.startDrag(
+                    view, parent, srcs, mState.stack.getRoot(), invalidDest, mIconHelper);
 
             return true;
         }
@@ -168,63 +158,29 @@
             }
             return selection;
         }
-
-        /**
-         * This exists as a testing workaround since {@link View#startDragAndDrop} is final.
-         */
-        @VisibleForTesting
-        void startDragAndDrop(
-                View view,
-                ClipData data,
-                DragShadowBuilder shadowBuilder,
-                Object localState,
-                int flags) {
-
-            view.startDragAndDrop(data, shadowBuilder, localState, flags);
-        }
     }
 
-    public static DragStartListener create(
+    static DragStartListener create(
             IconHelper iconHelper,
-            Context context,
             Model model,
             SelectionManager selectionMgr,
-            DocumentClipper clipper,
             State state,
             Function<View, String> idFinder,
             ViewFinder viewFinder,
-            Drawable defaultDragIcon,
-            DragShadowBuilder shadowBuilder) {
-
-        DragShadowBuilder.Updater shadowFactory = new DragShadowBuilder.Updater(
-                context,
-                shadowBuilder,
-                model,
-                iconHelper,
-                defaultDragIcon);
+            DragAndDropManager dragAndDropManager) {
 
         return new ActiveListener(
+                iconHelper,
                 state,
                 selectionMgr,
                 viewFinder,
                 idFinder,
                 model::getDocuments,
-                (Selection selection, @OpType int operationType) -> {
-                    return clipper.getClipDataForDocuments(
-                            model::getItemUri,
-                            selection,
-                            FileOperationService.OPERATION_COPY);
-                },
-                shadowFactory);
+                dragAndDropManager);
     }
 
     @FunctionalInterface
     interface ViewFinder {
         @Nullable View findView(float x, float y);
     }
-
-    @FunctionalInterface
-    interface ClipDataFactory {
-        ClipData create(Selection selection, @OpType int operationType);
-    }
 }
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index b23b3f9..635a199 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -34,7 +34,7 @@
 import com.android.documentsui.ActivityConfig;
 import com.android.documentsui.DocumentsAccess;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.DragAndDropHelper;
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.Model;
@@ -62,6 +62,7 @@
 import com.android.documentsui.selection.Selection;
 import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
 import com.android.documentsui.ui.DialogController;
 import com.android.internal.annotations.VisibleForTesting;
@@ -85,6 +86,7 @@
     private final DialogController mDialogs;
     private final DocumentClipper mClipper;
     private final ClipStore mClipStore;
+    private final DragAndDropManager mDragAndDropManager;
     private final Model mModel;
 
     ActionHandler(
@@ -97,6 +99,7 @@
             ActionModeAddons actionModeAddons,
             DocumentClipper clipper,
             ClipStore clipStore,
+            DragAndDropManager dragAndDropManager,
             Injector injector) {
 
         super(activity, state, providers, docs, searchMgr, executors, injector);
@@ -107,6 +110,7 @@
         mDialogs = injector.dialogs;
         mClipper = clipper;
         mClipStore = clipStore;
+        mDragAndDropManager = dragAndDropManager;
         mModel = injector.getModel();
     }
 
@@ -121,21 +125,9 @@
         // references to ensure they are non null.
         final ClipData clipData = event.getClipData();
         final Object localState = event.getLocalState();
-        getRootDocument(
-                root,
-                TimeoutTask.DEFAULT_TIMEOUT,
-                (DocumentInfo rootDoc) -> dropOnCallback(clipData, localState, rootDoc, root));
-        return true;
-    }
 
-    private void dropOnCallback(
-            ClipData clipData, Object localState, DocumentInfo rootDoc, RootInfo root) {
-        if (!DragAndDropHelper.canCopyTo(localState, rootDoc)) {
-            return;
-        }
-
-        mClipper.copyFromClipData(
-                root, rootDoc, clipData, mDialogs::showFileOperationStatus);
+        return mDragAndDropManager.drop(
+                clipData, localState, root, this, mDialogs::showFileOperationStatus);
     }
 
     @Override
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index 6599332..646f88e 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -33,7 +33,6 @@
 import com.android.documentsui.ActionModeController;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
-import com.android.documentsui.DragShadowBuilder;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.MenuManager.DirectoryDetails;
@@ -70,7 +69,6 @@
     private Injector<ActionHandler<FilesActivity>> mInjector;
     private ActivityInputHandler mActivityInputHandler;
     private SharedInputHandler mSharedInputHandler;
-    private DragShadowBuilder mShadowBuilder;
 
     public FilesActivity() {
         super(R.layout.files_activity, TAG);
@@ -115,7 +113,6 @@
                 mProviders::getApplicationName,
                 mInjector.getModel()::getItemUri);
 
-        mShadowBuilder = new DragShadowBuilder(this);
         mInjector.actionModeController = new ActionModeController(
                 this,
                 mInjector.selectionMgr,
@@ -132,6 +129,7 @@
                 mInjector.actionModeController,
                 clipper,
                 DocumentsApplication.getClipStore(this),
+                DocumentsApplication.getDragAndDropManager(this),
                 mInjector);
 
         mInjector.searchManager = mSearchManager;
@@ -351,11 +349,6 @@
     }
 
     @Override
-    public DragShadowBuilder getShadowBuilder() {
-        return mShadowBuilder;
-    }
-
-    @Override
     public boolean onKeyShortcut(int keyCode, KeyEvent event) {
         DirectoryFragment dir;
         // TODO: All key events should be statically bound using alphabeticShortcut.
diff --git a/src/com/android/documentsui/sidebar/DragHost.java b/src/com/android/documentsui/sidebar/DragHost.java
index 1bd0c6f..6b034df 100644
--- a/src/com/android/documentsui/sidebar/DragHost.java
+++ b/src/com/android/documentsui/sidebar/DragHost.java
@@ -20,33 +20,32 @@
 import android.util.Log;
 import android.view.View;
 
+import com.android.documentsui.AbstractDragHost;
 import com.android.documentsui.ActionHandler;
-import com.android.documentsui.DragAndDropHelper;
-import com.android.documentsui.DragShadowBuilder;
-import com.android.documentsui.ItemDragListener;
+import com.android.documentsui.DragAndDropManager;
 import com.android.documentsui.base.DocumentInfo;
 import com.android.documentsui.base.Lookup;
 
 /**
  * Drag host for items in {@link RootsFragment}.
  */
-class DragHost implements ItemDragListener.DragHost {
+class DragHost extends AbstractDragHost {
 
     private static final String TAG = "RootsDragHost";
     private static final int DRAG_LOAD_TIME_OUT = 500;
 
     private final Activity mActivity;
-    private final DragShadowBuilder mShadowBuilder;
     private final Lookup<View, Item> mDestinationLookup;
     private final ActionHandler mActions;
 
     DragHost(
             Activity activity,
-            DragShadowBuilder shadowBuilder,
+            DragAndDropManager dragAndDropManager,
             Lookup<View, Item> destinationLookup,
             ActionHandler actions) {
+        super(dragAndDropManager);
         mActivity = activity;
-        mShadowBuilder = shadowBuilder;
+        mDragAndDropManager = dragAndDropManager;
         mDestinationLookup = destinationLookup;
         mActions = actions;
     }
@@ -57,7 +56,7 @@
     }
 
     @Override
-    public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
+    public void setDropTargetHighlight(View v, boolean highlight) {
         // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
         RootItemView itemView = (RootItemView) v;
         itemView.setHighlight(highlight);
@@ -73,41 +72,34 @@
     }
 
     @Override
-    public void onDragEntered(View v, Object localState) {
+    public void onDragEntered(View v) {
         final Item item = mDestinationLookup.lookup(v);
 
         // If a read-only root, no need to see if top level is writable (it's not)
         if (!item.isDropTarget()) {
-            mShadowBuilder.setAppearDroppable(false);
-            v.updateDragShadow(mShadowBuilder);
+            mDragAndDropManager.updateStateToNotAllowed(v);
             return;
         }
 
         final RootItem rootItem = (RootItem) item;
-        mActions.getRootDocument(
-                rootItem.root,
-                DRAG_LOAD_TIME_OUT,
-                (DocumentInfo doc) -> {
-                    updateDropShadow(v, localState, rootItem, doc);
-                });
+        if (mDragAndDropManager.updateState(v, rootItem.root, null)
+                == DragAndDropManager.STATE_UNKNOWN) {
+            mActions.getRootDocument(
+                    rootItem.root,
+                    DRAG_LOAD_TIME_OUT,
+                    (DocumentInfo doc) -> {
+                        updateDropShadow(v, rootItem, doc);
+                    });
+        }
     }
 
     private void updateDropShadow(
-            View v, Object localState, RootItem rootItem, DocumentInfo rootDoc) {
+            View v, RootItem rootItem, DocumentInfo rootDoc) {
         if (rootDoc == null) {
-            Log.e(TAG, "Root DocumentInfo is null. Defaulting to appear not droppable.");
-            mShadowBuilder.setAppearDroppable(false);
+            Log.e(TAG, "Root DocumentInfo is null. Defaulting to unknown.");
         } else {
             rootItem.docInfo = rootDoc;
-            mShadowBuilder.setAppearDroppable(rootDoc.isCreateSupported()
-                    && DragAndDropHelper.canCopyTo(localState, rootDoc));
+            mDragAndDropManager.updateState(v, rootItem.root, rootDoc);
         }
-        v.updateDragShadow(mShadowBuilder);
-    }
-
-    @Override
-    public void onDragExited(View v, Object localState) {
-        mShadowBuilder.resetBackground();
-        v.updateDragShadow(mShadowBuilder);
     }
 }
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index fa792be..c5c188f 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -192,7 +192,7 @@
         if (mInjector.config.dragAndDropEnabled()) {
             final DragHost host = new DragHost(
                     activity,
-                    activity.getShadowBuilder(),
+                    DocumentsApplication.getDragAndDropManager(activity),
                     this::getItem,
                     mActionHandler);
             mDragListener = new ItemDragListener<DragHost>(host) {