From da2c0f0b075ad9f770182e706c2ec158989568a7 Mon Sep 17 00:00:00 2001 From: Garfield Tan Date: Tue, 11 Apr 2017 13:47:58 -0700 Subject: 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 --- res/drawable/drop_badge_states.xml | 20 +- res/drawable/ic_drop_copy_badge.xml | 37 ++ res/drawable/ic_drop_not_ok_badge.xml | 38 -- res/drawable/ic_drop_ok_badge.xml | 37 -- res/drawable/ic_reject_drop_badge.xml | 38 ++ res/values/attrs.xml | 6 +- src/com/android/documentsui/AbstractDragHost.java | 46 ++ src/com/android/documentsui/BaseActivity.java | 8 +- .../android/documentsui/DocumentsApplication.java | 7 + src/com/android/documentsui/DragAndDropHelper.java | 54 -- .../android/documentsui/DragAndDropManager.java | 467 +++++++++++++ src/com/android/documentsui/DragShadowBuilder.java | 83 +-- src/com/android/documentsui/DrawerController.java | 11 +- src/com/android/documentsui/DropBadgeView.java | 38 +- .../android/documentsui/HorizontalBreadcrumb.java | 11 +- src/com/android/documentsui/Injector.java | 4 +- src/com/android/documentsui/ItemDragListener.java | 30 +- src/com/android/documentsui/base/Shared.java | 3 +- .../documentsui/clipping/DocumentClipper.java | 59 +- .../clipping/RuntimeDocumentClipper.java | 87 +-- .../documentsui/dirlist/DirectoryDragListener.java | 4 +- .../documentsui/dirlist/DirectoryFragment.java | 11 +- .../documentsui/dirlist/DocumentHolder.java | 1 - src/com/android/documentsui/dirlist/DragHost.java | 79 +-- .../documentsui/dirlist/DragStartListener.java | 100 +-- .../android/documentsui/files/ActionHandler.java | 22 +- .../android/documentsui/files/FilesActivity.java | 9 +- src/com/android/documentsui/sidebar/DragHost.java | 50 +- .../android/documentsui/sidebar/RootsFragment.java | 2 +- .../com/android/documentsui/testing/ClipDatas.java | 9 + .../com/android/documentsui/testing/KeyEvents.java | 52 ++ .../documentsui/testing/TestActionHandler.java | 10 + .../documentsui/testing/TestDocumentClipper.java | 49 +- .../testing/TestDragAndDropManager.java | 83 +++ .../documentsui/testing/TestIconHelper.java | 43 ++ .../android/documentsui/testing/TestResources.java | 18 + .../documentsui/ui/TestDialogController.java | 12 +- .../documentsui/DragAndDropManagerTests.java | 732 +++++++++++++++++++++ .../android/documentsui/ItemDragListenerTest.java | 12 +- .../dirlist/DragScrollListenerTest.java | 18 +- .../documentsui/dirlist/DragStartListenerTest.java | 76 ++- .../documentsui/files/ActionHandlerTest.java | 64 +- 42 files changed, 1898 insertions(+), 642 deletions(-) create mode 100644 res/drawable/ic_drop_copy_badge.xml delete mode 100644 res/drawable/ic_drop_not_ok_badge.xml delete mode 100644 res/drawable/ic_drop_ok_badge.xml create mode 100644 res/drawable/ic_reject_drop_badge.xml create mode 100644 src/com/android/documentsui/AbstractDragHost.java delete mode 100644 src/com/android/documentsui/DragAndDropHelper.java create mode 100644 src/com/android/documentsui/DragAndDropManager.java create mode 100644 tests/common/com/android/documentsui/testing/KeyEvents.java create mode 100644 tests/common/com/android/documentsui/testing/TestDragAndDropManager.java create mode 100644 tests/common/com/android/documentsui/testing/TestIconHelper.java create mode 100644 tests/unit/com/android/documentsui/DragAndDropManagerTests.java 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 @@ + + + app:state_reject_drop="true" + android:drawable="@drawable/ic_reject_drop_badge"/> + + + app:state_reject_drop="false" + app:state_copy="true" + android:drawable="@drawable/ic_drop_copy_badge"/> + + \ No newline at end of file diff --git a/res/drawable/ic_drop_copy_badge.xml b/res/drawable/ic_drop_copy_badge.xml new file mode 100644 index 000000000..7f1be3151 --- /dev/null +++ b/res/drawable/ic_drop_copy_badge.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/res/drawable/ic_drop_not_ok_badge.xml b/res/drawable/ic_drop_not_ok_badge.xml deleted file mode 100644 index 402aff8f8..000000000 --- a/res/drawable/ic_drop_not_ok_badge.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - diff --git a/res/drawable/ic_drop_ok_badge.xml b/res/drawable/ic_drop_ok_badge.xml deleted file mode 100644 index 7f1be3151..000000000 --- a/res/drawable/ic_drop_ok_badge.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - diff --git a/res/drawable/ic_reject_drop_badge.xml b/res/drawable/ic_reject_drop_badge.xml new file mode 100644 index 000000000..402aff8f8 --- /dev/null +++ b/res/drawable/ic_reject_drop_badge.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + 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 @@ - - - + + + 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 srcs, + RootInfo root, + List 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 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 srcs, + RootInfo root, + List invalidDest, + IconHelper iconHelper) { + + mView = v; + mInvalidDest = invalidDest; + + List 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 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 { - - 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 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 } @@ -156,6 +156,11 @@ public abstract class DrawerController implements DrawerListener { setOpen(true); } + @Override + public void onDragEnded() { + // do nothing + } + @Override public void setOpen(boolean open) { if (open) { 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 { public DialogController dialogs; public SearchViewManager searchManager; + public final DebugHelper debugHelper; + @ContentScoped public ActionModeController actionModeController; @@ -67,8 +69,6 @@ public class Injector { 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 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 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 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 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 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 uriBuilder, Selection selection, @OpType int opType); + ClipData getClipDataForDocuments(Function 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 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 uris = new ArrayList<>(selection.size()); + for (String id : selection) { + uris.add(uriBuilder.apply(id)); + } + return getClipDataForDocuments(uris, opType); + } + + @Override + public ClipData getClipDataForDocuments( + List 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 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 uriBuilder, Selection selection, @OpType int opType) { + private ClipData createStandardClipData(List 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 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 uriBuilder, Selection selection, @OpType int opType) { + private ClipData createJumboClipData(List 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 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 clipItems = new ArrayList<>(capacity); // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT final ContentResolver resolver = mContext.getContentResolver(); final Set 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> { + DirectoryDragListener(com.android.documentsui.dirlist.DragHost host) { super(host); } @@ -54,7 +56,7 @@ class DirectoryDragListener extends ItemDragListener> { @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 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 - implements ItemDragListener.DragHost { +class DragHost 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 private final Predicate mIsDocumentView; private final Lookup mHolderLookup; private final Lookup mDestinationLookup; - private final DocumentClipper mClipper; DragHost( T activity, - DragShadowBuilder shadowBuilder, + DragAndDropManager dragAndDropManager, SelectionManager selectionMgr, ActionHandler actions, State state, DialogController dialogs, Predicate isDocumentView, Lookup holderLookup, - Lookup destinationLookup, - DocumentClipper clipper) { + Lookup destinationLookup) { + super(dragAndDropManager); + mActivity = activity; - mShadowBuilder = shadowBuilder; mSelectionMgr = selectionMgr; mActions = actions; mState = state; @@ -74,7 +68,6 @@ class DragHost mIsDocumentView = isDocumentView; mHolderLookup = holderLookup; mDestinationLookup = destinationLookup; - mClipper = clipper; } void dragStopped(boolean result) { @@ -89,7 +82,7 @@ class DragHost } @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 if (!highlight) { holder.resetDropHighlight(); } else { - holder.setDroppableHighlight(canCopyTo(localState, v)); + holder.setDroppableHighlight(canSpringOpen(v)); } } } @@ -113,16 +106,14 @@ class DragHost } @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 } } + 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 mIdFinder; - private final ClipDataFactory mClipFactory; - private final Function mShadowFactory; - private Function> mDocsConverter; + private final Function> mDocsConverter; + private final DragAndDropManager mDragAndDropManager; // use DragStartListener.create @VisibleForTesting public ActiveListener( + IconHelper iconHelper, State state, SelectionManager selectionMgr, ViewFinder viewFinder, Function idFinder, Function> docsConverter, - ClipDataFactory clipFactory, - Function 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 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 srcs = mDocsConverter.apply(selection); + + final DocumentInfo parent = mState.stack.peek(); + final List 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 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 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 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 extends AbstractActionHa mDialogs = injector.dialogs; mClipper = clipper; mClipStore = clipStore; + mDragAndDropManager = dragAndDropManager; mModel = injector.getModel(); } @@ -121,21 +125,9 @@ public class ActionHandler 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> 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; @@ -350,11 +348,6 @@ public class FilesActivity extends BaseActivity implements ActionHandler.Addons || super.onKeyDown(keyCode, event); } - @Override - public DragShadowBuilder getShadowBuilder() { - return mShadowBuilder; - } - @Override public boolean onKeyShortcut(int keyCode, KeyEvent event) { DirectoryFragment dir; 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 mDestinationLookup; private final ActionHandler mActions; DragHost( Activity activity, - DragShadowBuilder shadowBuilder, + DragAndDropManager dragAndDropManager, Lookup 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(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 { public final TestEventHandler open = new TestEventHandler<>(); public boolean mDeleteHappened; + public DocumentInfo nextRootDocument; + public TestActionHandler() { this(TestEnv.create()); } @@ -69,4 +74,9 @@ public class TestActionHandler extends AbstractActionHandler { protected void launchToDefaultLocation() { throw new UnsupportedOperationException(); } + + @Override + public void getRootDocument(RootInfo root, int timeout, Consumer 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> copy = new TestEventListener<>(); + public final TestEventListener opType = new TestEventListener<>(); @Override public boolean hasItemsToPaste() { @@ -43,12 +47,17 @@ public class TestDocumentClipper implements DocumentClipper { @Override public ClipData getClipDataForDocuments(Function uriBuilder, Selection selection, int opType) { - return null; + return nextClip; } @Override - public void clipDocumentsForCopy(Function uriBuilder, Selection selection) { + public ClipData getClipDataForDocuments(List uris, + @FileOperationService.OpType int opType, DocumentInfo parent) { + return nextClip; + } + @Override + public void clipDocumentsForCopy(Function 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> startDragHandler = new TestEventListener<>(); + public final TestEventHandler> dropOnRootHandler = + new TestEventHandler<>(); + public final TestEventHandler> dropOnDocumentHandler = + new TestEventHandler<>(); + + @Override + public void onKeyEvent(KeyEvent event) {} + + @Override + public void startDrag(View v, DocumentInfo parent, List srcs, RootInfo root, + List 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 strings; + public SparseArray 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 mStartDragListener; + private TestEventListener mShadowUpdateListener; + + private TestEventListener 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 title; + public TestEventListener icon; + public TestEventListener 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(); }, - // 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 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 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 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 ); } -- cgit v1.2.3-59-g8ed1b