From 6d0b46eb12316f5204919852988169759fbcb969 Mon Sep 17 00:00:00 2001 From: "Garfield, Tan" Date: Thu, 12 May 2016 14:12:18 -0700 Subject: [PATCH] Separate paste code from DirectoryFragment. Also untie DocumentClipper from activities. We'll have a single DocumentClipper across the app from now on. Bug: 28742120 Change-Id: Ib9e2537c3499ba2fa0a203ddad3ae71246d5b009 (cherry picked from commit 0cd92d35c533c16b301bd228cce8c68a289407a1) --- .../android/documentsui/DocumentClipper.java | 142 ++++++++++++++-- .../documentsui/DocumentsApplication.java | 8 + .../android/documentsui/FilesActivity.java | 2 +- .../com/android/documentsui/Snackbars.java | 18 +- .../dirlist/DirectoryFragment.java | 156 ++++-------------- .../documentsui/services/FileOperations.java | 76 +++++---- 6 files changed, 230 insertions(+), 172 deletions(-) diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java index 51c47a468df1a..cc9ab978295a2 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java @@ -18,21 +18,21 @@ package com.android.documentsui; import android.content.ClipData; import android.content.ClipboardManager; -import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Context; -import android.database.Cursor; import android.net.Uri; +import android.os.AsyncTask; 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.model.DocumentStack; +import com.android.documentsui.model.RootInfo; import com.android.documentsui.services.FileOperationService; import com.android.documentsui.services.FileOperationService.OpType; - -import libcore.io.IoUtils; +import com.android.documentsui.services.FileOperations; import java.util.ArrayList; import java.util.Arrays; @@ -53,7 +53,7 @@ public final class DocumentClipper { private Context mContext; private ClipboardManager mClipboard; - public DocumentClipper(Context context) { + DocumentClipper(Context context) { mContext = context; mClipboard = context.getSystemService(ClipboardManager.class); } @@ -114,7 +114,13 @@ public final class DocumentClipper { for (int i = 0; i < count; ++i) { ClipData.Item item = clipData.getItemAt(i); Uri itemUri = item.getUri(); - srcDocs.add(createDocument(itemUri)); + DocumentInfo docInfo = createDocument(itemUri); + if (docInfo != null) { + srcDocs.add(docInfo); + } else { + // This uri either doesn't exist, or is invalid. + Log.w(TAG, "Can't create document info from uri: " + itemUri); + } } return srcDocs; @@ -196,23 +202,129 @@ public final class DocumentClipper { 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()); + doc = DocumentInfo.fromUri(resolver, uri); } catch (Exception e) { Log.e(TAG, e.getMessage()); - } finally { - IoUtils.closeQuietly(cursor); - ContentProviderClient.releaseQuietly(client); } } return doc; } + /** + * Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData + * returned from {@link ClipboardManager#getPrimaryClip()}. + * + * @param destination destination document. + * @param docStack the document stack to the destination folder, + * @param callback callback to notify when operation finishes. + */ + public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack, + FileOperations.Callback callback) { + copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback); + } + + /** + * Copies documents from given clip data. + * + * @param destination destination document + * @param docStack the document stack to the destination folder + * @param clipData the clipData to copy from, or null to copy from clipboard + * @param callback callback to notify when operation finishes + */ + public void copyFromClipData(final DocumentInfo destination, DocumentStack docStack, + @Nullable final ClipData clipData, final FileOperations.Callback callback) { + if (clipData == null) { + Log.i(TAG, "Received null clipData. Ignoring."); + return; + } + + new AsyncTask() { + + @Override + protected ClipDetails doInBackground(Void... params) { + return getClipDetails(clipData); + } + + @Override + protected void onPostExecute(ClipDetails clipDetails) { + if (clipDetails == null) { + Log.w(TAG, "Received null clipDetails. Ignoring."); + return; + } + + List docs = clipDetails.docs; + @OpType int type = clipDetails.opType; + DocumentInfo srcParent = clipDetails.parent; + moveDocuments(docs, destination, docStack, type, srcParent, callback); + } + }.execute(); + } + + /** + * 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( + List docs, + DocumentInfo destination, + DocumentStack docStack, + @OpType int operationType, + DocumentInfo srcParent, + FileOperations.Callback callback) { + + RootInfo destRoot = docStack.root; + if (!canCopy(docs, destRoot, destination)) { + callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, operationType, 0); + return; + } + + if (docs.isEmpty()) { + callback.onOperationResult(FileOperations.Callback.STATUS_ACCEPTED, operationType, 0); + return; + } + + DocumentStack dstStack = new DocumentStack(); + dstStack.push(destination); + dstStack.addAll(docStack); + switch (operationType) { + case FileOperationService.OPERATION_MOVE: + FileOperations.move(mContext, docs, srcParent, dstStack, callback); + break; + case FileOperationService.OPERATION_COPY: + FileOperations.copy(mContext, docs, dstStack, callback); + break; + default: + throw new UnsupportedOperationException("Unsupported operation: " + operationType); + } + } + + /** + * Returns true if the list of files can be copied to destination. Note that this + * is a policy check only. Currently the method does not attempt to verify + * available space or any other environmental aspects possibly resulting in + * failure to copy. + * + * @return true if the list of files can be copied to destination. + */ + private boolean canCopy(List files, RootInfo root, DocumentInfo dest) { + if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) { + return false; + } + + // Can't copy folders to downloads, because we don't show folders there. + if (root.isDownloads()) { + for (DocumentInfo docs : files) { + if (docs.isDirectory()) { + return false; + } + } + } + + return true; + } + public static class ClipDetails { public final @OpType int opType; public final List docs; diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java index cb9ce25152793..0c615018558a5 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java @@ -35,6 +35,8 @@ public class DocumentsApplication extends Application { private ThumbnailCache mThumbnailCache; + private DocumentClipper mClipper; + public static RootsCache getRootsCache(Context context) { return ((DocumentsApplication) context.getApplicationContext()).mRoots; } @@ -55,6 +57,10 @@ public class DocumentsApplication extends Application { return client; } + public static DocumentClipper getDocumentClipper(Context context) { + return ((DocumentsApplication) context.getApplicationContext()).mClipper; + } + @Override public void onCreate() { super.onCreate(); @@ -67,6 +73,8 @@ public class DocumentsApplication extends Application { mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4); + mClipper = new DocumentClipper(this); + final IntentFilter packageFilter = new IntentFilter(); packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java index 282cc70cd22a1..8445cf716e94d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java @@ -76,7 +76,7 @@ public class FilesActivity extends BaseActivity { public void onCreate(Bundle icicle) { super.onCreate(icicle); - mClipper = new DocumentClipper(this); + mClipper = DocumentsApplication.getDocumentClipper(this); RootsFragment.show(getFragmentManager(), null); diff --git a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java index b4d79715d4799..4274efaed5d82 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java +++ b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java @@ -16,6 +16,7 @@ package com.android.documentsui; +import android.annotation.StringRes; import android.app.Activity; import android.support.design.widget.Snackbar; import android.view.View; @@ -23,7 +24,22 @@ import android.view.View; public final class Snackbars { private Snackbars() {} - public static final Snackbar makeSnackbar(Activity activity, int messageId, int duration) { + public static final void showMove(Activity activity, int docCount) { + CharSequence message = Shared.getQuantityString(activity, R.plurals.move_begin, docCount); + makeSnackbar(activity, message, Snackbar.LENGTH_SHORT).show(); + } + + public static final void showCopy(Activity activity, int docCount) { + CharSequence message = Shared.getQuantityString(activity, R.plurals.copy_begin, docCount); + makeSnackbar(activity, message, Snackbar.LENGTH_SHORT).show(); + } + + public static final void showPasteFailed(Activity activity) { + makeSnackbar(activity, R.string.clipboard_files_cannot_paste, Snackbar.LENGTH_SHORT).show(); + } + + public static final Snackbar makeSnackbar(Activity activity, @StringRes int messageId, + int duration) { return Snackbars.makeSnackbar( activity, activity.getResources().getText(messageId), duration); } diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java index 56d7925423de2..8cc7c4fb0ecda 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -27,7 +27,6 @@ import static com.android.documentsui.model.DocumentInfo.getCursorString; import com.google.common.collect.Lists; import android.annotation.IntDef; -import android.annotation.StringRes; import android.app.Activity; import android.app.ActivityManager; import android.app.AlertDialog; @@ -53,6 +52,7 @@ import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.support.annotation.Nullable; +import android.support.annotation.StringRes; import android.support.design.widget.Snackbar; import android.support.v13.view.DragStartHelper; import android.support.v7.widget.GridLayoutManager; @@ -181,6 +181,32 @@ public class DirectoryFragment extends Fragment private DirectoryDragListener mOnDragListener; + /** + * A callback to show snackbar at the beginning of moving and copying. + */ + private final FileOperations.Callback mFileOpCallback = (status, opType, docCount) -> { + if (status == FileOperations.Callback.STATUS_REJECTED) { + Snackbars.showPasteFailed(getActivity()); + return; + } + + if (docCount == 0) { + // Nothing has been pasted, so there is no need to show a snackbar. + return; + } + + switch (opType) { + case FileOperationService.OPERATION_MOVE: + Snackbars.showMove(getActivity(), docCount); + break; + case FileOperationService.OPERATION_COPY: + Snackbars.showCopy(getActivity(), docCount); + break; + default: + throw new UnsupportedOperationException("Unsupported Operation: " + opType); + } + }; + @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -283,7 +309,7 @@ public class DirectoryFragment extends Fragment mFocusManager = new FocusManager(context, mRecView, mModel); mTuner = FragmentTuner.pick(getContext(), state); - mClipper = new DocumentClipper(context); + mClipper = DocumentsApplication.getDocumentClipper(getContext()); final ActivityManager am = (ActivityManager) context.getSystemService( Context.ACTIVITY_SERVICE); @@ -343,11 +369,12 @@ public class DirectoryFragment extends Fragment FileOperationService.OPERATION_COPY); FileOperations.start( - getActivity(), + getContext(), getDisplayState().selectedDocumentsForCopy, getDisplayState().stack.peek(), (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK), - operationType); + operationType, + mFileOpCallback); } protected boolean onDoubleTap(MotionEvent e) { @@ -1035,94 +1062,6 @@ public class DirectoryFragment extends Fragment return commonType[0] + "/" + commonType[1]; } - private void copyFromClipboard(final DocumentInfo destination) { - new AsyncTask() { - - @Override - protected ClipDetails doInBackground(Void... params) { - return mClipper.getClipDetails(); - } - - @Override - protected void onPostExecute(ClipDetails clipDetails) { - if (clipDetails == null) { - Log.w(TAG, "Received null clipDetails from primary clipboard. Ignoring."); - return; - } - List docs = clipDetails.docs; - @OpType int type = clipDetails.opType; - DocumentInfo srcParent = clipDetails.parent; - moveDocuments(docs, destination, type, srcParent); - } - }.execute(); - } - - private void copyFromClipData(final ClipData clipData, final DocumentInfo destination) { - assert(clipData != null); - - new AsyncTask() { - - @Override - protected ClipDetails doInBackground(Void... params) { - return mClipper.getClipDetails(clipData); - } - - @Override - protected void onPostExecute(ClipDetails clipDetails) { - if (clipDetails == null) { - Log.w(TAG, "Received null clipDetails. Ignoring."); - return; - } - - List docs = clipDetails.docs; - @OpType int type = clipDetails.opType; - DocumentInfo srcParent = clipDetails.parent; - moveDocuments(docs, destination, type, srcParent); - } - }.execute(); - } - - /** - * 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 docs, final DocumentInfo destination, - final @OpType int operationType, final DocumentInfo srcParent) { - BaseActivity activity = (BaseActivity) getActivity(); - if (!canCopy(docs, activity.getCurrentRoot(), destination)) { - Snackbars.makeSnackbar( - getActivity(), - R.string.clipboard_files_cannot_paste, - Snackbar.LENGTH_SHORT) - .show(); - return; - } - - if (docs.isEmpty()) { - return; - } - - final DocumentStack curStack = getDisplayState().stack; - DocumentStack dstStack = new DocumentStack(); - if (destination != null) { - dstStack.push(destination); - dstStack.addAll(curStack); - } else { - 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); - } - } - public void copySelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_COPY_CLIPBOARD); @@ -1147,6 +1086,7 @@ public class DirectoryFragment extends Fragment public void cutSelectedToClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_CUT_CLIPBOARD); + Selection selection = mSelectionManager.getSelection(new Selection()); if (selection.isEmpty()) { return; @@ -1171,36 +1111,12 @@ public class DirectoryFragment extends Fragment public void pasteFromClipboard() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_PASTE_CLIPBOARD); - DocumentInfo destination = ((BaseActivity) getActivity()).getCurrentDirectory(); - copyFromClipboard(destination); + BaseActivity activity = (BaseActivity) getActivity(); + DocumentInfo destination = activity.getCurrentDirectory(); + mClipper.copyFromClipboard(destination, activity.getDisplayState().stack, mFileOpCallback); getActivity().invalidateOptionsMenu(); } - /** - * Returns true if the list of files can be copied to destination. Note that this - * is a policy check only. Currently the method does not attempt to verify - * available space or any other environmental aspects possibly resulting in - * failure to copy. - * - * @return true if the list of files can be copied to destination. - */ - private boolean canCopy(List files, RootInfo root, DocumentInfo dest) { - if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) { - return false; - } - - // Can't copy folders to downloads, because we don't show folders there. - if (root.isDownloads()) { - for (DocumentInfo docs : files) { - if (docs.isDirectory()) { - return false; - } - } - } - - return true; - } - public void selectAllFiles() { Metrics.logUserAction(getContext(), Metrics.USER_ACTION_SELECT_ALL); @@ -1316,7 +1232,7 @@ public class DirectoryFragment extends Fragment src == null ? Metrics.USER_ACTION_DRAG_N_DROP_MULTI_WINDOW : Metrics.USER_ACTION_DRAG_N_DROP); - copyFromClipData(clipData, dst); + mClipper.copyFromClipData(dst, getDisplayState().stack, clipData, mFileOpCallback); return true; } diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java index 748da006207f4..3de8145268223 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java +++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java @@ -20,7 +20,6 @@ import static android.os.SystemClock.elapsedRealtime; import static com.android.documentsui.Shared.DEBUG; import static com.android.documentsui.Shared.EXTRA_STACK; import static com.android.documentsui.Shared.asArrayList; -import static com.android.documentsui.Shared.getQuantityString; import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL; import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID; import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION; @@ -30,21 +29,20 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_CO import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE; import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE; +import android.annotation.IntDef; import android.app.Activity; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.os.Parcelable; import android.support.annotation.VisibleForTesting; -import android.support.design.widget.Snackbar; import android.util.Log; -import com.android.documentsui.R; -import com.android.documentsui.Snackbars; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; import com.android.documentsui.services.FileOperationService.OpType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.List; /** @@ -65,15 +63,14 @@ public final class FileOperations { /** * Tries to start the activity. Returns the job id. */ - public static String start( - Activity activity, List srcDocs, - DocumentStack stack, int operationType) { + public static String start(Context context, List srcDocs, DocumentStack stack, + @OpType int operationType, Callback callback) { if (DEBUG) Log.d(TAG, "Handling generic 'start' call."); switch (operationType) { case OPERATION_COPY: - return FileOperations.copy(activity, srcDocs, stack); + return FileOperations.copy(context, srcDocs, stack, callback); case OPERATION_MOVE: throw new IllegalArgumentException("Moving requires providing the source parent."); case OPERATION_DELETE: @@ -86,17 +83,16 @@ public final class FileOperations { /** * Tries to start the activity. Returns the job id. */ - public static String start( - Activity activity, List srcDocs, DocumentInfo srcParent, - DocumentStack stack, int operationType) { + public static String start(Context context, List srcDocs, DocumentInfo srcParent, + DocumentStack stack, @OpType int operationType, Callback callback) { if (DEBUG) Log.d(TAG, "Handling generic 'start' call."); switch (operationType) { case OPERATION_COPY: - return FileOperations.copy(activity, srcDocs, stack); + return FileOperations.copy(context, srcDocs, stack, callback); case OPERATION_MOVE: - return FileOperations.move(activity, srcDocs, srcParent, stack); + return FileOperations.move(context, srcDocs, srcParent, stack, callback); case OPERATION_DELETE: throw new UnsupportedOperationException("Delete isn't currently supported."); default: @@ -116,17 +112,16 @@ public final class FileOperations { } @VisibleForTesting - public static String copy( - Activity activity, List srcDocs, DocumentStack destination) { + public static String copy(Context context, List srcDocs, + DocumentStack destination, Callback callback) { String jobId = createJobId(); if (DEBUG) Log.d(TAG, "Initiating 'copy' operation id: " + jobId); - Intent intent = createBaseIntent(OPERATION_COPY, activity, jobId, srcDocs, destination); + Intent intent = createBaseIntent(OPERATION_COPY, context, jobId, srcDocs, destination); - createSharedSnackBar(activity, R.plurals.copy_begin, srcDocs.size()) - .show(); + callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_COPY, srcDocs.size()); - activity.startService(intent); + context.startService(intent); return jobId; } @@ -140,19 +135,17 @@ public final class FileOperations { * @param srcParent Parent of all the source documents. * @param destination The move destination stack. */ - public static String move( - Activity activity, List srcDocs, DocumentInfo srcParent, - DocumentStack destination) { + public static String move(Context context, List srcDocs, DocumentInfo srcParent, + DocumentStack destination, Callback callback) { String jobId = createJobId(); if (DEBUG) Log.d(TAG, "Initiating 'move' operation id: " + jobId); - Intent intent = createBaseIntent(OPERATION_MOVE, activity, jobId, srcDocs, srcParent, + Intent intent = createBaseIntent(OPERATION_MOVE, context, jobId, srcDocs, srcParent, destination); - createSharedSnackBar(activity, R.plurals.move_begin, srcDocs.size()) - .show(); + callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_MOVE, srcDocs.size()); - activity.startService(intent); + context.startService(intent); return jobId; } @@ -223,14 +216,6 @@ public final class FileOperations { return intent; } - private static Snackbar createSharedSnackBar(Activity activity, int contentId, int fileCount) { - Resources res = activity.getResources(); - return Snackbars.makeSnackbar( - activity, - getQuantityString(activity, contentId, fileCount), - Snackbar.LENGTH_SHORT); - } - private static final class IdBuilder { // Remember last job time so we can guard against collisions. @@ -250,4 +235,25 @@ public final class FileOperations { return String.valueOf(mLastJobTime) + "-" + String.valueOf(mSubId); } } + + /** + * A functional callback called when the file operation starts or fails to start. + */ + @FunctionalInterface + public interface Callback { + @Retention(RetentionPolicy.SOURCE) + @IntDef({STATUS_ACCEPTED, STATUS_REJECTED}) + @interface Status {} + static final int STATUS_ACCEPTED = 0; + static final int STATUS_REJECTED = 1; + + /** + * Performs operation when the file operation starts or fails to start. + * + * @param status {@link Status} of this operation + * @param opType file operation type {@link OpType}. + * @param docCount number of documents operated. + */ + void onOperationResult(@Status int status, @OpType int opType, int docCount); + } }