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)
This commit is contained in:
Garfield, Tan
2016-05-12 14:12:18 -07:00
committed by Garfield Tan
parent 438266a4f7
commit 6d0b46eb12
6 changed files with 230 additions and 172 deletions

View File

@@ -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<Void, Void, ClipDetails>() {
@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<DocumentInfo> 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<DocumentInfo> 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<DocumentInfo> 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<DocumentInfo> docs;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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<Void, Void, ClipDetails>() {
@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<DocumentInfo> 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<Void, Void, ClipDetails>() {
@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<DocumentInfo> 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<DocumentInfo> 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<DocumentInfo> 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;
}

View File

@@ -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<DocumentInfo> srcDocs,
DocumentStack stack, int operationType) {
public static String start(Context context, List<DocumentInfo> 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<DocumentInfo> srcDocs, DocumentInfo srcParent,
DocumentStack stack, int operationType) {
public static String start(Context context, List<DocumentInfo> 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<DocumentInfo> srcDocs, DocumentStack destination) {
public static String copy(Context context, List<DocumentInfo> 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<DocumentInfo> srcDocs, DocumentInfo srcParent,
DocumentStack destination) {
public static String move(Context context, List<DocumentInfo> 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);
}
}