Prevent Drag n' Drop/Spring-load onto a selected folder.

Bug: 29538691
Change-Id: I01b1bb59ea816d6dc328dbf7830640442041bcfb
diff --git a/src/com/android/documentsui/ItemDragListener.java b/src/com/android/documentsui/ItemDragListener.java
index 152d3a0..2d67ff9 100644
--- a/src/com/android/documentsui/ItemDragListener.java
+++ b/src/com/android/documentsui/ItemDragListener.java
@@ -29,6 +29,8 @@
 import java.util.Timer;
 import java.util.TimerTask;
 
+import javax.annotation.Nullable;
+
 /**
  * An {@link OnDragListener} that adds support for "spring loading views". Use this when you want
  * items to pop-open when user hovers on them during a drag n drop.
@@ -59,7 +61,7 @@
             case DragEvent.ACTION_DRAG_STARTED:
                 return true;
             case DragEvent.ACTION_DRAG_ENTERED:
-                handleEnteredEvent(v);
+                handleEnteredEvent(v, event);
                 return true;
             case DragEvent.ACTION_DRAG_LOCATION:
                 handleLocationEvent(v, event.getX(), event.getY());
@@ -75,11 +77,12 @@
         return false;
     }
 
-    private void handleEnteredEvent(View v) {
+    private void handleEnteredEvent(View v, DragEvent event) {
+        @Nullable TimerTask task = createOpenTask(v, event);
+        if (task == null) {
+            return;
+        }
         mDragHost.setDropTargetHighlight(v, true);
-
-        TimerTask task = createOpenTask(v);
-        assert (task != null);
         v.setTag(R.id.drag_hovering_tag, task);
         mHoverTimer.schedule(task, SPRING_TIMEOUT);
     }
@@ -110,8 +113,10 @@
         return handleDropEventChecked(v, event);
     }
 
-    @VisibleForTesting
-    TimerTask createOpenTask(final View v) {
+    /**
+     * Sub-classes such as {@link DirectoryDragListener} can override this method and return null.
+     */
+    public @Nullable TimerTask createOpenTask(final View v, DragEvent event) {
         TimerTask task = new TimerTask() {
             @Override
             public void run() {
diff --git a/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
index f0a7aae..8b0a663 100644
--- a/src/com/android/documentsui/dirlist/DirectoryDragListener.java
+++ b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -21,6 +21,10 @@
 
 import com.android.documentsui.ItemDragListener;
 
+import java.util.TimerTask;
+
+import javax.annotation.Nullable;
+
 class DirectoryDragListener extends ItemDragListener<DirectoryFragment> {
 
     DirectoryDragListener(DirectoryFragment fragment) {
@@ -45,4 +49,10 @@
     public boolean handleDropEventChecked(View v, DragEvent event) {
         return mDragHost.handleDropEvent(v, event);
     }
+
+    @Override
+    public @Nullable TimerTask createOpenTask(final View v, DragEvent event) {
+        return mDragHost.shouldCopyTo(event.getLocalState(), 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 367a134..7982e5c 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -1350,7 +1350,6 @@
         if (getModelId(view) != null) {
            activity.springOpenDirectory(getDestination(view));
         }
-
         activity.setRootsDrawerOpen(false);
     }
 
@@ -1363,13 +1362,7 @@
 
         assert(DocumentClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
 
-        // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
-        // multi-window drag, because localState isn't carried over from one process to
-        // another.
-        Object src = event.getLocalState();
-        DocumentInfo dst = getDestination(v);
-        if (Objects.equals(src, dst)) {
-            if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
+        if (!shouldCopyTo(event.getLocalState(), v)) {
             return false;
         }
 
@@ -1379,13 +1372,31 @@
         // 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(getContext(),
-                src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+                event.getLocalState() == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
                         : Metrics.USER_ACTION_DRAG_N_DROP);
 
+        DocumentInfo dst = getDestination(v);
         mClipper.copyFromClipData(dst, getDisplayState().stack, clipData, activity.fileOpCallback);
         return true;
     }
 
+    // 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.
+    boolean shouldCopyTo(Object dragLocalState, View destinationView) {
+        if (dragLocalState == null || !(dragLocalState instanceof List<?>)) {
+            if (DEBUG) Log.d(TAG, "Invalid local state object. Will allow copy.");
+            return true;
+        }
+        DocumentInfo dst = getDestination(destinationView);
+        List<?> src = (List<?>) dragLocalState;
+        if (src.contains(dst)) {
+            if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
+            return false;
+        }
+        return true;
+    }
+
     private DocumentInfo getDestination(View v) {
         String id = getModelId(v);
         if (id != null) {
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index 563e3a8..8690600 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -34,6 +34,7 @@
 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;
@@ -70,6 +71,7 @@
         private final Function<View, String> mIdFinder;
         private final ClipDataFactory mClipFactory;
         private final Function<Selection, DragShadowBuilder> mShadowFactory;
+        private Function<Selection, List<DocumentInfo>> mDocsConverter;
 
         // use DragStartListener.create
         @VisibleForTesting
@@ -78,6 +80,7 @@
                 MultiSelectManager selectionMgr,
                 ViewFinder viewFinder,
                 Function<View, String> idFinder,
+                Function<Selection, List<DocumentInfo>> docsConverter,
                 ClipDataFactory clipFactory,
                 Function<Selection, DragShadowBuilder> shadowFactory) {
 
@@ -85,6 +88,7 @@
             mSelectionMgr = selectionMgr;
             mViewFinder = viewFinder;
             mIdFinder = idFinder;
+            mDocsConverter = docsConverter;
             mClipFactory = clipFactory;
             mShadowFactory = shadowFactory;
         }
@@ -133,6 +137,8 @@
                 mSelectionMgr.getSelection(selection);
             }
 
+            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
@@ -143,7 +149,7 @@
                             selection,
                             FileOperationService.OPERATION_COPY),
                     mShadowFactory.apply(selection),
-                    mState.stack.peek(),
+                    invalidDest,
                     View.DRAG_FLAG_GLOBAL
                             | View.DRAG_FLAG_GLOBAL_URI_READ
                             | View.DRAG_FLAG_GLOBAL_URI_WRITE);
@@ -159,10 +165,10 @@
                 View view,
                 ClipData data,
                 DragShadowBuilder shadowBuilder,
-                DocumentInfo currentDirectory,
+                Object localState,
                 int flags) {
 
-            view.startDragAndDrop(data, shadowBuilder, currentDirectory, flags);
+            view.startDragAndDrop(data, shadowBuilder, localState, flags);
         }
     }
 
@@ -185,6 +191,7 @@
                 selectionMgr,
                 viewFinder,
                 idFinder,
+                model::getDocuments,
                 (Selection selection, @OpType int operationType) -> {
                     return clipper.getClipDataForDocuments(
                             model::getItemUri,
diff --git a/tests/src/com/android/documentsui/ItemDragListenerTest.java b/tests/src/com/android/documentsui/ItemDragListenerTest.java
index 37f6532..e1fb87a 100644
--- a/tests/src/com/android/documentsui/ItemDragListenerTest.java
+++ b/tests/src/com/android/documentsui/ItemDragListenerTest.java
@@ -177,8 +177,8 @@
         }
 
         @Override
-        public TimerTask createOpenTask(View v) {
-            TimerTask task = super.createOpenTask(v);
+        public TimerTask createOpenTask(View v, DragEvent event) {
+            TimerTask task = super.createOpenTask(v, event);
             TestTimer.Task testTask = new TestTimer.Task(task);
 
             return testTask;
diff --git a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
index dc67a74..9e3ed56 100644
--- a/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/src/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -57,6 +57,10 @@
                 (View view) -> {
                     return viewModelId;
                 },
+                // docInfo Converter
+                (Selection selection) -> {
+                    return new ArrayList<>();
+                },
                 // ClipDataFactory
                 (Selection selection, int operationType) -> {
                     return null;
@@ -71,7 +75,7 @@
                     View view,
                     ClipData data,
                     DragShadowBuilder shadowBuilder,
-                    DocumentInfo currentDirectory,
+                    Object localState,
                     int flags) {
 
                 dragStarted = true;