Add Drag and Drop states to RootList.

Also, changed Drag Shadow to 100% opaque.

Bug: 32772075
Change-Id: I8629e4c1caed3cef6460866e5fd39f14801ccbb4
diff --git a/src/com/android/documentsui/DragAndDropHelper.java b/src/com/android/documentsui/DragAndDropHelper.java
new file mode 100644
index 0000000..1b634c3
--- /dev/null
+++ b/src/com/android/documentsui/DragAndDropHelper.java
@@ -0,0 +1,54 @@
+/*
+ * 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/DrawerController.java b/src/com/android/documentsui/DrawerController.java
index 12f00ee..3bf4bf9 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -167,6 +167,11 @@
         }
 
         @Override
+        public void onDragExited(View v, Object localState) {
+            // do nothing
+        }
+
+        @Override
         public void onViewHovered(View v) {
         }
 
diff --git a/src/com/android/documentsui/HorizontalBreadcrumb.java b/src/com/android/documentsui/HorizontalBreadcrumb.java
index 598decc..60d1f96 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -131,6 +131,11 @@
     }
 
     @Override
+    public void onDragExited(View v, Object localState) {
+        // do nothing
+    }
+
+    @Override
     public void onViewHovered(View v) {
         int pos = getChildAdapterPosition(v);
         if (pos != mAdapter.getItemCount() - 1) {
diff --git a/src/com/android/documentsui/ItemDragListener.java b/src/com/android/documentsui/ItemDragListener.java
index 87c1274..f966b74 100644
--- a/src/com/android/documentsui/ItemDragListener.java
+++ b/src/com/android/documentsui/ItemDragListener.java
@@ -16,6 +16,8 @@
 
 package com.android.documentsui;
 
+import static com.android.documentsui.base.Shared.DEBUG;
+
 import android.content.ClipData;
 import android.graphics.drawable.Drawable;
 import android.util.Log;
@@ -24,8 +26,10 @@
 import android.view.View.OnDragListener;
 
 import com.android.documentsui.ItemDragListener.DragHost;
+import com.android.documentsui.base.DocumentInfo;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
 
@@ -67,6 +71,8 @@
                 handleLocationEvent(v, event.getX(), event.getY());
                 return true;
             case DragEvent.ACTION_DRAG_EXITED:
+                mDragHost.onDragExited(v, event.getLocalState());
+                // fall through
             case DragEvent.ACTION_DRAG_ENDED:
                 handleExitedEndedEvent(v, event);
                 return true;
@@ -97,7 +103,6 @@
 
     private void handleExitedEndedEvent(View v, DragEvent event) {
         mDragHost.setDropTargetHighlight(v, event.getLocalState(), false);
-
         TimerTask task = (TimerTask) v.getTag(R.id.drag_hovering_tag);
         if (task != null) {
             task.cancel();
@@ -171,5 +176,12 @@
          * @param localState the Local state object given by DragEvent
          */
         void onDragEntered(View v, Object localState);
