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