summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ben Lin <linben@google.com> 2017-03-07 15:37:16 -0800
committer Ben Lin <linben@google.com> 2017-03-10 14:25:22 -0800
commit30b0dc1896abc67a970b61ebfd420275a31c1e18 (patch)
tree292846bd8e828474c55c7cb2ef949c226d060e80
parent15ed8b2c8a943d9aa3d01662b3052f8989ca3a09 (diff)
Fix a bug where dropping on Root never works.
DragEvent gets recycled, so by passing a DragEvent reference directly to ActionHandler#dropOn, by the time the callback occurs, DragEvent is updated to ACTION_DRAG_ENDED. ACTION_DRAG_ENDED events have no ClipData and no localState, so the file operation never will occur. Also added tests that involved ... refactoring lots of things. Change-Id: I87daf1a4ec4e536701e03fd6dc53fc55829e5e51
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java14
-rw-r--r--src/com/android/documentsui/ActionHandler.java10
-rw-r--r--src/com/android/documentsui/BaseActivity.java7
-rw-r--r--src/com/android/documentsui/DocumentsApplication.java2
-rw-r--r--src/com/android/documentsui/RefreshTask.java3
-rw-r--r--src/com/android/documentsui/TimeoutTask.java13
-rw-r--r--src/com/android/documentsui/base/RootInfo.java14
-rw-r--r--src/com/android/documentsui/clipping/DocumentClipper.java293
-rw-r--r--src/com/android/documentsui/clipping/RuntimeDocumentClipper.java336
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java3
-rw-r--r--src/com/android/documentsui/files/ActionHandler.java34
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java9
-rw-r--r--src/com/android/documentsui/roots/GetRootDocumentTask.java19
-rw-r--r--src/com/android/documentsui/sidebar/RootsFragment.java7
-rw-r--r--tests/common/com/android/documentsui/testing/TestDocumentClipper.java79
-rw-r--r--tests/unit/com/android/documentsui/files/ActionHandlerTest.java92
16 files changed, 592 insertions, 343 deletions
diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java
index d4e964c2b..617e5aef3 100644
--- a/src/com/android/documentsui/AbstractActionHandler.java
+++ b/src/com/android/documentsui/AbstractActionHandler.java
@@ -51,6 +51,7 @@ import com.android.documentsui.dirlist.DocumentDetails;
import com.android.documentsui.dirlist.FocusHandler;
import com.android.documentsui.files.LauncherActivity;
import com.android.documentsui.queries.SearchViewManager;
+import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.LoadRootTask;
import com.android.documentsui.roots.RootsAccess;
import com.android.documentsui.selection.Selection;
@@ -61,6 +62,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
import javax.annotation.Nullable;
@@ -142,6 +144,18 @@ public abstract class AbstractActionHandler<T extends Activity & CommonAddons>
}
@Override
+ public void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback) {
+ GetRootDocumentTask task = new GetRootDocumentTask(
+ root,
+ mActivity,
+ timeout,
+ mDocs,
+ callback);
+
+ task.executeOnExecutor(mExecutors.lookup(root.authority));
+ }
+
+ @Override
public void refreshDocument(DocumentInfo doc, BooleanConsumer callback) {
RefreshTask task = new RefreshTask(
mInjector.features,
diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java
index 33f1d7dcb..96187cc85 100644
--- a/src/com/android/documentsui/ActionHandler.java
+++ b/src/com/android/documentsui/ActionHandler.java
@@ -28,6 +28,8 @@ import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.dirlist.DocumentDetails;
+import java.util.function.Consumer;
+
import javax.annotation.Nullable;
public interface ActionHandler {
@@ -44,6 +46,14 @@ public interface ActionHandler {
*/
void ejectRoot(RootInfo root, BooleanConsumer listener);
+
+ /**
+ * Attempts to fetch the DocumentInfo for the supplied root. Returns the DocumentInfo to the
+ * callback. If the task times out, callback will be called with null DocumentInfo. Supply
+ * {@link TimeoutTask#DEFAULT_TIMEOUT} if you don't want to the task to ever time out.
+ */
+ void getRootDocument(RootInfo root, int timeout, Consumer<DocumentInfo> callback);
+
/**
* Attempts to refresh the given DocumentInfo, which should be at the top of the state stack.
* Returns a boolean answer to the callback, given by {@link ContentProvider#refresh}.
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index 1bb5bb88f..3bccfc4e7 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -309,11 +309,10 @@ public abstract class BaseActivity
if (mRoots.isRecentsRoot(root)) {
refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
} else {
- new GetRootDocumentTask(
+ mInjector.actions.getRootDocument(
root,
- this,
- mInjector.actions::openRootDocument)
- .executeOnExecutor(getExecutorForCurrentDirectory());
+ TimeoutTask.DEFAULT_TIMEOUT,
+ mInjector.actions::openRootDocument);
}
}
diff --git a/src/com/android/documentsui/DocumentsApplication.java b/src/com/android/documentsui/DocumentsApplication.java
index 94e9da608..512baba59 100644
--- a/src/com/android/documentsui/DocumentsApplication.java
+++ b/src/com/android/documentsui/DocumentsApplication.java
@@ -84,7 +84,7 @@ public class DocumentsApplication extends Application {
mClipStore = new ClipStorage(
ClipStorage.prepareStorage(getCacheDir()),
getSharedPreferences(ClipStorage.PREF_NAME, 0));
- mClipper = new DocumentClipper(this, mClipStore);
+ mClipper = DocumentClipper.create(this, mClipStore);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
diff --git a/src/com/android/documentsui/RefreshTask.java b/src/com/android/documentsui/RefreshTask.java
index deaf0ca6a..fc9bc4d54 100644
--- a/src/com/android/documentsui/RefreshTask.java
+++ b/src/com/android/documentsui/RefreshTask.java
@@ -52,14 +52,13 @@ public class RefreshTask extends TimeoutTask<Void, Boolean> {
public RefreshTask(Features features, State state, DocumentInfo doc, long timeout,
@ApplicationScope Context context, Check check, BooleanConsumer callback) {
- super(check);
+ super(check, timeout);
mFeatures = features;
mState = state;
mDoc = doc;
mContext = context;
mCallback = callback;
mSignal = new CancellationSignal();
- setTimeout(timeout);
}
@Override
diff --git a/src/com/android/documentsui/TimeoutTask.java b/src/com/android/documentsui/TimeoutTask.java
index 57b119e8f..9e7bbb810 100644
--- a/src/com/android/documentsui/TimeoutTask.java
+++ b/src/com/android/documentsui/TimeoutTask.java
@@ -22,23 +22,18 @@ import android.os.Handler;
import android.os.Looper;
import com.android.documentsui.base.CheckedTask;
-import com.android.documentsui.base.DocumentInfo;
/**
- * A {@link CheckedTask} that takes and query SAF to obtain the
- * {@link DocumentInfo} of its root document and call supplied callback to handle the
- * {@link DocumentInfo}.
+ * A {@link CheckedTask} that will timeout after a certain period of time, and do any properly clean
+ * up necessary before ending itself.
*/
public abstract class TimeoutTask<Input, Output> extends CheckedTask<Input, Output> {
- private static final int DEFAULT_TIMEOUT = -1;
+ public static final int DEFAULT_TIMEOUT = -1;
private long mTimeout = DEFAULT_TIMEOUT;
- public TimeoutTask(Check check) {
+ public TimeoutTask(Check check, long timeout) {
super(check);
- }
-
- public void setTimeout(long timeout) {
mTimeout = timeout;
}
diff --git a/src/com/android/documentsui/base/RootInfo.java b/src/com/android/documentsui/base/RootInfo.java
index 64677e596..3fe9a2188 100644
--- a/src/com/android/documentsui/base/RootInfo.java
+++ b/src/com/android/documentsui/base/RootInfo.java
@@ -349,20 +349,6 @@ public class RootInfo implements Durable, Parcelable, Comparable<RootInfo> {
return IconUtils.applyTintColor(context, R.drawable.ic_eject, R.color.item_eject_icon);
}
- /**
- * @deprecate use {@link DocumentsAccess#getRootDocument}.
- */
- @Deprecated
- public @Nullable DocumentInfo getRootDocumentBlocking(Context context) {
- try {
- final Uri uri = DocumentsContract.buildDocumentUri(authority, documentId);
- return DocumentInfo.fromUri(context.getContentResolver(), uri);
- } catch (FileNotFoundException e) {
- Log.w(TAG, "Failed to find root", e);
- return null;
- }
- }
-
@Override
public boolean equals(Object o) {
if (o == null) {
diff --git a/src/com/android/documentsui/clipping/DocumentClipper.java b/src/com/android/documentsui/clipping/DocumentClipper.java
index 0c1b59c14..b8171bdf5 100644
--- a/src/com/android/documentsui/clipping/DocumentClipper.java
+++ b/src/com/android/documentsui/clipping/DocumentClipper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,203 +17,49 @@
package com.android.documentsui.clipping;
import android.content.ClipData;
-import android.content.ClipDescription;
import android.content.ClipboardManager;
-import android.content.ContentResolver;
import android.content.Context;
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.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
-import com.android.documentsui.base.Features;
import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.base.Shared;
import com.android.documentsui.selection.Selection;
-import com.android.documentsui.services.FileOperation;
-import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.services.FileOperationService.OpType;
import com.android.documentsui.services.FileOperations;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
import java.util.function.Function;
-/**
- * ClipboardManager wrapper class providing higher level logical
- * support for dealing with Documents.
- */
-public final class DocumentClipper {
-
- private static final String TAG = "DocumentClipper";
+public interface DocumentClipper {
- static final String SRC_PARENT_KEY = "srcParent";
- static final String OP_TYPE_KEY = "opType";
static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
static final String OP_JUMBO_SELECTION_TAG = "jumboSelection-tag";
- private final Context mContext;
- private final ClipStore mClipStore;
- private final ClipboardManager mClipboard;
-
- public DocumentClipper(Context context, ClipStore clipStore) {
- mContext = context;
- mClipStore = clipStore;
- mClipboard = context.getSystemService(ClipboardManager.class);
- }
-
- public boolean hasItemsToPaste() {
- if (mClipboard.hasPrimaryClip()) {
- ClipData clipData = mClipboard.getPrimaryClip();
-
- int count = clipData.getItemCount();
- if (count > 0) {
- for (int i = 0; i < count; ++i) {
- ClipData.Item item = clipData.getItemAt(i);
- Uri uri = item.getUri();
- if (isDocumentUri(uri)) {
- return true;
- }
- }
- }
- }
- return false;
+ static public DocumentClipper create(Context context, ClipStore clipStore) {
+ return new RuntimeDocumentClipper(context, clipStore);
}
- private boolean isDocumentUri(@Nullable Uri uri) {
- return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
- }
+ boolean hasItemsToPaste();
+ @OpType int getOpType(ClipData data);
/**
* Returns {@link ClipData} representing the selection, or null if selection is empty,
* or cannot be converted.
*/
- public ClipData getClipDataForDocuments(
- Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
-
- assert(selection != null);
-
- if (selection.isEmpty()) {
- Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
- return null;
- }
-
- return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
- ? createJumboClipData(uriBuilder, selection, opType)
- : createStandardClipData(uriBuilder, selection, opType);
- }
-
- /**
- * Returns ClipData representing the selection.
- */
- private ClipData createStandardClipData(
- Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
-
- assert(!selection.isEmpty());
- assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
-
- final ContentResolver resolver = mContext.getContentResolver();
- final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
- final Set<String> clipTypes = new HashSet<>();
-
- PersistableBundle bundle = new PersistableBundle();
- bundle.putInt(OP_TYPE_KEY, opType);
-
- for (String id : selection) {
- assert(id != null);
- Uri uri = uriBuilder.apply(id);
- DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
- clipItems.add(new ClipData.Item(uri));
- }
-
- ClipDescription description = new ClipDescription(
- "", // Currently "label" is not displayed anywhere in the UI.
- clipTypes.toArray(new String[0]));
- description.setExtras(bundle);
-
- return createClipData(description, clipItems);
- }
-
- /**
- * Returns ClipData representing the list of docs
- */
- private ClipData createJumboClipData(
- Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
-
- assert(!selection.isEmpty());
- assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
-
- final List<Uri> uris = new ArrayList<>(selection.size());
-
- final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
- final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
-
- // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
- final ContentResolver resolver = mContext.getContentResolver();
- final Set<String> clipTypes = new HashSet<>();
- int docCount = 0;
- for (String id : selection) {
- assert(id != null);
- Uri uri = uriBuilder.apply(id);
- if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
- DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
- clipItems.add(new ClipData.Item(uri));
- }
-
- uris.add(uri);
- }
-
- // Prepare metadata
- PersistableBundle bundle = new PersistableBundle();
- bundle.putInt(OP_TYPE_KEY, opType);
- bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
-
- // Persists clip items and gets the slot they were saved under.
- int tag = mClipStore.persistUris(uris);
- bundle.putInt(OP_JUMBO_SELECTION_TAG, tag);
-
- ClipDescription description = new ClipDescription(
- "", // Currently "label" is not displayed anywhere in the UI.
- clipTypes.toArray(new String[0]));
- description.setExtras(bundle);
-
- return createClipData(description, clipItems);
- }
+ ClipData getClipDataForDocuments(
+ Function<String, Uri> uriBuilder, Selection selection, @OpType int opType);
/**
* Puts {@code ClipData} in a primary clipboard, describing a copy operation
*/
- public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
- ClipData data =
- getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
- assert(data != null);
-
- mClipboard.setPrimaryClip(data);
- }
+ void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection);
/**
* Puts {@Code ClipData} in a primary clipboard, describing a cut operation
*/
- public void clipDocumentsForCut(
- Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
- assert(!selection.isEmpty());
- assert(parent.derivedUri != null);
-
- ClipData data = getClipDataForDocuments(uriBuilder, selection,
- FileOperationService.OPERATION_MOVE);
- assert(data != null);
-
- PersistableBundle bundle = data.getDescription().getExtras();
- bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
-
- mClipboard.setPrimaryClip(data);
- }
+ void clipDocumentsForCut(
+ Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent);
/**
* Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
@@ -223,13 +69,10 @@ public final class DocumentClipper {
* @param docStack the document stack to the destination folder,
* @param callback callback to notify when operation finishes.
*/
- public void copyFromClipboard(
+ void copyFromClipboard(
DocumentInfo destination,
DocumentStack docStack,
- FileOperations.Callback callback) {
-
- copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
- }
+ FileOperations.Callback callback);
/**
* Copies documents from clipboard. It's the same as {@link #copyFromClipData} with clipData
@@ -238,12 +81,9 @@ public final class DocumentClipper {
* @param docStack the document stack to the destination folder,
* @param callback callback to notify when operation finishes.
*/
- public void copyFromClipboard(
+ void copyFromClipboard(
DocumentStack docStack,
- FileOperations.Callback callback) {
-
- copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
- }
+ FileOperations.Callback callback);
/**
* Copied documents from given clip data to a root directory.
@@ -252,14 +92,11 @@ public final class DocumentClipper {
* @param clipData the clipData to copy from
* @param callback callback to notify when operation finishes
*/
- public void copyFromClipData(
+ void copyFromClipData(
final RootInfo root,
final @Nullable DocumentInfo destination,
final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
- DocumentStack dstStack = new DocumentStack(root, destination);
- copyFromClipData(dstStack, clipData, callback);
- }
+ final FileOperations.Callback callback);
/**
* Copies documents from given clip data to a folder.
@@ -270,101 +107,9 @@ public final class DocumentClipper {
* @param clipData the clipData to copy from
* @param callback callback to notify when operation finishes
*/
- public void copyFromClipData(
+ void copyFromClipData(
final @Nullable DocumentInfo destination,
final DocumentStack docStack,
final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
-
- DocumentStack dstStack = new DocumentStack(docStack, destination);
- copyFromClipData(dstStack, clipData, callback);
- }
-
- /**
- * Copies documents from given clip data to a folder.
- *
- * @param dstStack the document stack to the destination folder, including the destination
- * folder.
- * @param clipData the clipData to copy from
- * @param callback callback to notify when operation finishes
- */
- private void copyFromClipData(
- final DocumentStack dstStack,
- final @Nullable ClipData clipData,
- final FileOperations.Callback callback) {
-
- if (clipData == null) {
- Log.i(TAG, "Received null clipData. Ignoring.");
- return;
- }
-
- PersistableBundle bundle = clipData.getDescription().getExtras();
- @OpType int opType = getOpType(bundle);
- try {
- if (!canCopy(dstStack.peek())) {
- callback.onOperationResult(
- FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0);
- return;
- }
-
- UrisSupplier uris = UrisSupplier.create(clipData, mClipStore);
- if (uris.getItemCount() == 0) {
- callback.onOperationResult(
- FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
- return;
- }
-
- String srcParentString = bundle.getString(SRC_PARENT_KEY);
- Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
-
- FileOperation operation = new FileOperation.Builder()
- .withOpType(opType)
- .withSrcParent(srcParent)
- .withDestination(dstStack)
- .withSrcs(uris)
- .build();
-
- FileOperations.start(mContext, operation, callback);
- } catch(IOException e) {
- Log.e(TAG, "Cannot create uris supplier.", e);
- callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
- return;
- }
- }
-
- /**
- * 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 static boolean canCopy(@Nullable DocumentInfo dest) {
- return dest != null && dest.isDirectory() && dest.isCreateSupported();
- }
-
- public static @OpType int getOpType(ClipData data) {
- PersistableBundle bundle = data.getDescription().getExtras();
- return getOpType(bundle);
- }
-
- private static @OpType int getOpType(PersistableBundle bundle) {
- return bundle.getInt(OP_TYPE_KEY);
- }
-
- private static ClipData createClipData(
- ClipDescription description, ArrayList<ClipData.Item> clipItems) {
-
- // technically we want to check >= O, but we'd need to patch back the O version code :|
- if (Features.OMC_RUNTIME) {
- return new ClipData(description, clipItems);
- }
-
- ClipData clip = new ClipData(description, clipItems.get(0));
- for (int i = 1; i < clipItems.size(); i++) {
- clip.addItem(clipItems.get(i));
- }
- return clip;
- }
+ final FileOperations.Callback callback);
}
diff --git a/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java b/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
new file mode 100644
index 000000000..36233e137
--- /dev/null
+++ b/src/com/android/documentsui/clipping/RuntimeDocumentClipper.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.clipping;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+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.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.Features;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.base.Shared;
+import com.android.documentsui.selection.Selection;
+import com.android.documentsui.services.FileOperation;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.services.FileOperations;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * ClipboardManager wrapper class providing higher level logical
+ * support for dealing with Documents.
+ */
+final class RuntimeDocumentClipper implements DocumentClipper {
+
+ private static final String TAG = "DocumentClipper";
+ private static final String SRC_PARENT_KEY = "srcParent";
+ private static final String OP_TYPE_KEY = "opType";
+
+ private final Context mContext;
+ private final ClipStore mClipStore;
+ private final ClipboardManager mClipboard;
+
+ RuntimeDocumentClipper(Context context, ClipStore clipStore) {
+ mContext = context;
+ mClipStore = clipStore;
+ mClipboard = context.getSystemService(ClipboardManager.class);
+ }
+
+ @Override
+ public boolean hasItemsToPaste() {
+ if (mClipboard.hasPrimaryClip()) {
+ ClipData clipData = mClipboard.getPrimaryClip();
+
+ int count = clipData.getItemCount();
+ if (count > 0) {
+ for (int i = 0; i < count; ++i) {
+ ClipData.Item item = clipData.getItemAt(i);
+ Uri uri = item.getUri();
+ if (isDocumentUri(uri)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isDocumentUri(@Nullable Uri uri) {
+ return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
+ }
+
+ @Override
+ public ClipData getClipDataForDocuments(
+ Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+
+ assert(selection != null);
+
+ if (selection.isEmpty()) {
+ Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
+ return null;
+ }
+
+ return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
+ ? createJumboClipData(uriBuilder, selection, opType)
+ : createStandardClipData(uriBuilder, selection, opType);
+ }
+
+ /**
+ * Returns ClipData representing the selection.
+ */
+ private ClipData createStandardClipData(
+ Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+
+ assert(!selection.isEmpty());
+ assert(selection.size() <= Shared.MAX_DOCS_IN_INTENT);
+
+ final ContentResolver resolver = mContext.getContentResolver();
+ final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
+ final Set<String> clipTypes = new HashSet<>();
+
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putInt(OP_TYPE_KEY, opType);
+
+ for (String id : selection) {
+ assert(id != null);
+ Uri uri = uriBuilder.apply(id);
+ DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
+ clipItems.add(new ClipData.Item(uri));
+ }
+
+ ClipDescription description = new ClipDescription(
+ "", // Currently "label" is not displayed anywhere in the UI.
+ clipTypes.toArray(new String[0]));
+ description.setExtras(bundle);
+
+ return createClipData(description, clipItems);
+ }
+
+ /**
+ * Returns ClipData representing the list of docs
+ */
+ private ClipData createJumboClipData(
+ Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+
+ assert(!selection.isEmpty());
+ assert(selection.size() > Shared.MAX_DOCS_IN_INTENT);
+
+ final List<Uri> uris = new ArrayList<>(selection.size());
+
+ final int capacity = Math.min(selection.size(), Shared.MAX_DOCS_IN_INTENT);
+ final ArrayList<ClipData.Item> clipItems = new ArrayList<>(capacity);
+
+ // Set up mime types for the first Shared.MAX_DOCS_IN_INTENT
+ final ContentResolver resolver = mContext.getContentResolver();
+ final Set<String> clipTypes = new HashSet<>();
+ int docCount = 0;
+ for (String id : selection) {
+ assert(id != null);
+ Uri uri = uriBuilder.apply(id);
+ if (docCount++ < Shared.MAX_DOCS_IN_INTENT) {
+ DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
+ clipItems.add(new ClipData.Item(uri));
+ }
+
+ uris.add(uri);
+ }
+
+ // Prepare metadata
+ PersistableBundle bundle = new PersistableBundle();
+ bundle.putInt(OP_TYPE_KEY, opType);
+ bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
+
+ // Persists clip items and gets the slot they were saved under.
+ int tag = mClipStore.persistUris(uris);
+ bundle.putInt(OP_JUMBO_SELECTION_TAG, tag);
+
+ ClipDescription description = new ClipDescription(
+ "", // Currently "label" is not displayed anywhere in the UI.
+ clipTypes.toArray(new String[0]));
+ description.setExtras(bundle);
+
+ return createClipData(description, clipItems);
+ }
+
+ @Override
+ public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
+ ClipData data =
+ getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
+ assert(data != null);
+
+ mClipboard.setPrimaryClip(data);
+ }
+
+ @Override
+ public void clipDocumentsForCut(
+ Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
+ assert(!selection.isEmpty());
+ assert(parent.derivedUri != null);
+
+ ClipData data = getClipDataForDocuments(uriBuilder, selection,
+ FileOperationService.OPERATION_MOVE);
+ assert(data != null);
+
+ PersistableBundle bundle = data.getDescription().getExtras();
+ bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
+
+ mClipboard.setPrimaryClip(data);
+ }
+
+
+ @Override
+ public void copyFromClipboard(
+ DocumentInfo destination,
+ DocumentStack docStack,
+ FileOperations.Callback callback) {
+
+ copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
+ }
+
+ @Override
+ public void copyFromClipboard(
+ DocumentStack docStack,
+ FileOperations.Callback callback) {
+
+ copyFromClipData(docStack, mClipboard.getPrimaryClip(), callback);
+ }
+
+ @Override
+ public void copyFromClipData(
+ final RootInfo root,
+ final @Nullable DocumentInfo destination,
+ final @Nullable ClipData clipData,
+ final FileOperations.Callback callback) {
+ DocumentStack dstStack = new DocumentStack(root, destination);
+ copyFromClipData(dstStack, clipData, callback);
+ }
+
+ @Override
+ public void copyFromClipData(
+ final @Nullable DocumentInfo destination,
+ final DocumentStack docStack,
+ final @Nullable ClipData clipData,
+ final FileOperations.Callback callback) {
+
+ DocumentStack dstStack = new DocumentStack(docStack, destination);
+ copyFromClipData(dstStack, clipData, callback);
+ }
+
+ /**
+ * Copies documents from given clip data to a folder.
+ *
+ * @param dstStack the document stack to the destination folder, including the destination
+ * folder.
+ * @param clipData the clipData to copy from
+ * @param callback callback to notify when operation finishes
+ */
+ private void copyFromClipData(
+ final DocumentStack dstStack,
+ final @Nullable ClipData clipData,
+ final FileOperations.Callback callback) {
+
+ if (clipData == null) {
+ Log.i(TAG, "Received null clipData. Ignoring.");
+ return;
+ }
+
+ PersistableBundle bundle = clipData.getDescription().getExtras();
+ @OpType int opType = getOpType(bundle);
+ try {
+ if (!canCopy(dstStack.peek())) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_REJECTED, getOpType(clipData), 0);
+ return;
+ }
+
+ UrisSupplier uris = UrisSupplier.create(clipData, mClipStore);
+ if (uris.getItemCount() == 0) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
+ return;
+ }
+
+ String srcParentString = bundle.getString(SRC_PARENT_KEY);
+ Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
+
+ FileOperation operation = new FileOperation.Builder()
+ .withOpType(opType)
+ .withSrcParent(srcParent)
+ .withDestination(dstStack)
+ .withSrcs(uris)
+ .build();
+
+ FileOperations.start(mContext, operation, callback);
+ } catch(IOException e) {
+ Log.e(TAG, "Cannot create uris supplier.", e);
+ callback.onOperationResult(FileOperations.Callback.STATUS_REJECTED, opType, 0);
+ return;
+ }
+ }
+
+ /**
+ * 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 static boolean canCopy(@Nullable DocumentInfo dest) {
+ return dest != null && dest.isDirectory() && dest.isCreateSupported();
+ }
+
+ @Override
+ public @OpType int getOpType(ClipData data) {
+ PersistableBundle bundle = data.getDescription().getExtras();
+ return getOpType(bundle);
+ }
+
+ private @OpType int getOpType(PersistableBundle bundle) {
+ return bundle.getInt(OP_TYPE_KEY);
+ }
+
+ private static ClipData createClipData(
+ ClipDescription description, ArrayList<ClipData.Item> clipItems) {
+
+ // technically we want to check >= O, but we'd need to patch back the O version code :|
+ if (Features.OMC_RUNTIME) {
+ return new ClipData(description, clipItems);
+ }
+
+ ClipData clip = new ClipData(description, clipItems.get(0));
+ for (int i = 1; i < clipItems.size(); i++) {
+ clip.addItem(clipItems.get(i));
+ }
+ return clip;
+ }
+}
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index d3c19a705..588a4d0e8 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -34,7 +34,6 @@ import android.app.FragmentTransaction;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
-import android.content.Loader;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.StateListDrawable;
@@ -956,7 +955,7 @@ public class DirectoryFragment extends Fragment
ClipData clipData = event.getClipData();
assert (clipData != null);
- assert(DocumentClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
+ assert(mClipper.getOpType(clipData) == FileOperationService.OPERATION_COPY);
if (!DragAndDropHelper.canCopyTo(event.getLocalState(), getDestination(v))) {
return false;
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index 6926fb81e..f8071d2bb 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -20,6 +20,7 @@ import static com.android.documentsui.base.Shared.DEBUG;
import android.app.Activity;
import android.content.ActivityNotFoundException;
+import android.content.ClipData;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Intent;
@@ -36,7 +37,9 @@ import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.DragAndDropHelper;
import com.android.documentsui.Injector;
import com.android.documentsui.Metrics;
+import com.android.documentsui.Model;
import com.android.documentsui.R;
+import com.android.documentsui.TimeoutTask;
import com.android.documentsui.base.ConfirmationCallback;
import com.android.documentsui.base.ConfirmationCallback.Result;
import com.android.documentsui.base.DocumentFilters;
@@ -53,10 +56,8 @@ import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.dirlist.AnimationView;
import com.android.documentsui.dirlist.DocumentDetails;
-import com.android.documentsui.Model;
import com.android.documentsui.files.ActionHandler.Addons;
import com.android.documentsui.queries.SearchViewManager;
-import com.android.documentsui.roots.GetRootDocumentTask;
import com.android.documentsui.roots.RootsAccess;
import com.android.documentsui.selection.Selection;
import com.android.documentsui.services.FileOperation;
@@ -111,21 +112,26 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
@Override
public boolean dropOn(DragEvent event, RootInfo root) {
- new GetRootDocumentTask(
+ // DragEvent gets recycled, so it is possible that by the time the callback is called,
+ // event.getLocalState() and event.getClipData() returns null. Thus, we want to save
+ // references to ensure they are non null.
+ final ClipData clipData = event.getClipData();
+ final Object localState = event.getLocalState();
+ getRootDocument(
root,
- mActivity,
- (DocumentInfo rootDoc) -> dropOnCallback(event, rootDoc, root)
- ).executeOnExecutor(mExecutors.lookup(root.authority));
+ TimeoutTask.DEFAULT_TIMEOUT,
+ (DocumentInfo rootDoc) -> dropOnCallback(clipData, localState, rootDoc, root));
return true;
}
- private void dropOnCallback(DragEvent event, DocumentInfo rootDoc, RootInfo root) {
- if (!DragAndDropHelper.canCopyTo(event.getLocalState(), rootDoc)) {
+ private void dropOnCallback(
+ ClipData clipData, Object localState, DocumentInfo rootDoc, RootInfo root) {
+ if (!DragAndDropHelper.canCopyTo(localState, rootDoc)) {
return;
}
mClipper.copyFromClipData(
- root, rootDoc, event.getClipData(), mDialogs::showFileOperationStatus);
+ root, rootDoc, clipData, mDialogs::showFileOperationStatus);
}
@Override
@@ -147,17 +153,15 @@ public class ActionHandler<T extends Activity & Addons> extends AbstractActionHa
@Override
public void pasteIntoFolder(RootInfo root) {
- new GetRootDocumentTask(
+ this.getRootDocument(
root,
- mActivity,
- (DocumentInfo doc) -> pasteIntoFolder(root, doc)
- ).executeOnExecutor(mExecutors.lookup(root.authority));
+ TimeoutTask.DEFAULT_TIMEOUT,
+ (DocumentInfo doc) -> pasteIntoFolder(root, doc));
}
private void pasteIntoFolder(RootInfo root, @Nullable DocumentInfo doc) {
- DocumentClipper clipper = DocumentsApplication.getDocumentClipper(mActivity);
DocumentStack stack = new DocumentStack(root, doc);
- clipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
+ mClipper.copyFromClipboard(doc, stack, mDialogs::showFileOperationStatus);
}
@Override
diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java
index c90948859..3b0223962 100644
--- a/src/com/android/documentsui/files/FilesActivity.java
+++ b/src/com/android/documentsui/files/FilesActivity.java
@@ -17,16 +17,12 @@
package com.android.documentsui.files;
import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN;
-import static com.android.documentsui.base.Shared.DEBUG;
-import android.app.Activity;
import android.app.FragmentManager;
-import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.CallSuper;
-import android.util.Log;
import android.view.KeyEvent;
import android.view.KeyboardShortcutGroup;
import android.view.Menu;
@@ -45,15 +41,13 @@ import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.R;
import com.android.documentsui.SharedInputHandler;
import com.android.documentsui.base.DocumentInfo;
-import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.Features;
import com.android.documentsui.base.RootInfo;
-import com.android.documentsui.base.Shared;
import com.android.documentsui.base.State;
import com.android.documentsui.clipping.DocumentClipper;
import com.android.documentsui.dirlist.AnimationView.AnimationType;
-import com.android.documentsui.prefs.ScopedPreferences;
import com.android.documentsui.dirlist.DirectoryFragment;
+import com.android.documentsui.prefs.ScopedPreferences;
import com.android.documentsui.selection.SelectionManager;
import com.android.documentsui.services.FileOperationService;
import com.android.documentsui.sidebar.RootsFragment;
@@ -61,7 +55,6 @@ import com.android.documentsui.ui.DialogController;
import com.android.documentsui.ui.MessageBuilder;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
diff --git a/src/com/android/documentsui/roots/GetRootDocumentTask.java b/src/com/android/documentsui/roots/GetRootDocumentTask.java
index e7fdadf75..84b58122f 100644
--- a/src/com/android/documentsui/roots/GetRootDocumentTask.java
+++ b/src/com/android/documentsui/roots/GetRootDocumentTask.java
@@ -18,10 +18,9 @@ package com.android.documentsui.roots;
import android.annotation.Nullable;
import android.app.Activity;
-import android.app.Fragment;
-import android.content.Context;
import android.util.Log;
+import com.android.documentsui.DocumentsAccess;
import com.android.documentsui.TimeoutTask;
import com.android.documentsui.base.CheckedTask;
import com.android.documentsui.base.DocumentInfo;
@@ -39,20 +38,24 @@ public class GetRootDocumentTask extends TimeoutTask<Void, DocumentInfo> {
private final static String TAG = "GetRootDocumentTask";
private final RootInfo mRootInfo;
- private final Context mContext;
private final Consumer<DocumentInfo> mCallback;
+ private final DocumentsAccess mDocs;
public GetRootDocumentTask(
- RootInfo rootInfo, Activity activity, Consumer<DocumentInfo> callback) {
- super(activity::isDestroyed);
+ RootInfo rootInfo,
+ Activity activity,
+ long timeout,
+ DocumentsAccess docs,
+ Consumer<DocumentInfo> callback) {
+ super(activity::isDestroyed, timeout);
mRootInfo = rootInfo;
- mContext = activity.getApplicationContext();
+ mDocs = docs;
mCallback = callback;
}
@Override
- public @Nullable DocumentInfo run(Void... rootInfo) {
- return mRootInfo.getRootDocumentBlocking(mContext);
+ public @Nullable DocumentInfo run(Void... args) {
+ return mDocs.getRootDocument(mRootInfo);
}
@Override
diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java
index 1d4950f82..299ac578e 100644
--- a/src/com/android/documentsui/sidebar/RootsFragment.java
+++ b/src/com/android/documentsui/sidebar/RootsFragment.java
@@ -56,6 +56,7 @@ import com.android.documentsui.Injector;
import com.android.documentsui.Injector.Injected;
import com.android.documentsui.ItemDragListener;
import com.android.documentsui.R;
+import com.android.documentsui.TimeoutTask;
import com.android.documentsui.base.BooleanConsumer;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
@@ -473,14 +474,12 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost
private void getRootDocument(RootItem rootItem, RootUpdater updater) {
// We need to start a GetRootDocumentTask so we can know whether items can be directly
// pasted into root
- GetRootDocumentTask task = new GetRootDocumentTask(
+ mActionHandler.getRootDocument(
rootItem.root,
- getBaseActivity(),
+ CONTEXT_MENU_ITEM_TIMEOUT,
(DocumentInfo doc) -> {
updater.updateDocInfoForRoot(doc);
});
- task.setTimeout(CONTEXT_MENU_ITEM_TIMEOUT);
- task.executeOnExecutor(getBaseActivity().getExecutorForCurrentDirectory());
}
static void ejectClicked(View ejectIcon, RootInfo root, ActionHandler actionHandler) {
diff --git a/tests/common/com/android/documentsui/testing/TestDocumentClipper.java b/tests/common/com/android/documentsui/testing/TestDocumentClipper.java
new file mode 100644
index 000000000..2e5924b02
--- /dev/null
+++ b/tests/common/com/android/documentsui/testing/TestDocumentClipper.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.testing;
+
+import android.content.ClipData;
+import android.net.Uri;
+
+import com.android.documentsui.base.DocumentInfo;
+import com.android.documentsui.base.DocumentStack;
+import com.android.documentsui.base.RootInfo;
+import com.android.documentsui.clipping.DocumentClipper;
+import com.android.documentsui.selection.Selection;
+import com.android.documentsui.services.FileOperations.Callback;
+
+import java.util.function.Function;
+
+public class TestDocumentClipper implements DocumentClipper {
+
+ @Override
+ public boolean hasItemsToPaste() {
+ return false;
+ }
+
+ @Override
+ public ClipData getClipDataForDocuments(Function<String, Uri> uriBuilder, Selection selection,
+ int opType) {
+ return null;
+ }
+
+ @Override
+ public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
+
+ }
+
+ @Override
+ public void clipDocumentsForCut(Function<String, Uri> uriBuilder, Selection selection,
+ DocumentInfo parent) {
+ }
+
+ @Override
+ public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack,
+ Callback callback) {
+ }
+
+ @Override
+ public void copyFromClipboard(DocumentStack docStack, Callback callback) {
+ }
+
+ @Override
+ public void copyFromClipData(RootInfo root, DocumentInfo destination, ClipData clipData,
+ Callback callback) {
+ }
+
+ @Override
+ public void copyFromClipData(DocumentInfo destination, DocumentStack docStack,
+ ClipData clipData, Callback callback) {
+ }
+
+ @Override
+ public int getOpType(ClipData data) {
+ return 0;
+ }
+
+
+}
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 3f1eb8169..d5b7379d9 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -24,8 +24,10 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Parcelable;
@@ -33,6 +35,7 @@ import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Path;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
+import android.view.DragEvent;
import com.android.documentsui.R;
import com.android.documentsui.TestActionModeAddons;
@@ -41,9 +44,12 @@ import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.Shared;
+import com.android.documentsui.services.FileOperations.Callback;
+import com.android.documentsui.testing.ClipDatas;
import com.android.documentsui.testing.DocumentStackAsserts;
import com.android.documentsui.testing.Roots;
import com.android.documentsui.testing.TestConfirmationCallback;
+import com.android.documentsui.testing.TestDocumentClipper;
import com.android.documentsui.testing.TestEnv;
import com.android.documentsui.testing.TestRootsAccess;
import com.android.documentsui.ui.TestDialogController;
@@ -52,7 +58,9 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -65,6 +73,7 @@ public class ActionHandlerTest {
private TestConfirmationCallback mCallback;
private ActionHandler<TestActivity> mHandler;
private boolean refreshAnswer = false;
+ private ClipData mClipDataFromCallback;
@Before
public void setUp() {
@@ -81,6 +90,8 @@ public class ActionHandlerTest {
mDialogs.confirmNext();
mEnv.selectDocument(TestEnv.FILE_GIF);
+
+ mClipDataFromCallback = null;
}
@Test
@@ -352,6 +363,83 @@ public class ActionHandlerTest {
}
@Test
+ public void testClipper_suppliedCorrectClipData() throws Exception {
+ // DragEvent gets recycled in Android, so it is possible that by the time the callback is
+ // called, event.getLocalState() and event.getClipData() returns null. This tests to ensure
+ // our Clipper is getting the original CipData passed in.
+ mHandler = new ActionHandler<>(
+ mActivity,
+ mEnv.state,
+ mEnv.roots,
+ mEnv.docs,
+ mEnv.searchViewManager,
+ mEnv::lookupExecutor,
+ mActionModeAddons,
+ new TestDocumentClipper() {
+ @Override
+ public void copyFromClipData(
+ RootInfo root,
+ DocumentInfo destination,
+ ClipData clipData,
+ Callback callback) {
+ mClipDataFromCallback = clipData;
+ }
+ },
+ null,
+ mEnv.injector
+ );
+ Object localState = new Object();
+ ClipData clipData = ClipDatas.createTestClipData();
+ DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, localState, null, clipData,
+ null, true);
+ assertSame(localState, event.getLocalState());
+ assertSame(clipData, event.getClipData());
+
+ mHandler.dropOn(event, TestRootsAccess.DOWNLOADS);
+ event.recycle();
+
+ mEnv.beforeAsserts();
+
+ assertSame(clipData, mClipDataFromCallback);
+ }
+
+ @Test
+ public void testClipper_notCalledIfDestInSelection() throws Exception {
+ mHandler = new ActionHandler<>(
+ mActivity,
+ mEnv.state,
+ mEnv.roots,
+ mEnv.docs,
+ mEnv.searchViewManager,
+ mEnv::lookupExecutor,
+ mActionModeAddons,
+ new TestDocumentClipper() {
+ @Override
+ public void copyFromClipData(
+ RootInfo root,
+ DocumentInfo destination,
+ ClipData clipData,
+ Callback callback) {
+ mClipDataFromCallback = clipData;
+ }
+ },
+ null,
+ mEnv.injector
+ );
+ List<DocumentInfo> localState = new ArrayList<>();
+ localState.add(mEnv.docs.getRootDocument(TestRootsAccess.DOWNLOADS));
+ ClipData clipData = ClipDatas.createTestClipData();
+ DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, localState, null, clipData,
+ null, true);
+
+ mHandler.dropOn(event, TestRootsAccess.DOWNLOADS);
+
+ mEnv.beforeAsserts();
+
+ assertNull(mClipDataFromCallback);
+ }
+
+ @Test
public void testRefresh_nullUri() throws Exception {
refreshAnswer = true;
mHandler.refreshDocument(null, (boolean answer) -> {
@@ -404,8 +492,8 @@ public class ActionHandlerTest {
mEnv.searchViewManager,
mEnv::lookupExecutor,
mActionModeAddons,
- null, // clipper, only used in drag/drop
- null, // clip storage, not utilized unless we venture into *jumbo* clip terratory.
+ new TestDocumentClipper(),
+ null, // clip storage, not utilized unless we venture into *jumbo* clip territory.
mEnv.injector
);
}