Loading packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java +95 −36 Original line number Diff line number Diff line Loading @@ -23,11 +23,14 @@ import android.content.ContentResolver; 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; Loading @@ -42,6 +45,8 @@ import java.util.List; 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; Loading Loading @@ -73,47 +78,41 @@ public final class DocumentClipper { } /** * 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; Loading @@ -123,26 +122,86 @@ public final class DocumentClipper { * 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; } } } packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java +6 −0 Original line number Diff line number Diff line Loading @@ -359,6 +359,12 @@ public class FilesActivity extends BaseActivity { 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) { Loading packages/DocumentsUI/src/com/android/documentsui/Metrics.java +3 −1 Original line number Diff line number Diff line Loading @@ -243,6 +243,7 @@ public final class Metrics { 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, Loading @@ -269,7 +270,8 @@ public final class Metrics { 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 {} Loading packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +113 −55 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.os.AsyncTask; 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; Loading Loading @@ -82,6 +83,7 @@ import com.android.documentsui.BaseActivity; 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; Loading Loading @@ -1000,19 +1002,24 @@ public class DirectoryFragment extends Fragment 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(); } Loading @@ -1020,21 +1027,35 @@ public class DirectoryFragment extends Fragment 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( Loading @@ -1050,33 +1071,60 @@ public class DirectoryFragment extends Fragment } 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; } 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); } FileOperations.copy(getActivity(), docs, tmpStack); } public void copySelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD); Selection selection = mSelectionManager.getSelection(new Selection()); if (!selection.isEmpty()) { copySelectionToClipboard(selection); if (selection.isEmpty()) { return; } new GetDocumentsTask() { @Override void onDocumentsReady(List<DocumentInfo> docs) { mClipper.clipDocumentsForCopy(docs); 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 cutSelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD); Selection selection = mSelectionManager.getSelection(new Selection()); if (selection.isEmpty()) { return; } void copySelectionToClipboard(Selection selection) { assert(!selection.isEmpty()); new GetDocumentsTask() { @Override void onDocumentsReady(List<DocumentInfo> docs) { mClipper.clipDocuments(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( Loading @@ -1084,12 +1132,14 @@ public class DirectoryFragment extends Fragment 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(); } Loading Loading @@ -1198,6 +1248,8 @@ public class DirectoryFragment extends Fragment 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(); Loading @@ -1205,8 +1257,12 @@ public class DirectoryFragment extends Fragment return true; case DragEvent.ACTION_DROP: // After a drop event, always stop highlighting the target. setDropTargetHighlight(v, false); return handleDropEvent(v, event); } return false; } private boolean handleDropEvent(View v, DragEvent event) { ClipData clipData = event.getClipData(); if (clipData == null) { Loading @@ -1214,6 +1270,9 @@ public class DirectoryFragment extends Fragment 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. Loading @@ -1236,8 +1295,6 @@ public class DirectoryFragment extends Fragment copyFromClipData(clipData, dst); return true; } return false; } private DocumentInfo getDestination(View v) { String id = getModelId(v); Loading Loading @@ -1552,7 +1609,8 @@ public class DirectoryFragment extends Fragment 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 | Loading packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java +44 −0 Original line number Diff line number Diff line Loading @@ -138,6 +138,50 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { 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(); Loading Loading
packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java +95 −36 Original line number Diff line number Diff line Loading @@ -23,11 +23,14 @@ import android.content.ContentResolver; 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; Loading @@ -42,6 +45,8 @@ import java.util.List; 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; Loading Loading @@ -73,47 +78,41 @@ public final class DocumentClipper { } /** * 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; Loading @@ -123,26 +122,86 @@ public final class DocumentClipper { * 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; } } }
packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java +6 −0 Original line number Diff line number Diff line Loading @@ -359,6 +359,12 @@ public class FilesActivity extends BaseActivity { 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) { Loading
packages/DocumentsUI/src/com/android/documentsui/Metrics.java +3 −1 Original line number Diff line number Diff line Loading @@ -243,6 +243,7 @@ public final class Metrics { 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, Loading @@ -269,7 +270,8 @@ public final class Metrics { 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 {} Loading
packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +113 −55 Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ import android.os.AsyncTask; 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; Loading Loading @@ -82,6 +83,7 @@ import com.android.documentsui.BaseActivity; 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; Loading Loading @@ -1000,19 +1002,24 @@ public class DirectoryFragment extends Fragment 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(); } Loading @@ -1020,21 +1027,35 @@ public class DirectoryFragment extends Fragment 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( Loading @@ -1050,33 +1071,60 @@ public class DirectoryFragment extends Fragment } 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; } 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); } FileOperations.copy(getActivity(), docs, tmpStack); } public void copySelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD); Selection selection = mSelectionManager.getSelection(new Selection()); if (!selection.isEmpty()) { copySelectionToClipboard(selection); if (selection.isEmpty()) { return; } new GetDocumentsTask() { @Override void onDocumentsReady(List<DocumentInfo> docs) { mClipper.clipDocumentsForCopy(docs); 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 cutSelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD); Selection selection = mSelectionManager.getSelection(new Selection()); if (selection.isEmpty()) { return; } void copySelectionToClipboard(Selection selection) { assert(!selection.isEmpty()); new GetDocumentsTask() { @Override void onDocumentsReady(List<DocumentInfo> docs) { mClipper.clipDocuments(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( Loading @@ -1084,12 +1132,14 @@ public class DirectoryFragment extends Fragment 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(); } Loading Loading @@ -1198,6 +1248,8 @@ public class DirectoryFragment extends Fragment 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(); Loading @@ -1205,8 +1257,12 @@ public class DirectoryFragment extends Fragment return true; case DragEvent.ACTION_DROP: // After a drop event, always stop highlighting the target. setDropTargetHighlight(v, false); return handleDropEvent(v, event); } return false; } private boolean handleDropEvent(View v, DragEvent event) { ClipData clipData = event.getClipData(); if (clipData == null) { Loading @@ -1214,6 +1270,9 @@ public class DirectoryFragment extends Fragment 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. Loading @@ -1236,8 +1295,6 @@ public class DirectoryFragment extends Fragment copyFromClipData(clipData, dst); return true; } return false; } private DocumentInfo getDestination(View v) { String id = getModelId(v); Loading Loading @@ -1552,7 +1609,8 @@ public class DirectoryFragment extends Fragment 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 | Loading
packages/DocumentsUI/tests/src/com/android/documentsui/FilesActivityUiTest.java +44 −0 Original line number Diff line number Diff line Loading @@ -138,6 +138,50 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { 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(); Loading