+
+        /**
+         * 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);
     }
 }
diff --git a/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
index 044d688..e7340a1 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.DragAndDropHelper;
 import com.android.documentsui.ItemDragListener;
 
 import java.util.TimerTask;
@@ -36,10 +37,6 @@
         final boolean result = super.onDrag(v, event);
 
         switch (event.getAction()) {
-            case DragEvent.ACTION_DRAG_EXITED:
-                // If drag exits, we want to update drag and drop status on the drop shadow
-                mDragHost.dragExited(v);
-                break;
             case DragEvent.ACTION_DRAG_ENDED:
                 // getResult() is true if drag was accepted
                 mDragHost.dragStopped(event.getResult());
@@ -58,7 +55,7 @@
 
     @Override
     public @Nullable TimerTask createOpenTask(final View v, DragEvent event) {
-        return mDragHost.canCopyTo(event.getLocalState(), v) ?
-                super.createOpenTask(v, event) : null;
+        return DragAndDropHelper.canCopyTo(event.getLocalState(), mDragHost.getDestination(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 d44c2f7..4e04ef8 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -72,6 +72,7 @@
 import com.android.documentsui.DirectoryReloadLock;
 import com.android.documentsui.DirectoryResult;
 import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DragAndDropHelper;
 import com.android.documentsui.FocusManager;
 import com.android.documentsui.Injector;
 import com.android.documentsui.Injector.ContentScoped;
@@ -896,18 +897,6 @@
         }
     }
 
-    void dragExited(View v) {
-        // For now, just always reset drag shadow when drag exits
-        mActivity.getShadowBuilder().resetBackground();
-        v.updateDragShadow(mActivity.getShadowBuilder());
-        if (v.getParent() == mRecView) {
-            DocumentHolder holder = getDocumentHolder(v);
-            if (holder != null) {
-                holder.resetDropHighlight();
-            }
-        }
-    }
-
     void dragStopped(boolean result) {
         if (result) {
             mSelectionMgr.clearSelection();
@@ -919,25 +908,32 @@
         getActivity().runOnUiThread(runnable);
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * In DirectoryFragment, we close the roots drawer right away.
-     * We also want to update the Drag Shadow to indicate whether the
-     * item is droppable or not.
-     */
+    // In DirectoryFragment, we close the roots drawer right away.
+    // We also want to update the Drag Shadow to indicate whether the
+    // item is droppable or not.
     @Override
     public void onDragEntered(View v, Object localState) {
         mActivity.setRootsDrawerOpen(false);
-        mActivity.getShadowBuilder().setAppearDroppable(canCopyTo(localState, v));
+        mActivity.getShadowBuilder()
+                .setAppearDroppable(DragAndDropHelper.canCopyTo(localState, getDestination(v)));
         v.updateDragShadow(mActivity.getShadowBuilder());
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * In DirectoryFragment, we spring loads the hovered folder.
-     */
+    // In DirectoryFragment, we always reset the background of the Drag Shadow once it
+    // exits.
+    @Override
+    public void onDragExited(View v, Object localState) {
+        mActivity.getShadowBuilder().resetBackground();
+        v.updateDragShadow(mActivity.getShadowBuilder());
+        if (v.getParent() == mRecView) {
+            DocumentHolder holder = getDocumentHolder(v);
+            if (holder != null) {
+                holder.resetDropHighlight();
+            }
+        }
+    }
+
+    // In DirectoryFragment, we spring loads the hovered folder.
     @Override
     public void onViewHovered(View view) {
         BaseActivity activity = mActivity;
@@ -956,7 +952,7 @@
 
         assert(DocumentClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
 
-        if (!canCopyTo(event.getLocalState(), v)) {
+        if (!DragAndDropHelper.canCopyTo(event.getLocalState(), getDestination(v))) {
             return false;
         }
 
@@ -978,24 +974,7 @@
         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 canCopyTo(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) {
+    DocumentInfo getDestination(View v) {
         String id = getModelId(v);
         if (id != null) {
             Cursor dstCursor = mModel.getItem(id);
@@ -1023,7 +1002,8 @@
                 if (!highlight) {
                     holder.resetDropHighlight();
                 } else {
-                    holder.setDroppableHighlight(canCopyTo(localState, v));
+                    holder.setDroppableHighlight(
+                            DragAndDropHelper.canCopyTo(localState, getDestination(v)));
                 }
             }
         }
diff --git a/src/com/android/documentsui/dirlist/DragStartListener.java b/src/com/android/documentsui/dirlist/DragStartListener.java
index af864d9..6598a87 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -138,6 +138,7 @@
                     mShadowFactory.apply(selection),
                     invalidDest,
                     View.DRAG_FLAG_GLOBAL
+                            | View.DRAG_FLAG_OPAQUE
                             | View.DRAG_FLAG_GLOBAL_URI_READ
                             | View.DRAG_FLAG_GLOBAL_URI_WRITE);
 
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 71c66b7..e913857 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -50,6 +50,7 @@
 import com.android.documentsui.ActionHandler;
 import com.android.documentsui.BaseActivity;
 import com.android.documentsui.DocumentsApplication;
+import com.android.documentsui.DragAndDropHelper;
 import com.android.documentsui.Injector;
 import com.android.documentsui.Injector.Injected;
 import com.android.documentsui.ItemDragListener;
@@ -164,31 +165,19 @@
     }
 
     private boolean onRightClick(View v, int x, int y, Runnable callback) {
-        int pos = mList.pointToPosition(x, y);
+        final int pos = mList.pointToPosition(x, y);
         final Item item = mAdapter.getItem(pos);
-        if (!(item instanceof RootItem)) {
+
+        // If a read-only root, no need to see if top level is writable (it's not)
+        if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
             return false;
         }
+
         final RootItem rootItem = (RootItem) item;
-
-        if (!rootItem.root.supportsCreate()) {
-            // If a read-only root, no need to see if top level is writable (it's not)
+        getRootDocument(rootItem, (DocumentInfo doc) -> {
+            rootItem.docInfo = doc;
             callback.run();
-            return true;
-        }
-        // We need to start a GetRootDocumentTask so we can know whether items can be directly
-        // pasted into root
-        GetRootDocumentTask task = new GetRootDocumentTask(
-                rootItem.root,
-                getBaseActivity(),
-                (DocumentInfo doc) -> {
-                    rootItem.docInfo = doc;
-                    callback.run();
-                });
-        task.setTimeout(CONTEXT_MENU_ITEM_TIMEOUT);
-        task.setForceCallback(true);
-        task.executeOnExecutor(getBaseActivity().getExecutorForCurrentDirectory());
-
+        });
         return true;
     }
 
@@ -372,20 +361,38 @@
         getActivity().runOnUiThread(runnable);
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * In RootsFragment we don't do anything
-     */
+    // In RootsFragment, we check whether the item corresponds to a RootItem, and whether
+    // the currently dragged objects can be droppable or not, and change the drop-shadow
+    // accordingly
     @Override
     public void onDragEntered(View v, Object localState) {
+        final int pos = (Integer) v.getTag(R.id.item_position_tag);
+        final Item item = mAdapter.getItem(pos);
+
+        // If a read-only root, no need to see if top level is writable (it's not)
+        if (!(item instanceof RootItem) || !((RootItem) item).root.supportsCreate()) {
+            getBaseActivity().getShadowBuilder().setAppearDroppable(false);
+            v.updateDragShadow(getBaseActivity().getShadowBuilder());
+            return;
+        }
+
+        final RootItem rootItem = (RootItem) item;
+        getRootDocument(rootItem, (DocumentInfo doc) -> {
+            rootItem.docInfo = doc;
+            getBaseActivity().getShadowBuilder().setAppearDroppable(
+                    doc.isCreateSupported() && DragAndDropHelper.canCopyTo(localState, doc));
+            v.updateDragShadow(getBaseActivity().getShadowBuilder());
+        });
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * In RootsFragment we open the hovered root.
-     */
+    // In RootsFragment we always reset the drag shadow as it exits a RootItemView.
+    @Override
+    public void onDragExited(View v, Object localState) {
+        getBaseActivity().getShadowBuilder().resetBackground();
+        v.updateDragShadow(getBaseActivity().getShadowBuilder());
+    }
+
+    // In RootsFragment we open the hovered root.
     @Override
     public void onViewHovered(View v) {
         // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView.
@@ -446,6 +453,25 @@
         }
     }
 
+    @FunctionalInterface
+    interface RootUpdater {
+        void updateDocInfoForRoot(DocumentInfo doc);
+    }
+
+    private void getRootDocument(RootItem rootItem, RootUpdater updater) {
+        // We need to start a GetRootDocumentTask so we can know whether items can be directly
+        // pasted into root
+        GetRootDocumentTask task = new GetRootDocumentTask(
+                rootItem.root,
+                getBaseActivity(),
+                (DocumentInfo doc) -> {
+                    updater.updateDocInfoForRoot(doc);
+                });
+        task.setTimeout(CONTEXT_MENU_ITEM_TIMEOUT);
+        task.setForceCallback(true);
+        task.executeOnExecutor(getBaseActivity().getExecutorForCurrentDirectory());
+    }
+
     static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
         assert(ejectIcon != null);
         assert(!root.ejecting);
diff --git a/tests/unit/com/android/documentsui/ItemDragListenerTest.java b/tests/unit/com/android/documentsui/ItemDragListenerTest.java
index 71799ac..d3c5051 100644
--- a/tests/unit/com/android/documentsui/ItemDragListenerTest.java
+++ b/tests/unit/com/android/documentsui/ItemDragListenerTest.java
@@ -129,6 +129,7 @@
         mTestTimer.fastForwardTo(DELAY_AFTER_HOVERING);
 
         assertSame(mTestView, mTestDragHost.mLastEnteredView);
+        assertSame(mTestView, mTestDragHost.mLastExitedView);
         assertNull(mTestDragHost.mLastHoveredView);
     }
 
@@ -200,6 +201,7 @@
         private View mHighlightedView;
         private View mLastHoveredView;
         private View mLastEnteredView;
+        private View mLastExitedView;
 
         @Override
         public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
@@ -220,5 +222,10 @@
         public void onDragEntered(View v, Object localState) {
             mLastEnteredView = v;
         }
+
+        @Override
+        public void onDragExited(View v, Object localState) {
+            mLastExitedView = v;
+        }
     }
 }
diff --git a/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
index 3a0cc4c..e5c31aa 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
@@ -202,6 +202,10 @@
         @Override
         public void onDragEntered(View v, Object localState) {
         }
+
+        @Override
+        public void onDragExited(View v, Object localState) {
+        }
     }
 
     private class TestScrollActionDelegate implements ScrollActionDelegate {