summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Garfield Tan <xutan@google.com> 2017-04-11 13:47:58 -0700
committer Garfield Tan <xutan@google.com> 2017-04-14 15:09:35 -0700
commitda2c0f0b075ad9f770182e706c2ec158989568a7 (patch)
tree13d2c7335a151d03c3788bdd93a510582428030e
parentaa3d615c9a710de50445985bf374f99790d3ec42 (diff)
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
-rw-r--r--res/drawable/drop_badge_states.xml20
-rw-r--r--res/drawable/ic_drop_copy_badge.xml (renamed from res/drawable/ic_drop_ok_badge.xml)0
-rw-r--r--res/drawable/ic_reject_drop_badge.xml (renamed from res/drawable/ic_drop_not_ok_badge.xml)0
-rw-r--r--res/values/attrs.xml6
-rw-r--r--src/com/android/documentsui/AbstractDragHost.java46
-rw-r--r--src/com/android/documentsui/BaseActivity.java8
-rw-r--r--src/com/android/documentsui/DocumentsApplication.java7
-rw-r--r--src/com/android/documentsui/DragAndDropHelper.java54
-rw-r--r--src/com/android/documentsui/DragAndDropManager.java467
-rw-r--r--src/com/android/documentsui/DragShadowBuilder.java83
-rw-r--r--src/com/android/documentsui/DrawerController.java11
-rw-r--r--src/com/android/documentsui/DropBadgeView.java38
-rw-r--r--src/com/android/documentsui/HorizontalBreadcrumb.java11
-rw-r--r--src/com/android/documentsui/Injector.java4
-rw-r--r--src/com/android/documentsui/ItemDragListener.java30
-rw-r--r--src/com/android/documentsui/base/Shared.java3
-rw-r--r--src/com/android/documentsui/clipping/DocumentClipper.java59
-rw-r--r--src/com/android/documentsui/clipping/RuntimeDocumentClipper.java87
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryDragListener.java4
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java11
-rw-r--r--src/com/android/documentsui/dirlist/DocumentHolder.java1
-rw-r--r--src/com/android/documentsui/dirlist/DragHost.java79
-rw-r--r--src/com/android/documentsui/dirlist/DragStartListener.java100
-rw-r--r--src/com/android/documentsui/files/ActionHandler.java22
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java9
-rw-r--r--src/com/android/documentsui/sidebar/DragHost.java50
-rw-r--r--src/com/android/documentsui/sidebar/RootsFragment.java2
-rw-r--r--tests/common/com/android/documentsui/testing/ClipDatas.java9
-rw-r--r--tests/common/com/android/documentsui/testing/KeyEvents.java52
-rw-r--r--tests/common/com/android/documentsui/testing/TestActionHandler.java10
-rw-r--r--tests/common/com/android/documentsui/testing/TestDocumentClipper.java49
-rw-r--r--tests/common/com/android/documentsui/testing/TestDragAndDropManager.java83
-rw-r--r--tests/common/com/android/documentsui/testing/TestIconHelper.java43
-rw-r--r--tests/common/com/android/documentsui/testing/TestResources.java18
-rw-r--r--tests/common/com/android/documentsui/ui/TestDialogController.java12
-rw-r--r--tests/unit/com/android/documentsui/DragAndDropManagerTests.java732
-rw-r--r--tests/unit/com/android/documentsui/ItemDragListenerTest.java12
-rw-r--r--tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java18
-rw-r--r--tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java76
-rw-r--r--tests/unit/com/android/documentsui/files/ActionHandlerTest.java64
40 files changed, 1823 insertions, 567 deletions
diff --git a/res/drawable/drop_badge_states.xml b/res/drawable/drop_badge_states.xml
index f859b7ca1..57d43d3fc 100644
--- a/res/drawable/drop_badge_states.xml
+++ b/res/drawable/drop_badge_states.xml
@@ -16,15 +16,21 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <!-- state when we can't drop -->
<item
- app:state_droppable="true"
- app:state_drop_hovered="true"
- android:drawable="@drawable/ic_drop_ok_badge" />
+ app:state_reject_drop="true"
+ android:drawable="@drawable/ic_reject_drop_badge"/>
+
+ <!-- state when we can drop, and it will be a copy -->
<item
- app:state_droppable="false"
- app:state_drop_hovered="true"
- android:drawable="@drawable/ic_drop_not_ok_badge" />
+ app:state_reject_drop="false"
+ app:state_copy="true"
+ android:drawable="@drawable/ic_drop_copy_badge"/>
+
+ <!-- default state. Also used to show state when we can drop, and it will be a move -->
<item
- app:state_drop_hovered="false"
+ app:state_reject_drop="false"
+ app:state_copy="false"
android:drawable="@android:color/transparent" />
</selector> \ No newline at end of file
diff --git a/res/drawable/ic_drop_ok_badge.xml b/res/drawable/ic_drop_copy_badge.xml
index 7f1be3151..7f1be3151 100644
--- a/res/drawable/ic_drop_ok_badge.xml
+++ b/res/drawable/ic_drop_copy_badge.xml
diff --git a/res/drawable/ic_drop_not_ok_badge.xml b/res/drawable/ic_reject_drop_badge.xml
index 402aff8f8..402aff8f8 100644
--- a/res/drawable/ic_drop_not_ok_badge.xml
+++ b/res/drawable/ic_reject_drop_badge.xml
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index a5b128026..b4c0812d2 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -17,8 +17,8 @@
<declare-styleable name="HighlightedItemView">
<attr name="state_highlighted" format="boolean"/>
</declare-styleable>
- <declare-styleable name="DroppableItemView">
- <attr name="state_droppable" format="boolean"/>
- <attr name="state_drop_hovered" format="boolean"/>
+ <declare-styleable name="DropBadgeView">
+ <attr name="state_reject_drop" format="boolean"/>
+ <attr name="state_copy" format="boolean"/>
</declare-styleable>
</resources>
diff --git a/src/com/android/documentsui/AbstractDragHost.java b/src/com/android/documentsui/AbstractDragHost.java
new file mode 100644
index 000000000..a0d13a939
--- /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 cd3b57e41..ded4621cc 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -443,11 +443,6 @@ public abstract class BaseActivity
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 @@ public abstract class BaseActivity
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 ee466d4a5..e0b255991 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -40,6 +40,7 @@ public class DocumentsApplication extends Application {
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 @@ public class DocumentsApplication extends Application {
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 @@ public class DocumentsApplication extends Application {
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 1b634c39a..000000000
--- 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 000000000..9262ac7a5
--- /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 3ba09d0d7..10a0106d5 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.LayoutInflater;
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 @@ public final class DragShadowBuilder extends View.DragShadowBuilder {
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 @@ public final class DragShadowBuilder extends View.DragShadowBuilder {
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 92921571a..d8c679aa7 100644
--- a/src/com/android/documentsui/DrawerController.java
+++ b/src/com/android/documentsui/DrawerController.java
@@ -131,7 +131,7 @@ public abstract class DrawerController implements DrawerListener {
}
@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 @@ public abstract class DrawerController implements DrawerListener {
}
@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 @@ public abstract class DrawerController implements DrawerListener {
}
@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 4bf3f74c5..6f71d018e 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 @@ import android.widget.ImageView;
* 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 @@ public final class DropBadgeView extends ImageView {
@Override
public int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 2);
-
- if (mDroppable) {
- mergeDrawableStates(drawableState, STATE_DROPPABLE);
- }
-
- if (mDropHovered) {
- mergeDrawableStates(drawableState, STATE_DROP_HOVERED);
+ // STATE_REJECT_DROP and STATE_COPY can't exist at the same time.
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ 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;
- refreshDrawableState();
- }
-
- public void setDropHovered(boolean hovered) {
- mDropHovered = hovered;
+ void updateState(@State int state) {
+ mState = state;
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 d6410c8bf..2a3b82d0a 100644
--- a/src/com/android/documentsui/HorizontalBreadcrumb.java
+++ b/src/com/android/documentsui/HorizontalBreadcrumb.java
@@ -146,7 +146,7 @@ public final class HorizontalBreadcrumb extends RecyclerView
}
@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 @@ public final class HorizontalBreadcrumb extends RecyclerView
}
@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 @@ public final class HorizontalBreadcrumb extends RecyclerView
}
}
+ @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 387a9abe9..aa3d43c32 100644
--- a/src/com/android/documentsui/Injector.java
+++ b/src/com/android/documentsui/Injector.java
@@ -53,6 +53,8 @@ public class Injector<T extends ActionHandler> {
public DialogController dialogs;
public SearchViewManager searchManager;
+ public final DebugHelper debugHelper;
+
@ContentScoped
public ActionModeController actionModeController;
@@ -67,8 +69,6 @@ public class Injector<T extends ActionHandler> {
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 783f6d446..cee5e9149 100644
--- a/src/com/android/documentsui/ItemDragListener.java
+++ b/src/com/android/documentsui/ItemDragListener.java
@@ -73,9 +73,11 @@ public class ItemDragListener<H extends DragHost> implements OnDragListener {
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 @@ public class ItemDragListener<H extends DragHost> implements OnDragListener {
}
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 @@ public class ItemDragListener<H extends DragHost> implements OnDragListener {
}
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 @@ public class ItemDragListener<H extends DragHost> implements OnDragListener {
/**
* 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 @@ public class ItemDragListener<H extends DragHost> implements OnDragListener {
/**
* 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 0e24eda22..de42ab443 100644
--- a/src/com/android/documentsui/base/Shared.java
+++ b/src/com/android/documentsui/base/Shared.java
@@ -34,6 +34,7 @@ import android.util.Log;
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 @@ public final class Shared {
}
/**
- * @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 e462dded1..3777d7a9a 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.selection.Selection;
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 @@ public interface DocumentClipper {
}
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 @@ public interface DocumentClipper {
* @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 @@ public interface DocumentClipper {
* 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
+ * 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 RootInfo root,
- final DocumentInfo destination,
- 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.
+ * Copies documents from given clip data to a folder, ignoring the op type in clip data.
*
- * @param destination destination folder
- * @param docStack the document stack to the destination folder (not including the destination
- * folder)
+ * @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 opType the operation type
+ * @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);
+ DocumentStack dstStack,
+ ClipData clipData,
+ @OpType int opType,
+ FileOperations.Callback callback);
/**
* Copies documents from given clip data to a folder.
@@ -120,10 +127,10 @@ public interface DocumentClipper {
* @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 8b837d386..012d3fbe5 100644
--- a/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
+++ b/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
@@ -52,8 +52,8 @@ import java.util.function.Function;
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 @@ final class RuntimeDocumentClipper implements DocumentClipper {
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 @@ final class RuntimeDocumentClipper implements DocumentClipper {
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 @@ final class RuntimeDocumentClipper implements DocumentClipper {
/**
* 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,30 +233,31 @@ final class RuntimeDocumentClipper implements DocumentClipper {
@Override
public void copyFromClipData(
- final RootInfo root,
- final DocumentInfo destination,
- final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
- DocumentStack dstStack = new DocumentStack(root, destination);
+ DocumentInfo destination,
+ DocumentStack docStack,
+ @Nullable ClipData clipData,
+ FileOperations.Callback callback) {
+
+ DocumentStack dstStack = new DocumentStack(docStack, destination);
copyFromClipData(dstStack, clipData, callback);
}
@Override
public void copyFromClipData(
- final DocumentInfo destination,
- final DocumentStack docStack,
- final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
+ DocumentStack dstStack,
+ ClipData clipData,
+ @OpType int opType,
+ FileOperations.Callback callback) {
- DocumentStack dstStack = new DocumentStack(docStack, destination);
+ clipData.getDescription().getExtras().putInt(OP_TYPE_KEY, opType);
copyFromClipData(dstStack, clipData, callback);
}
@Override
public void copyFromClipData(
- final DocumentStack dstStack,
- final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
+ DocumentStack dstStack,
+ @Nullable ClipData clipData,
+ FileOperations.Callback callback) {
if (clipData == null) {
Log.i(TAG, "Received null clipData. Ignoring.");
@@ -302,8 +310,7 @@ final class RuntimeDocumentClipper implements DocumentClipper {
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 591e402ca..949d51ecb 100644
--- a/src/com/android/documentsui/dirlist/DirectoryDragListener.java
+++ b/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -19,6 +19,7 @@ package com.android.documentsui.dirlist;
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 @@ import javax.annotation.Nullable;
class DirectoryDragListener extends ItemDragListener<DragHost<?>> {
+
DirectoryDragListener(com.android.documentsui.dirlist.DragHost<?> host) {
super(host);
}
@@ -54,7 +56,7 @@ class DirectoryDragListener extends ItemDragListener<DragHost<?>> {
@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 0170b3f70..cf4b6a2d7 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;
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 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
DirectoryDragListener listener = new DirectoryDragListener(
new DragHost<>(
mActivity,
- mActivity.getShadowBuilder(),
+ DocumentsApplication.getDragAndDropManager(mActivity),
mInjector.selectionMgr,
mInjector.actions,
mActivity.getDisplayState(),
@@ -245,8 +246,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
return getModelId(v) != null;
},
this::getDocumentHolder,
- this::getDestination,
- mClipper
+ this::getDestination
));
mDragHoverListener = DragHoverListener.create(listener, mRecView);
}
@@ -349,15 +349,12 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On
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 3015d4264..7c6c6a89d 100644
--- a/src/com/android/documentsui/dirlist/DocumentHolder.java
+++ b/src/com/android/documentsui/dirlist/DocumentHolder.java
@@ -121,7 +121,6 @@ public abstract class DocumentHolder
/**
* 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 0ab399496..0391431fd 100644
--- a/src/com/android/documentsui/dirlist/DragHost.java
+++ b/src/com/android/documentsui/dirlist/DragHost.java
@@ -22,17 +22,14 @@ import android.view.DragEvent;
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 @@ import java.util.function.Predicate;
/**
* 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 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
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 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
mIsDocumentView = isDocumentView;
mHolderLookup = holderLookup;
mDestinationLookup = destinationLookup;
- mClipper = clipper;
}
void dragStopped(boolean result) {
@@ -89,7 +82,7 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
}
@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 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
if (!highlight) {
holder.resetDropHighlight();
} else {
- holder.setDroppableHighlight(canCopyTo(localState, v));
+ holder.setDroppableHighlight(canSpringOpen(v));
}
}
}
@@ -113,16 +106,14 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
}
@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 @@ class DragHost<T extends Activity & AbstractActionHandler.CommonAddons>
}
}
+ 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 a0b0f645d..a35d1c1a0 100644
--- a/src/com/android/documentsui/dirlist/DragStartListener.java
+++ b/src/com/android/documentsui/dirlist/DragStartListener.java
@@ -18,25 +18,21 @@ package com.android.documentsui.dirlist;
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 @@ import javax.annotation.Nullable;
*/
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 @@ interface DragStartListener {
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 @@ interface DragStartListener {
/**
* 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 @@ interface DragStartListener {
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 @@ interface DragStartListener {
}
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 b23b3f9fb..635a19984 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.ActionModeAddons;
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.roots.ProvidersAccess;
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 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
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 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
ActionModeAddons actionModeAddons,
DocumentClipper clipper,
ClipStore clipStore,
+ DragAndDropManager dragAndDropManager,
Injector injector) {
super(activity, state, providers, docs, searchMgr, executors, injector);
@@ -107,6 +110,7 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
mDialogs = injector.dialogs;
mClipper = clipper;
mClipStore = clipStore;
+ mDragAndDropManager = dragAndDropManager;
mModel = injector.getModel();
}
@@ -121,21 +125,9 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
// 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 6599332eb..646f88e39 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -33,7 +33,6 @@ import android.view.MenuItem;
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 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons
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 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons
mProviders::getApplicationName,
mInjector.getModel()::getItemUri);
- mShadowBuilder = new DragShadowBuilder(this);
mInjector.actionModeController = new ActionModeController(
this,
mInjector.selectionMgr,
@@ -132,6 +129,7 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons
mInjector.actionModeController,
clipper,
DocumentsApplication.getClipStore(this),
+ DocumentsApplication.getDragAndDropManager(this),
mInjector);
mInjector.searchManager = mSearchManager;
@@ -351,11 +349,6 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons
}
@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 1bd0c6f84..6b034df4f 100644
--- a/src/com/android/documentsui/sidebar/DragHost.java
+++ b/src/com/android/documentsui/sidebar/DragHost.java
@@ -20,33 +20,32 @@ import android.app.Activity;
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 @@ class DragHost implements ItemDragListener.DragHost {
}
@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 @@ class DragHost implements ItemDragListener.DragHost {
}
@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 fa792be15..c5c188f5c 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -192,7 +192,7 @@ public class RootsFragment extends Fragment {
if (mInjector.config.dragAndDropEnabled()) {
final DragHost host = new DragHost(
activity,
- activity.getShadowBuilder(),
+ DocumentsApplication.getDragAndDropManager(activity),
this::getItem,
mActionHandler);
mDragListener = new ItemDragListener<DragHost>(host) {
diff --git a/tests/common/com/android/documentsui/testing/ClipDatas.java b/tests/common/com/android/documentsui/testing/ClipDatas.java
index 525a02e4f..de570711b 100644
--- a/tests/common/com/android/documentsui/testing/ClipDatas.java
+++ b/tests/common/com/android/documentsui/testing/ClipDatas.java
@@ -17,7 +17,9 @@
package com.android.documentsui.testing;
import android.content.ClipData;
+import android.content.ClipDescription;
+import org.mockito.Answers;
import org.mockito.Mockito;
public final class ClipDatas {
@@ -30,4 +32,11 @@ public final class ClipDatas {
return data;
}
+ public static ClipData createTestClipData(ClipDescription description) {
+ final ClipData data = createTestClipData();
+
+ Mockito.when(data.getDescription()).thenReturn(description);
+
+ return data;
+ }
}
diff --git a/tests/common/com/android/documentsui/testing/KeyEvents.java b/tests/common/com/android/documentsui/testing/KeyEvents.java
new file mode 100644
index 000000000..f965fcc2a
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/KeyEvents.java
@@ -0,0 +1,52 @@
+/*
+ * 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.testing;
+
+import android.os.SystemClock;
+import android.view.KeyEvent;
+
+public class KeyEvents {
+
+ private KeyEvents() {}
+
+ public static KeyEvent createTestEvent(int action, int keyCode, int meta) {
+ long time = SystemClock.uptimeMillis();
+ return new KeyEvent(
+ time,
+ time,
+ action,
+ keyCode,
+ 0,
+ meta);
+ }
+
+ public static KeyEvent createLeftCtrlKey(int action) {
+ int meta = (action == KeyEvent.ACTION_UP)
+ ? 0
+ : KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_META_ON;
+
+ return createTestEvent(action, KeyEvent.KEYCODE_CTRL_LEFT, meta);
+ }
+
+ public static KeyEvent createRightCtrlKey(int action) {
+ int meta = (action == KeyEvent.ACTION_UP)
+ ? 0
+ : KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_RIGHT_ON | KeyEvent.META_META_ON;
+
+ return createTestEvent(action, KeyEvent.KEYCODE_CTRL_RIGHT, meta);
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestActionHandler.java b/tests/common/com/android/documentsui/testing/TestActionHandler.java
index c09e0b0fb..01956ad6b 100644
--- a/tests/common/com/android/documentsui/testing/TestActionHandler.java
+++ b/tests/common/com/android/documentsui/testing/TestActionHandler.java
@@ -21,15 +21,20 @@ import android.content.Intent;
import com.android.documentsui.AbstractActionHandler;
import com.android.documentsui.ActionHandler;
import com.android.documentsui.TestActivity;
+import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.Model;
+import java.util.function.Consumer;
+
public class TestActionHandler extends AbstractActionHandler<TestActivity> {
public final TestEventHandler<DocumentDetails> open = new TestEventHandler<>();
public boolean mDeleteHappened;
+ public DocumentInfo nextRootDocument;
+
public TestActionHandler() {
this(TestEnv.create());
}
@@ -69,4 +74,9 @@ public class TestActionHandler extends AbstractActionHandler<TestActivity> {
protected void launchToDefaultLocation() {
throw new UnsupportedOperationException();
}
+
+ @Override
+ public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) {
+ callback.accept(nextRootDocument);
+ }
}
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentClipper.java b/tests/common/com/android/documentsui/testing/TestDocumentClipper.java
index 0caf83602..b183f5afc 100644
--- a/tests/common/com/android/documentsui/testing/TestDocumentClipper.java
+++ b/tests/common/com/android/documentsui/testing/TestDocumentClipper.java
@@ -16,24 +16,28 @@
package com.android.documentsui.testing;
-import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertSame;
-
import android.content.ClipData;
import android.net.Uri;
+import android.util.Pair;
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.selection.Selection;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations.Callback;
+import java.util.List;
import java.util.function.Function;
public class TestDocumentClipper implements DocumentClipper {
- private ClipData mLastClipData;
+ public ClipData nextClip;
+ public ClipData primaryClip;
+
+ public final TestEventListener<Pair<DocumentStack, ClipData>> copy = new TestEventListener<>();
+ public final TestEventListener<Integer> opType = new TestEventListener<>();
@Override
public boolean hasItemsToPaste() {
@@ -43,12 +47,17 @@ public class TestDocumentClipper implements DocumentClipper {
@Override
public ClipData getClipDataForDocuments(Function<String, Uri> uriBuilder, Selection selection,
int opType) {
- return null;
+ return nextClip;
}
@Override
- public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
+ public ClipData getClipDataForDocuments(List<Uri> uris,
+ @FileOperationService.OpType int opType, DocumentInfo parent) {
+ return nextClip;
+ }
+ @Override
+ public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
}
@Override
@@ -59,37 +68,29 @@ public class TestDocumentClipper implements DocumentClipper {
@Override
public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack,
Callback callback) {
+ copy.accept(Pair.create(new DocumentStack(docStack, destination), primaryClip));
}
@Override
public void copyFromClipboard(DocumentStack docStack, Callback callback) {
- }
-
- @Override
- public void copyFromClipData(RootInfo root, DocumentInfo destination, ClipData clipData,
- Callback callback) {
- mLastClipData = clipData;
+ copy.accept(Pair.create(docStack, primaryClip));
}
@Override
public void copyFromClipData(DocumentInfo destination, DocumentStack docStack,
ClipData clipData, Callback callback) {
+ copy.accept(Pair.create(new DocumentStack(docStack, destination), clipData));
}
@Override
- public void copyFromClipData(DocumentStack docStack, ClipData clipData, Callback callback) {
+ public void copyFromClipData(DocumentStack dstStack, ClipData clipData,
+ @OpType int opType, Callback callback) {
+ copy.accept(Pair.create(dstStack, clipData));
+ this.opType.accept(opType);
}
@Override
- public int getOpType(ClipData data) {
- return 0;
- }
-
- public void assertNoClipData() {
- assertNull(mLastClipData);
- }
-
- public void assertSameClipData(ClipData expect) {
- assertSame(expect, mLastClipData);
+ public void copyFromClipData(DocumentStack docStack, ClipData clipData, Callback callback) {
+ copy.accept(Pair.create(docStack, clipData));
}
}
diff --git a/tests/common/com/android/documentsui/testing/TestDragAndDropManager.java b/tests/common/com/android/documentsui/testing/TestDragAndDropManager.java
new file mode 100644
index 000000000..6f4bebea7
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestDragAndDropManager.java
@@ -0,0 +1,83 @@
+/*
+ * 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.testing;
+
+import android.annotation.Nullable;
+import android.content.ClipData;
+import android.net.Uri;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.documentsui.ActionHandler;
+import com.android.documentsui.DragAndDropManager;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.dirlist.IconHelper;
+import com.android.documentsui.services.FileOperations;
+
+import java.util.List;
+
+public class TestDragAndDropManager implements DragAndDropManager {
+
+ public final TestEventListener<List<DocumentInfo>> startDragHandler = new TestEventListener<>();
+ public final TestEventHandler<Pair<ClipData, RootInfo>> dropOnRootHandler =
+ new TestEventHandler<>();
+ public final TestEventHandler<Pair<ClipData, DocumentStack>> dropOnDocumentHandler =
+ new TestEventHandler<>();
+
+ @Override
+ public void onKeyEvent(KeyEvent event) {}
+
+ @Override
+ public void startDrag(View v, DocumentInfo parent, List<DocumentInfo> srcs, RootInfo root,
+ List<Uri> invalidDest, IconHelper iconHelper) {
+ startDragHandler.accept(srcs);
+ }
+
+ @Override
+ public boolean canSpringOpen(RootInfo root, DocumentInfo doc) {
+ return false;
+ }
+
+ @Override
+ public void updateStateToNotAllowed(View v) {}
+
+ @Override
+ public int updateState(View v, RootInfo destRoot, @Nullable DocumentInfo destDoc) {
+ return 0;
+ }
+
+ @Override
+ public void resetState(View v) {}
+
+ @Override
+ public boolean drop(ClipData clipData, Object localState, RootInfo root, ActionHandler actions,
+ FileOperations.Callback callback) {
+ return dropOnRootHandler.accept(Pair.create(clipData, root));
+ }
+
+ @Override
+ public boolean drop(ClipData clipData, Object localState, DocumentStack dstStack,
+ FileOperations.Callback callback) {
+ return dropOnDocumentHandler.accept(Pair.create(clipData, dstStack));
+ }
+
+ @Override
+ public void dragEnded() {}
+}
diff --git a/tests/common/com/android/documentsui/testing/TestIconHelper.java b/tests/common/com/android/documentsui/testing/TestIconHelper.java
new file mode 100644
index 000000000..ff7956de6
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestIconHelper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.testing;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.dirlist.IconHelper;
+
+import org.mockito.Mockito;
+
+public class TestIconHelper extends IconHelper {
+
+ public Drawable nextDocumentIcon;
+
+ private TestIconHelper() {
+ super(null, 0);
+ }
+
+ @Override
+ public Drawable getDocumentIcon(Context context, DocumentInfo doc) {
+ return nextDocumentIcon;
+ }
+
+ public static TestIconHelper create() {
+ return Mockito.mock(TestIconHelper.class, Mockito.CALLS_REAL_METHODS);
+ }
+}
diff --git a/tests/common/com/android/documentsui/testing/TestResources.java b/tests/common/com/android/documentsui/testing/TestResources.java
index 56d904c95..9f4cd383f 100644
--- a/tests/common/com/android/documentsui/testing/TestResources.java
+++ b/tests/common/com/android/documentsui/testing/TestResources.java
@@ -18,6 +18,7 @@ package com.android.documentsui.testing;
import android.annotation.BoolRes;
import android.annotation.NonNull;
+import android.annotation.PluralsRes;
import android.annotation.StringRes;
import android.content.res.Resources;
import android.util.SparseArray;
@@ -38,6 +39,7 @@ public abstract class TestResources extends Resources {
public SparseBooleanArray bools;
public SparseArray<String> strings;
+ public SparseArray<String> plurals;
public TestResources() {
super(ClassLoader.getSystemClassLoader());
@@ -48,6 +50,7 @@ public abstract class TestResources extends Resources {
TestResources.class, Mockito.CALLS_REAL_METHODS);
res.bools = new SparseBooleanArray();
res.strings = new SparseArray<>();
+ res.plurals = new SparseArray<>();
res.setProductivityDeviceEnabled(false);
@@ -83,6 +86,21 @@ public abstract class TestResources extends Resources {
return getString(id);
}
+ @Override
+ public final @Nullable String getQuantityString(@PluralsRes int id, int size) {
+ return plurals.get(id);
+ }
+
+ @Override
+ public final @Nullable String getQuantityString(@PluralsRes int id, int size, Object... args) {
+ String format = getQuantityString(id, size);
+ if (format != null) {
+ return String.format(format, args);
+ }
+
+ return null;
+ }
+
public final CharSequence getText(@StringRes int resId) {
return getString(resId);
}
diff --git a/tests/common/com/android/documentsui/ui/TestDialogController.java b/tests/common/com/android/documentsui/ui/TestDialogController.java
index 574802485..350755be8 100644
--- a/tests/common/com/android/documentsui/ui/TestDialogController.java
+++ b/tests/common/com/android/documentsui/ui/TestDialogController.java
@@ -29,7 +29,7 @@ import java.util.List;
public class TestDialogController implements DialogController {
public int mNextConfirmationCode;
- private boolean mFileOpFailed;
+ private int mFileOpStatus;
private boolean mNoApplicationFound;
private boolean mDocumentsClipped;
private boolean mViewInArchivesUnsupported;
@@ -47,9 +47,7 @@ public class TestDialogController implements DialogController {
@Override
public void showFileOperationStatus(int status, int opType, int docCount) {
- if (status == FileOperations.Callback.STATUS_REJECTED) {
- mFileOpFailed = true;
- }
+ mFileOpStatus = status;
}
@Override
@@ -78,7 +76,11 @@ public class TestDialogController implements DialogController {
}
public void assertNoFileFailures() {
- Assert.assertFalse(mFileOpFailed);
+ Assert.assertEquals(FileOperations.Callback.STATUS_ACCEPTED, mFileOpStatus);
+ }
+
+ public void assertFileOpFailed() {
+ Assert.assertEquals(FileOperations.Callback.STATUS_FAILED, mFileOpStatus);
}
public void assertNoAppFoundShown() {
diff --git a/tests/unit/com/android/documentsui/DragAndDropManagerTests.java b/tests/unit/com/android/documentsui/DragAndDropManagerTests.java
new file mode 100644
index 000000000..812fafaa9
--- /dev/null
+++ b/tests/unit/com/android/documentsui/DragAndDropManagerTests.java
@@ -0,0 +1,732 @@
+/*
+ * 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 static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertSame;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.graphics.drawable.Drawable;
+import android.os.PersistableBundle;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+
+import com.android.documentsui.DragAndDropManager.State;
+import com.android.documentsui.DragAndDropManager.RuntimeDragAndDropManager;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.services.FileOperations;
+import com.android.documentsui.testing.ClipDatas;
+import com.android.documentsui.testing.KeyEvents;
+import com.android.documentsui.testing.TestActionHandler;
+import com.android.documentsui.testing.TestDocumentClipper;
+import com.android.documentsui.testing.TestDrawable;
+import com.android.documentsui.testing.TestEnv;
+import com.android.documentsui.testing.TestEventListener;
+import com.android.documentsui.testing.TestIconHelper;
+import com.android.documentsui.testing.TestProvidersAccess;
+import com.android.documentsui.testing.Views;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DragAndDropManagerTests {
+
+ private static final String PLURAL_FORMAT = "%1$d items";
+
+ private TestEnv mEnv;
+ private TestActivity mActivity;
+ private TestDragShadowBuilder mShadowBuilder;
+ private View mStartDragView;
+ private View mUpdateShadowView;
+ private TestActionHandler mActions;
+
+ private TestDocumentClipper mClipper;
+ private ClipData mClipData;
+
+ private TestIconHelper mIconHelper;
+ private Drawable mDefaultIcon;
+
+ private TestEventListener<ClipData> mStartDragListener;
+ private TestEventListener<Void> mShadowUpdateListener;
+
+ private TestEventListener<Integer> mCallbackListener;
+ private FileOperations.Callback mCallback = new FileOperations.Callback() {
+ @Override
+ public void onOperationResult(@Status int status,
+ @FileOperationService.OpType int opType, int docCount) {
+ mCallbackListener.accept(status);
+ }
+ };
+
+ private DragAndDropManager mManager;
+
+ @Before
+ public void setUp() {
+ mEnv = TestEnv.create();
+ mActivity = TestActivity.create(mEnv);
+ mActivity.resources.plurals.put(R.plurals.elements_dragged, PLURAL_FORMAT);
+
+ mShadowBuilder = TestDragShadowBuilder.create();
+
+ mStartDragView = Views.createTestView();
+ mUpdateShadowView = Views.createTestView();
+
+ mActions = new TestActionHandler(mEnv);
+
+ mClipper = new TestDocumentClipper();
+ ClipDescription description = new ClipDescription("", new String[]{});
+ description.setExtras(new PersistableBundle());
+ mClipData = ClipDatas.createTestClipData(description);
+ mClipper.nextClip = mClipData;
+
+ mDefaultIcon = new TestDrawable();
+ mIconHelper = TestIconHelper.create();
+ mIconHelper.nextDocumentIcon = new TestDrawable();
+
+ mStartDragListener = new TestEventListener<>();
+ mShadowUpdateListener = new TestEventListener<>();
+ mCallbackListener = new TestEventListener<>();
+
+ mManager = new RuntimeDragAndDropManager(mActivity, mClipper, mShadowBuilder,
+ mDefaultIcon) {
+ @Override
+ void startDragAndDrop(View v, ClipData clipData, DragShadowBuilder builder,
+ Object localState, int flag) {
+ assertSame(mStartDragView, v);
+ assertSame(mShadowBuilder, builder);
+ assertNotNull(localState);
+
+ mStartDragListener.accept(clipData);
+ }
+
+ @Override
+ void updateDragShadow(View v) {
+ assertSame(mUpdateShadowView, v);
+
+ mShadowUpdateListener.accept(null);
+ }
+ };
+ }
+
+ @Test
+ public void testStartDrag_SetsCorrectClipData() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mStartDragListener.assertLastArgument(mClipper.nextClip);
+ }
+
+ @Test
+ public void testStartDrag_BuildsCorrectShadow_SingleDoc() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri),
+ mIconHelper);
+
+ mShadowBuilder.title.assertLastArgument(TestEnv.FILE_APK.displayName);
+ mShadowBuilder.icon.assertLastArgument(mIconHelper.nextDocumentIcon);
+ }
+
+ @Test
+ public void testStartDrag_BuildsCorrectShadow_MultipleDocs() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mShadowBuilder.title.assertLastArgument(mActivity.getResources().getQuantityString(
+ R.plurals.elements_dragged, 2, 2));
+ mShadowBuilder.icon.assertLastArgument(mDefaultIcon);
+ }
+
+ @Test
+ public void testCanSpringOpen_ReturnsFalse_RootNotSupportCreate() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FOLDER_1, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FOLDER_1.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ assertFalse(mManager.canSpringOpen(TestProvidersAccess.HAMMY, TestEnv.FOLDER_2));
+ }
+
+ @Test
+ public void testCanSpringOpen_ReturnsFalse_DocIsInvalidDestination() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FOLDER_1, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FOLDER_1.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ assertFalse(mManager.canSpringOpen(TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1));
+ }
+
+ @Test
+ public void testCanSpringOpen() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FOLDER_1, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FOLDER_1.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ assertTrue(mManager.canSpringOpen(TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_2));
+ }
+
+ @Test
+ public void testDefaultToUnknownState() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FOLDER_1, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FOLDER_1.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mShadowBuilder.state.assertLastArgument(DragAndDropManager.STATE_UNKNOWN);
+ }
+
+ @Test
+ public void testUpdateStateToNotAllowed() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateStateToNotAllowed(mUpdateShadowView);
+
+ assertStateUpdated(DragAndDropManager.STATE_NOT_ALLOWED);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToNotAllowed_RootNotSupportCreate() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.HAMMY, TestEnv.FOLDER_2);
+
+ assertEquals(DragAndDropManager.STATE_NOT_ALLOWED, state);
+ assertStateUpdated(DragAndDropManager.STATE_NOT_ALLOWED);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToUnknown_RootDocIsNull() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, null);
+
+ assertEquals(DragAndDropManager.STATE_UNKNOWN, state);
+ assertStateUpdated(DragAndDropManager.STATE_UNKNOWN);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToMove_SameRoot() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_MOVE, state);
+ assertStateUpdated(DragAndDropManager.STATE_MOVE);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToCopy_DifferentRoot() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_COPY, state);
+ assertStateUpdated(DragAndDropManager.STATE_COPY);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToCopy_SameRoot_LeftCtrlPressed() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_COPY, state);
+ assertStateUpdated(DragAndDropManager.STATE_COPY);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToCopy_SameRoot_RightCtrlPressed() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_COPY, state);
+ assertStateUpdated(DragAndDropManager.STATE_COPY);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToMove_DifferentRoot_LeftCtrlPressed() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_MOVE, state);
+ assertStateUpdated(DragAndDropManager.STATE_MOVE);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToMove_DifferentRoot_RightCtrlPressed() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_MOVE, state);
+ assertStateUpdated(DragAndDropManager.STATE_MOVE);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToMove_SameRoot_LeftCtrlReleased() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_UP);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_MOVE, state);
+ assertStateUpdated(DragAndDropManager.STATE_MOVE);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToMove_SameRoot_RightCtrlReleased() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_UP);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_MOVE, state);
+ assertStateUpdated(DragAndDropManager.STATE_MOVE);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToCopy_DifferentRoot_LeftCtrlReleased() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ event = KeyEvents.createLeftCtrlKey(KeyEvent.ACTION_UP);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_COPY, state);
+ assertStateUpdated(DragAndDropManager.STATE_COPY);
+ }
+
+ @Test
+ public void testUpdateState_UpdatesToCopy_DifferentRoot_RightCtrlReleased() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ KeyEvent event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_DOWN);
+ mManager.onKeyEvent(event);
+
+ event = KeyEvents.createRightCtrlKey(KeyEvent.ACTION_UP);
+ mManager.onKeyEvent(event);
+
+ final @State int state = mManager.updateState(
+ mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ assertEquals(DragAndDropManager.STATE_COPY, state);
+ assertStateUpdated(DragAndDropManager.STATE_COPY);
+ }
+
+ @Test
+ public void testResetState_UpdatesToUnknown() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateStateToNotAllowed(mUpdateShadowView);
+
+ mManager.resetState(mUpdateShadowView);
+
+ assertStateUpdated(DragAndDropManager.STATE_UNKNOWN);
+ }
+
+ @Test
+ public void testDrop_Rejects_RootNotSupportCreate_DropOnRoot() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.HAMMY, TestEnv.FOLDER_1);
+
+ assertFalse(mManager.drop(
+ mClipData, mManager, TestProvidersAccess.HAMMY, mActions, mCallback));
+ }
+
+ @Test
+ public void testDrop_Rejects_InvalidRoot() {
+ RootInfo root = new RootInfo();
+ root.authority = TestProvidersAccess.HOME.authority;
+ root.documentId = TestEnv.FOLDER_0.documentId;
+
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ root,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.HOME, TestEnv.FOLDER_0);
+
+ assertFalse(mManager.drop(mClipData, mManager, root, mActions, mCallback));
+ }
+
+ @Test
+ public void testDrop_Fails_NotGetRootDoc() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ mManager.drop(
+ mClipData, mManager, TestProvidersAccess.DOWNLOADS, mActions, mCallback);
+
+ mCallbackListener.assertLastArgument(FileOperations.Callback.STATUS_FAILED);
+ }
+
+ @Test
+ public void testDrop_DifferentRoot_DropOnRoot() {
+ mActions.nextRootDocument = TestEnv.FOLDER_1;
+
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ mManager.drop(
+ mClipData, mManager, TestProvidersAccess.DOWNLOADS, mActions, mCallback);
+
+ final DocumentStack expect =
+ new DocumentStack(TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+ mClipper.copy.assertLastArgument(Pair.create(expect, mClipData));
+ mClipper.opType.assertLastArgument(FileOperationService.OPERATION_COPY);
+ }
+
+ @Test
+ public void testDrop_SameRoot_DropOnRoot() {
+ mActions.nextRootDocument = TestEnv.FOLDER_1;
+
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+
+ mManager.drop(
+ mClipData, mManager, TestProvidersAccess.DOWNLOADS, mActions, mCallback);
+
+ final DocumentStack expect =
+ new DocumentStack(TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1);
+ mClipper.copy.assertLastArgument(Pair.create(expect, mClipData));
+ mClipper.opType.assertLastArgument(FileOperationService.OPERATION_MOVE);
+ }
+
+ @Test
+ public void testDrop_Rejects_RootNotSupportCreate_DropOnDocument() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.HAMMY, TestEnv.FOLDER_2);
+
+ final DocumentStack stack = new DocumentStack(
+ TestProvidersAccess.HAMMY, TestEnv.FOLDER_1, TestEnv.FOLDER_2);
+ assertFalse(mManager.drop(mClipData, mManager, stack, mCallback));
+ }
+
+ @Test
+ public void testDrop_DifferentRoot_DropOnDocument() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.HOME,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_2);
+
+ final DocumentStack stack = new DocumentStack(
+ TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1, TestEnv.FOLDER_2);
+ assertTrue(mManager.drop(mClipData, mManager, stack, mCallback));
+
+ mClipper.copy.assertLastArgument(Pair.create(stack, mClipData));
+ mClipper.opType.assertLastArgument(FileOperationService.OPERATION_COPY);
+ }
+
+ @Test
+ public void testDrop_SameRoot_DropOnDocument() {
+ mManager.startDrag(
+ mStartDragView,
+ TestEnv.FOLDER_0,
+ Arrays.asList(TestEnv.FILE_APK, TestEnv.FILE_JPG),
+ TestProvidersAccess.DOWNLOADS,
+ Arrays.asList(TestEnv.FOLDER_0.derivedUri, TestEnv.FILE_APK.derivedUri,
+ TestEnv.FILE_JPG.derivedUri),
+ mIconHelper);
+
+ mManager.updateState(mUpdateShadowView, TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_2);
+
+ final DocumentStack stack = new DocumentStack(
+ TestProvidersAccess.DOWNLOADS, TestEnv.FOLDER_1, TestEnv.FOLDER_2);
+ assertTrue(mManager.drop(mClipData, mManager, stack, mCallback));
+
+ mClipper.copy.assertLastArgument(Pair.create(stack, mClipData));
+ mClipper.opType.assertLastArgument(FileOperationService.OPERATION_MOVE);
+ }
+
+ private void assertStateUpdated(@State int expected) {
+ mShadowBuilder.state.assertLastArgument(expected);
+ mShadowUpdateListener.assertCalled();
+ }
+
+ public static class TestDragShadowBuilder extends DragShadowBuilder {
+
+ public TestEventListener<String> title;
+ public TestEventListener<Drawable> icon;
+ public TestEventListener<Integer> state;
+
+ private TestDragShadowBuilder() {
+ super(null);
+ }
+
+ @Override
+ void updateTitle(String title) {
+ this.title.accept(title);
+ }
+
+ @Override
+ void updateIcon(Drawable icon) {
+ this.icon.accept(icon);
+ }
+
+ @Override
+ void onStateUpdated(@State int state) {
+ this.state.accept(state);
+ }
+
+ public static TestDragShadowBuilder create() {
+ TestDragShadowBuilder builder =
+ Mockito.mock(TestDragShadowBuilder.class, Mockito.CALLS_REAL_METHODS);
+
+ builder.title = new TestEventListener<>();
+ builder.icon = new TestEventListener<>();
+ builder.state = new TestEventListener<>();
+
+ return builder;
+ }
+ }
+}
diff --git a/tests/unit/com/android/documentsui/ItemDragListenerTest.java b/tests/unit/com/android/documentsui/ItemDragListenerTest.java
index 8244e5ce2..c083f9f0c 100644
--- a/tests/unit/com/android/documentsui/ItemDragListenerTest.java
+++ b/tests/unit/com/android/documentsui/ItemDragListenerTest.java
@@ -18,6 +18,7 @@ package com.android.documentsui;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -179,7 +180,7 @@ public class ItemDragListenerTest {
assertSame(dropEvent, mListener.mLastDropEvent);
}
- protected boolean triggerDragEvent(int actionId) {
+ private boolean triggerDragEvent(int actionId) {
final DragEvent testEvent = DragEvents.createTestDragEvent(actionId);
return mListener.onDrag(mTestView, testEvent);
@@ -218,7 +219,7 @@ public class ItemDragListenerTest {
private View mLastExitedView;
@Override
- public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
+ public void setDropTargetHighlight(View v, boolean highlight) {
mHighlightedView = highlight ? v : null;
}
@@ -233,13 +234,16 @@ public class ItemDragListenerTest {
}
@Override
- public void onDragEntered(View v, Object localState) {
+ public void onDragEntered(View v) {
mLastEnteredView = v;
}
@Override
- public void onDragExited(View v, Object localState) {
+ public void onDragExited(View v) {
mLastExitedView = v;
}
+
+ @Override
+ public void onDragEnded() {}
}
}
diff --git a/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
index 722a819d4..f77933489 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragScrollListenerTest.java
@@ -186,24 +186,22 @@ public class DragScrollListenerTest {
private static class TestDragHost implements ItemDragListener.DragHost {
@Override
- public void setDropTargetHighlight(View v, Object localState, boolean highlight) {
- }
+ public void setDropTargetHighlight(View v, boolean highlight) {}
@Override
- public void runOnUiThread(Runnable runnable) {
- }
+ public void runOnUiThread(Runnable runnable) {}
@Override
- public void onViewHovered(View v) {
- }
+ public void onViewHovered(View v) {}
@Override
- public void onDragEntered(View v, Object localState) {
- }
+ public void onDragEntered(View v) {}
@Override
- public void onDragExited(View v, Object localState) {
- }
+ public void onDragExited(View v) {}
+
+ @Override
+ public void onDragEnded() {}
}
private class TestScrollActionDelegate implements ScrollActionDelegate {
diff --git a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
index 76cff7c9b..699c44755 100644
--- a/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
+++ b/tests/unit/com/android/documentsui/dirlist/DragStartListenerTest.java
@@ -16,39 +16,59 @@
package com.android.documentsui.dirlist;
-import android.content.ClipData;
-import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.TestCase.fail;
+
+import android.provider.DocumentsContract;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
import android.view.View;
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.Providers;
import com.android.documentsui.base.State;
import com.android.documentsui.dirlist.DragStartListener.ActiveListener;
-import com.android.documentsui.DragShadowBuilder;
import com.android.documentsui.base.Events.InputEvent;
import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.selection.Selection;
+import com.android.documentsui.testing.TestDragAndDropManager;
import com.android.documentsui.testing.TestEvent;
import com.android.documentsui.testing.SelectionManagers;
import com.android.documentsui.testing.Views;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
import java.util.ArrayList;
+@RunWith(AndroidJUnit4.class)
@SmallTest
-public class DragStartListenerTest extends AndroidTestCase {
+public class DragStartListenerTest {
private ActiveListener mListener;
private TestEvent.Builder mEvent;
private SelectionManager mMultiSelectManager;
private String mViewModelId;
- private boolean mDragStarted;
+ private TestDragAndDropManager mManager;
- @Override
+ @Before
public void setUp() throws Exception {
mMultiSelectManager = SelectionManagers.createTestInstance();
+ mManager = new TestDragAndDropManager();
+
+ DocumentInfo doc = new DocumentInfo();
+ doc.authority = Providers.AUTHORITY_STORAGE;
+ doc.documentId = "id";
+ doc.derivedUri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
+ State state = new State();
+ state.stack.push(doc);
mListener = new DragStartListener.ActiveListener(
- new State(),
+ null, // icon helper
+ state,
mMultiSelectManager,
// view finder
(float x, float y) -> {
@@ -60,30 +80,10 @@ public class DragStartListenerTest extends AndroidTestCase {
},
// docInfo Converter
(Selection selection) -> {
- return new ArrayList<>();
+ return new ArrayList<DocumentInfo>();
},
- // ClipDataFactory
- (Selection selection, int operationType) -> {
- return null;
- },
- // shawdowBuilderFactory
- (Selection selection) -> {
- return null;
- }) {
-
- @Override
- void startDragAndDrop(
- View view,
- ClipData data,
- DragShadowBuilder shadowBuilder,
- Object localState,
- int flags) {
-
- mDragStarted = true;
- }
- };
-
- mDragStarted = false;
+ mManager);
+
mViewModelId = "1234";
mEvent = TestEvent.builder()
@@ -94,17 +94,20 @@ public class DragStartListenerTest extends AndroidTestCase {
.primary();
}
+ @Test
public void testDragStarted_OnMouseMove() {
assertTrue(mListener.onMouseDragEvent(mEvent.build()));
- assertTrue(mDragStarted);
+ mManager.startDragHandler.assertCalled();
}
+ @Test
public void testDragNotStarted_NonModelBackedView() {
mViewModelId = null;
assertFalse(mListener.onMouseDragEvent(mEvent.build()));
- assertFalse(mDragStarted);
+ mManager.startDragHandler.assertNotCalled();
}
+ @Test
public void testThrows_OnNonMouseMove() {
TestEvent e = TestEvent.builder()
.at(1)
@@ -112,18 +115,22 @@ public class DragStartListenerTest extends AndroidTestCase {
assertThrows(e);
}
+ @Test
public void testThrows_OnNonPrimaryMove() {
assertThrows(mEvent.pressButton(MotionEvent.BUTTON_PRIMARY).build());
}
+ @Test
public void testThrows_OnNonMove() {
assertThrows(mEvent.action(MotionEvent.ACTION_UP).build());
}
+ @Test
public void testThrows_WhenNotOnItem() {
assertThrows(mEvent.at(-1).build());
}
+ @Test
public void testDragStart_nonSelectedItem() {
Selection selection = mListener.getSelectionToBeCopied("1234",
mEvent.action(MotionEvent.ACTION_MOVE).build());
@@ -131,6 +138,7 @@ public class DragStartListenerTest extends AndroidTestCase {
assertTrue(selection.contains("1234"));
}
+ @Test
public void testDragStart_selectedItem() {
Selection selection = new Selection();
selection.add("1234");
@@ -144,6 +152,7 @@ public class DragStartListenerTest extends AndroidTestCase {
assertTrue(selection.contains("5678"));
}
+ @Test
public void testDragStart_newNonSelectedItem() {
Selection selection = new Selection();
selection.add("5678");
@@ -157,6 +166,7 @@ public class DragStartListenerTest extends AndroidTestCase {
assertFalse(mMultiSelectManager.hasSelection());
}
+ @Test
public void testCtrlDragStart_newNonSelectedItem() {
Selection selection = new Selection();
selection.add("5678");
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 96694731f..d0737cc5a 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -35,6 +35,7 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Path;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
import android.view.DragEvent;
import com.android.documentsui.R;
@@ -48,8 +49,8 @@ import com.android.documentsui.testing.ClipDatas;
import com.android.documentsui.testing.DocumentStackAsserts;
import com.android.documentsui.testing.Roots;
import com.android.documentsui.testing.TestActivityConfig;
-import com.android.documentsui.testing.TestConfirmationCallback;
import com.android.documentsui.testing.TestDocumentClipper;
+import com.android.documentsui.testing.TestDragAndDropManager;
import com.android.documentsui.testing.TestEnv;
import com.android.documentsui.testing.TestProvidersAccess;
import com.android.documentsui.ui.TestDialogController;
@@ -58,9 +59,7 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.List;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -70,9 +69,9 @@ public class ActionHandlerTest {
private TestActivity mActivity;
private TestActionModeAddons mActionModeAddons;
private TestDialogController mDialogs;
- private TestConfirmationCallback mCallback;
private ActionHandler<TestActivity> mHandler;
private TestDocumentClipper mClipper;
+ private TestDragAndDropManager mDragAndDropManager;
private boolean refreshAnswer = false;
@Before
@@ -81,11 +80,12 @@ public class ActionHandlerTest {
mActivity = TestActivity.create(mEnv);
mActionModeAddons = new TestActionModeAddons();
mDialogs = new TestDialogController();
- mCallback = new TestConfirmationCallback();
+ mClipper = new TestDocumentClipper();
+ mDragAndDropManager = new TestDragAndDropManager();
+
mEnv.roots.configurePm(mActivity.packageMgr);
((TestActivityConfig) mEnv.injector.config).nextDocumentEnabled = true;
mEnv.injector.dialogs = mDialogs;
- mClipper = new TestDocumentClipper();
mHandler = createHandler();
@@ -403,62 +403,21 @@ public class ActionHandlerTest {
}
@Test
- public void testClipper_suppliedCorrectClipData() throws Exception {
+ public void testDragAndDrop_DropsOnWritableRoot() throws Exception {
// DragEvent gets recycled in Android, so it is possible that by the time the callback is
// called, event.getLocalState() and event.getClipData() returns null. This tests to ensure
// our Clipper is getting the original CipData passed in.
- mHandler = new ActionHandler<>(
- mActivity,
- mEnv.state,
- mEnv.roots,
- mEnv.docs,
- mEnv.searchViewManager,
- mEnv::lookupExecutor,
- mActionModeAddons,
- mClipper,
- null,
- mEnv.injector
- );
Object localState = new Object();
ClipData clipData = ClipDatas.createTestClipData();
DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, localState, null, clipData,
null, true);
- assertSame(localState, event.getLocalState());
- assertSame(clipData, event.getClipData());
mHandler.dropOn(event, TestProvidersAccess.DOWNLOADS);
event.recycle();
- mEnv.beforeAsserts();
-
- mClipper.assertSameClipData(clipData);
- }
-
- @Test
- public void testClipper_notCalledIfDestInSelection() throws Exception {
- mHandler = new ActionHandler<>(
- mActivity,
- mEnv.state,
- mEnv.roots,
- mEnv.docs,
- mEnv.searchViewManager,
- mEnv::lookupExecutor,
- mActionModeAddons,
- mClipper,
- null,
- mEnv.injector
- );
- List<DocumentInfo> localState = new ArrayList<>();
- localState.add(mEnv.docs.getRootDocument(TestProvidersAccess.DOWNLOADS));
- ClipData clipData = ClipDatas.createTestClipData();
- DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, localState, null, clipData,
- null, true);
-
- mHandler.dropOn(event, TestProvidersAccess.DOWNLOADS);
-
- mEnv.beforeAsserts();
-
- mClipper.assertNoClipData();
+ Pair<ClipData, RootInfo> actual = mDragAndDropManager.dropOnRootHandler.getLastValue();
+ assertSame(clipData, actual.first);
+ assertSame(TestProvidersAccess.DOWNLOADS, actual.second);
}
@Test
@@ -514,8 +473,9 @@ public class ActionHandlerTest {
mEnv.searchViewManager,
mEnv::lookupExecutor,
mActionModeAddons,
- new TestDocumentClipper(),
+ mClipper,
null, // clip storage, not utilized unless we venture into *jumbo* clip territory.
+ mDragAndDropManager,
mEnv.injector
);
}