Enable Ctrl+X cut operations, along with some code refactor.
Bug: 27451823
Change-Id: I062dcbd065434c22a3ffeb33d4cac2b4f9da104b
(cherry picked from commit 5b696f91aa0f12f29be5205647bdc398e2831af8)
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
index 059b5e0..908396c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
@@ -23,11 +23,14 @@
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
+import android.os.PersistableBundle;
import android.provider.DocumentsContract;
import android.support.annotation.Nullable;
import android.util.Log;
import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
import libcore.io.IoUtils;
@@ -42,6 +45,8 @@
public final class DocumentClipper {
private static final String TAG = "DocumentClipper";
+ private static final String SRC_PARENT_KEY = "srcParent";
+ private static final String OP_TYPE_KEY = "opType";
private Context mContext;
private ClipboardManager mClipboard;
@@ -73,47 +78,41 @@
}
/**
- * Returns a list of Documents as decoded from Clipboard primary clipdata.
- * This should be run from inside an AsyncTask.
+ * Returns details regarding the documents on the primary clipboard
*/
- public List<DocumentInfo> getClippedDocuments() {
- ClipData data = mClipboard.getPrimaryClip();
- return data == null ? Collections.EMPTY_LIST : getDocumentsFromClipData(data);
+ public ClipDetails getClipDetails() {
+ return getClipDetails(mClipboard.getPrimaryClip());
}
- /**
- * Returns a list of Documents as decoded in clipData.
- * This should be run from inside an AsyncTask.
- */
- public List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
+ public ClipDetails getClipDetails(@Nullable ClipData clipData) {
+ if (clipData == null) {
+ return null;
+ }
+
+ String srcParent = clipData.getDescription().getExtras().getString(SRC_PARENT_KEY);
+
+ ClipDetails clipDetails = new ClipDetails(
+ clipData.getDescription().getExtras().getInt(OP_TYPE_KEY),
+ getDocumentsFromClipData(clipData),
+ createDocument((srcParent != null) ? Uri.parse(srcParent) : null));
+
+ return clipDetails;
+ }
+
+ private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
assert(clipData != null);
- final List<DocumentInfo> srcDocs = new ArrayList<>();
int count = clipData.getItemCount();
if (count == 0) {
- return srcDocs;
+ return Collections.EMPTY_LIST;
}
- ContentResolver resolver = mContext.getContentResolver();
+ final List<DocumentInfo> srcDocs = new ArrayList<>();
+
for (int i = 0; i < count; ++i) {
ClipData.Item item = clipData.getItemAt(i);
Uri itemUri = item.getUri();
- if (itemUri != null && DocumentsContract.isDocumentUri(mContext, itemUri)) {
- ContentProviderClient client = null;
- Cursor cursor = null;
- try {
- client = DocumentsApplication.acquireUnstableProviderOrThrow(
- resolver, itemUri.getAuthority());
- cursor = client.query(itemUri, null, null, null, null);
- cursor.moveToPosition(0);
- srcDocs.add(DocumentInfo.fromCursor(cursor, itemUri.getAuthority()));
- } catch (Exception e) {
- Log.e(TAG, e.getMessage());
- } finally {
- IoUtils.closeQuietly(cursor);
- ContentProviderClient.releaseQuietly(client);
- }
- }
+ srcDocs.add(createDocument(itemUri));
}
return srcDocs;
@@ -123,26 +122,86 @@
* Returns ClipData representing the list of docs, or null if docs is empty,
* or docs cannot be converted.
*/
- public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs) {
+ public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs, @OpType int opType) {
final ContentResolver resolver = mContext.getContentResolver();
ClipData clipData = null;
for (DocumentInfo doc : docs) {
- final Uri uri = DocumentsContract.buildDocumentUri(doc.authority, doc.documentId);
+ assert(doc != null);
+ assert(doc.derivedUri != null);
if (clipData == null) {
// TODO: figure out what this string should be.
// Currently it is not displayed anywhere in the UI, but this might change.
- final String label = "";
- clipData = ClipData.newUri(resolver, label, uri);
+ final String clipLabel = "";
+ clipData = ClipData.newUri(resolver, clipLabel, doc.derivedUri);
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putInt(OP_TYPE_KEY, opType);
+ clipData.getDescription().setExtras(bundle);
} else {
// TODO: update list of mime types in ClipData.
- clipData.addItem(new ClipData.Item(uri));
+ clipData.addItem(new ClipData.Item(doc.derivedUri));
}
}
return clipData;
}
- public void clipDocuments(List<DocumentInfo> docs) {
- ClipData data = getClipDataForDocuments(docs);
+ /**
+ * Puts {@code ClipData} in a primary clipboard, describing a copy operation
+ */
+ public void clipDocumentsForCopy(List<DocumentInfo> docs) {
+ ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_COPY);
+ assert(data != null);
+
mClipboard.setPrimaryClip(data);
}
+
+ /**
+ * Puts {@Code ClipData} in a primary clipboard, describing a cut operation
+ */
+ public void clipDocumentsForCut(List<DocumentInfo> docs, DocumentInfo srcParent) {
+ assert(docs != null);
+ assert(!docs.isEmpty());
+ assert(srcParent != null);
+ assert(srcParent.derivedUri != null);
+
+ ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_MOVE);
+ assert(data != null);
+
+ PersistableBundle bundle = data.getDescription().getExtras();
+ bundle.putString(SRC_PARENT_KEY, srcParent.derivedUri.toString());
+
+ mClipboard.setPrimaryClip(data);
+ }
+
+ private DocumentInfo createDocument(Uri uri) {
+ DocumentInfo doc = null;
+ if (uri != null && DocumentsContract.isDocumentUri(mContext, uri)) {
+ ContentResolver resolver = mContext.getContentResolver();
+ ContentProviderClient client = null;
+ Cursor cursor = null;
+ try {
+ client = DocumentsApplication.acquireUnstableProviderOrThrow(resolver, uri.getAuthority());
+ cursor = client.query(uri, null, null, null, null);
+ cursor.moveToPosition(0);
+ doc = DocumentInfo.fromCursor(cursor, uri.getAuthority());
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage());
+ } finally {
+ IoUtils.closeQuietly(cursor);
+ ContentProviderClient.releaseQuietly(client);
+ }
+ }
+ return doc;
+ }
+
+ public static class ClipDetails {
+ public final @OpType int opType;
+ public final List<DocumentInfo> docs;
+ public final @Nullable DocumentInfo parent;
+
+ ClipDetails(@OpType int opType, List<DocumentInfo> docs, @Nullable DocumentInfo parent) {
+ this.opType = opType;
+ this.docs = docs;
+ this.parent = parent;
+ }
+ }
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index 84fc6fe..f067d5f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -359,6 +359,12 @@
dir.selectAllFiles();
}
return true;
+ case KeyEvent.KEYCODE_X:
+ dir = getDirectoryFragment();
+ if (dir != null) {
+ dir.cutSelectedToClipboard();
+ }
+ return true;
case KeyEvent.KEYCODE_C:
dir = getDirectoryFragment();
if (dir != null) {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
index 69a6e1f..79123d0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Metrics.java
@@ -243,6 +243,7 @@
public static final int USER_ACTION_COPY_CLIPBOARD = 23;
public static final int USER_ACTION_DRAG_N_DROP = 24;
public static final int USER_ACTION_DRAG_N_DROP_MULTI_WINDOW = 25;
+ public static final int USER_ACTION_CUT_CLIPBOARD = 26;
@IntDef(flag = false, value = {
USER_ACTION_OTHER,
@@ -269,7 +270,8 @@
USER_ACTION_PASTE_CLIPBOARD,
USER_ACTION_COPY_CLIPBOARD,
USER_ACTION_DRAG_N_DROP,
- USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+ USER_ACTION_DRAG_N_DROP_MULTI_WINDOW,
+ USER_ACTION_CUT_CLIPBOARD
})
@Retention(RetentionPolicy.SOURCE)
public @interface UserAction {}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 297fbc7..fab2494 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -48,6 +48,7 @@
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.os.PersistableBundle;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.support.annotation.Nullable;
@@ -82,6 +83,7 @@
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentClipper;
+import com.android.documentsui.DocumentClipper.ClipDetails;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events;
@@ -1000,19 +1002,24 @@
return commonType[0] + "/" + commonType[1];
}
- private void copyFromClipboard() {
- new AsyncTask<Void, Void, List<DocumentInfo>>() {
+ private void copyFromClipboard(final DocumentInfo destination) {
+ new AsyncTask<Void, Void, ClipDetails>() {
@Override
- protected List<DocumentInfo> doInBackground(Void... params) {
- return mClipper.getClippedDocuments();
+ protected ClipDetails doInBackground(Void... params) {
+ return mClipper.getClipDetails();
}
@Override
- protected void onPostExecute(List<DocumentInfo> docs) {
- DocumentInfo destination =
- ((BaseActivity) getActivity()).getCurrentDirectory();
- copyDocuments(docs, destination);
+ protected void onPostExecute(ClipDetails clipDetails) {
+ if (clipDetails == null) {
+ Log.w(TAG, "Received null clipDetails from primary clipboard. Ignoring.");
+ return;
+ }
+ List<DocumentInfo> docs = clipDetails.docs;
+ @OpType int type = clipDetails.opType;
+ DocumentInfo srcParent = clipDetails.parent;
+ moveDocuments(docs, destination, type, srcParent);
}
}.execute();
}
@@ -1020,21 +1027,35 @@
private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) {
assert(clipData != null);
- new AsyncTask<Void, Void, List<DocumentInfo>>() {
+ new AsyncTask<Void, Void, ClipDetails>() {
@Override
- protected List<DocumentInfo> doInBackground(Void... params) {
- return mClipper.getDocumentsFromClipData(clipData);
+ protected ClipDetails doInBackground(Void... params) {
+ return mClipper.getClipDetails(clipData);
}
@Override
- protected void onPostExecute(List<DocumentInfo> docs) {
- copyDocuments(docs, destination);
+ protected void onPostExecute(ClipDetails clipDetails) {
+ if (clipDetails == null) {
+ Log.w(TAG, "Received null clipDetails. Ignoring.");
+ return;
+ }
+
+ List<DocumentInfo> docs = clipDetails.docs;
+ @OpType int type = clipDetails.opType;
+ DocumentInfo srcParent = clipDetails.parent;
+ moveDocuments(docs, destination, type, srcParent);
}
}.execute();
}
- private void copyDocuments(final List<DocumentInfo> docs, final DocumentInfo destination) {
+ /**
+ * Moves {@code docs} from {@code srcParent} to {@code destination}.
+ * operationType can be copy or cut
+ * srcParent Must be non-null for move operations.
+ */
+ private void moveDocuments(final List<DocumentInfo> docs, final DocumentInfo destination,
+ final @OpType int operationType, final DocumentInfo srcParent) {
BaseActivity activity = (BaseActivity) getActivity();
if (!canCopy(docs, activity.getCurrentRoot(), destination)) {
Snackbars.makeSnackbar(
@@ -1050,33 +1071,37 @@
}
final DocumentStack curStack = getDisplayState().stack;
- DocumentStack tmpStack = new DocumentStack();
+ DocumentStack dstStack = new DocumentStack();
if (destination != null) {
- tmpStack.push(destination);
- tmpStack.addAll(curStack);
+ dstStack.push(destination);
+ dstStack.addAll(curStack);
} else {
- tmpStack = curStack;
+ dstStack = curStack;
}
-
- FileOperations.copy(getActivity(), docs, tmpStack);
+ switch (operationType) {
+ case FileOperationService.OPERATION_MOVE:
+ FileOperations.move(getActivity(), docs, srcParent, dstStack);
+ break;
+ case FileOperationService.OPERATION_COPY:
+ FileOperations.copy(getActivity(), docs, dstStack);
+ break;
+ default:
+ throw new UnsupportedOperationException("Unsupported operation: " + operationType);
+ }
}
public void copySelectedToClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD);
Selection selection = mSelectionManager.getSelection(new Selection());
- if (!selection.isEmpty()) {
- copySelectionToClipboard(selection);
- mSelectionManager.clearSelection();
+ if (selection.isEmpty()) {
+ return;
}
- }
- void copySelectionToClipboard(Selection selection) {
- assert(!selection.isEmpty());
new GetDocumentsTask() {
@Override
void onDocumentsReady(List<DocumentInfo> docs) {
- mClipper.clipDocuments(docs);
+ mClipper.clipDocumentsForCopy(docs);
Activity activity = getActivity();
Snackbars.makeSnackbar(activity,
activity.getResources().getQuantityString(
@@ -1084,12 +1109,37 @@
Snackbar.LENGTH_SHORT).show();
}
}.execute(selection);
+ mSelectionManager.clearSelection();
+ }
+
+ public void cutSelectedToClipboard() {
+ Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD);
+ Selection selection = mSelectionManager.getSelection(new Selection());
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ new GetDocumentsTask() {
+ @Override
+ void onDocumentsReady(List<DocumentInfo> docs) {
+ // We need the srcParent for move operations because we do a copy / delete
+ DocumentInfo currentDoc = getDisplayState().stack.peek();
+ mClipper.clipDocumentsForCut(docs, currentDoc);
+ Activity activity = getActivity();
+ Snackbars.makeSnackbar(activity,
+ activity.getResources().getQuantityString(
+ R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
+ Snackbar.LENGTH_SHORT).show();
+ }
+ }.execute(selection);
+ mSelectionManager.clearSelection();
}
public void pasteFromClipboard() {
Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD);
- copyFromClipboard();
+ DocumentInfo destination = ((BaseActivity) getActivity()).getCurrentDirectory();
+ copyFromClipboard(destination);
getActivity().invalidateOptionsMenu();
}
@@ -1198,6 +1248,8 @@
return true;
case DragEvent.ACTION_DRAG_ENDED:
+ // After a drop event, always stop highlighting the target.
+ setDropTargetHighlight(v, false);
if (event.getResult()) {
// Exit selection mode if the drop was handled.
mSelectionManager.clearSelection();
@@ -1205,40 +1257,45 @@
return true;
case DragEvent.ACTION_DROP:
- // After a drop event, always stop highlighting the target.
- setDropTargetHighlight(v, false);
-
- ClipData clipData = event.getClipData();
- if (clipData == null) {
- Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
- return false;
- }
-
- // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
- // multi-window drag, because localState isn't carried over from one process to
- // another.
- Object src = event.getLocalState();
- DocumentInfo dst = getDestination(v);
- if (Objects.equals(src, dst)) {
- if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
- 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(getContext(),
- src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
- : Metrics.USER_ACTION_DRAG_N_DROP);
-
- copyFromClipData(clipData, dst);
- return true;
+ return handleDropEvent(v, event);
}
return false;
}
+ private boolean handleDropEvent(View v, DragEvent event) {
+
+ ClipData clipData = event.getClipData();
+ if (clipData == null) {
+ Log.w(TAG, "Received invalid drop event with null clipdata. Ignoring.");
+ return false;
+ }
+
+ ClipDetails clipDetails = mClipper.getClipDetails(clipData);
+ assert(clipDetails.opType == FileOperationService.OPERATION_COPY);
+
+ // Don't copy from the cwd into the cwd. Note: this currently doesn't work for
+ // multi-window drag, because localState isn't carried over from one process to
+ // another.
+ Object src = event.getLocalState();
+ DocumentInfo dst = getDestination(v);
+ if (Objects.equals(src, dst)) {
+ if (DEBUG) Log.d(TAG, "Drop target same as source. Ignoring.");
+ 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(getContext(),
+ src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW
+ : Metrics.USER_ACTION_DRAG_N_DROP);
+
+ copyFromClipData(clipData, dst);
+ return true;
+ }
+
private DocumentInfo getDestination(View v) {
String id = getModelId(v);
if (id != null) {
@@ -1552,7 +1609,8 @@
return false;
}
v.startDragAndDrop(
- mClipper.getClipDataForDocuments(docs),
+ mClipper.getClipDataForDocuments(docs,
+ FileOperationService.OPERATION_COPY),
new DragShadowBuilder(getActivity(), mIconHelper, docs),
getDisplayState().stack.peek(),
View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
index 69f0e67..c9b9ff2 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java
@@ -138,6 +138,50 @@
bots.directory.assertDocumentsAbsent("file1.png");
}
+ public void testKeyboard_CutDocument() throws Exception {
+ initTestFiles();
+
+ bots.roots.openRoot(ROOT_0_ID);
+
+ bots.directory.clickDocument("file1.png");
+ device.waitForIdle();
+ bots.main.pressKey(KeyEvent.KEYCODE_X, KeyEvent.META_CTRL_ON);
+
+ device.waitForIdle();
+
+ bots.roots.openRoot(ROOT_1_ID);
+ bots.main.pressKey(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON);
+
+ device.waitForIdle();
+
+ bots.directory.assertDocumentsPresent("file1.png");
+
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.directory.assertDocumentsAbsent("file1.png");
+ }
+
+ public void testKeyboard_CopyDocument() throws Exception {
+ initTestFiles();
+
+ bots.roots.openRoot(ROOT_0_ID);
+
+ bots.directory.clickDocument("file1.png");
+ device.waitForIdle();
+ bots.main.pressKey(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON);
+
+ device.waitForIdle();
+
+ bots.roots.openRoot(ROOT_1_ID);
+ bots.main.pressKey(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON);
+
+ device.waitForIdle();
+
+ bots.directory.assertDocumentsPresent("file1.png");
+
+ bots.roots.openRoot(ROOT_0_ID);
+ bots.directory.assertDocumentsPresent("file1.png");
+ }
+
public void testDeleteDocument_Cancel() throws Exception {
initTestFiles();
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java
index 4c8dc00..b099d10 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/bots/UiBot.java
@@ -224,4 +224,8 @@
public void pressKey(int keyCode) {
mDevice.pressKeyCode(keyCode);
}
+
+ public void pressKey(int keyCode, int metaState) {
+ mDevice.pressKeyCode(keyCode, metaState);
+ }
}