summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/DocumentsUI/AndroidManifest.xml6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java343
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java179
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java307
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java16
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/Snackbars.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/State.java6
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java61
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java12
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java78
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java88
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java66
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java67
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java140
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/Job.java67
-rw-r--r--packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java29
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java163
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java71
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java11
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java20
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java9
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java13
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java43
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java10
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java8
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java32
-rw-r--r--packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestScheduledExecutorService.java (renamed from packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java)10
27 files changed, 1212 insertions, 649 deletions
diff --git a/packages/DocumentsUI/AndroidManifest.xml b/packages/DocumentsUI/AndroidManifest.xml
index 69912ab12098..69bcbc2d9962 100644
--- a/packages/DocumentsUI/AndroidManifest.xml
+++ b/packages/DocumentsUI/AndroidManifest.xml
@@ -113,9 +113,13 @@
</intent-filter>
</receiver>
+ <!-- Run FileOperationService in a separate process so that we can use FileLock class to
+ wait until jumbo clip is done writing to disk before reading it. See ClipStorage for
+ details. -->
<service
android:name=".services.FileOperationService"
- android:exported="false">
+ android:exported="false"
+ android:process=":com.android.documentsui.services">
</service>
</application>
</manifest>
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java b/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java
new file mode 100644
index 000000000000..6cd035376e1f
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java
@@ -0,0 +1,343 @@
+/*
+ * 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;
+
+import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_SIZE;
+import static com.android.documentsui.DocumentClipper.OP_JUMBO_SELECTION_TAG;
+import static com.android.documentsui.DocumentClipper.OP_TYPE_KEY;
+import static com.android.documentsui.DocumentClipper.SRC_PARENT_KEY;
+
+import android.annotation.CallSuper;
+import android.annotation.Nullable;
+import android.content.ClipData;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * ClipDetails is a parcelable project providing information of different type of file
+ * management operations like cut, move and copy.
+ *
+ * Under the hood it provides cross-process synchronization support such that its consumer doesn't
+ * need to explicitly synchronize its access.
+ */
+public abstract class ClipDetails implements Parcelable {
+ private final @OpType int mOpType;
+
+ // This field is used only for moving and deleting. Currently it's not the case,
+ // but in the future those files may be from multiple different parents. In
+ // such case, this needs to be replaced with pairs of parent and child.
+ private final @Nullable Uri mSrcParent;
+
+ private ClipDetails(ClipData clipData) {
+ PersistableBundle bundle = clipData.getDescription().getExtras();
+ mOpType = bundle.getInt(OP_TYPE_KEY);
+
+ String srcParentString = bundle.getString(SRC_PARENT_KEY);
+ mSrcParent = (srcParentString == null) ? null : Uri.parse(srcParentString);
+
+ // Only copy doesn't need src parent
+ assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
+ }
+
+ private ClipDetails(@OpType int opType, @Nullable Uri srcParent) {
+ mOpType = opType;
+ mSrcParent = srcParent;
+
+ // Only copy doesn't need src parent
+ assert(mOpType == FileOperationService.OPERATION_COPY || mSrcParent != null);
+ }
+
+ public @OpType int getOpType() {
+ return mOpType;
+ }
+
+ public @Nullable Uri getSrcParent() {
+ return mSrcParent;
+ }
+
+ public abstract int getItemCount();
+
+ /**
+ * Gets doc list from this clip detail. This may only be called once because it may read a file
+ * to get the list.
+ */
+ public Iterable<Uri> getDocs(Context context) throws IOException {
+ ClipStorage storage = DocumentsApplication.getClipStorage(context);
+
+ return getDocs(storage);
+ }
+
+ @VisibleForTesting
+ abstract Iterable<Uri> getDocs(ClipStorage storage) throws IOException;
+
+ public void dispose(Context context) {
+ ClipStorage storage = DocumentsApplication.getClipStorage(context);
+ dispose(storage);
+ }
+
+ @VisibleForTesting
+ void dispose(ClipStorage storage) {}
+
+ private ClipDetails(Parcel in) {
+ mOpType = in.readInt();
+ mSrcParent = in.readParcelable(ClassLoader.getSystemClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @CallSuper
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mOpType);
+ dest.writeParcelable(mSrcParent, 0);
+ }
+
+ private void appendTo(StringBuilder builder) {
+ builder.append("opType=").append(mOpType);
+ builder.append(", srcParent=").append(mSrcParent);
+ }
+
+ public static ClipDetails createClipDetails(ClipData clipData) {
+ ClipDetails details;
+ PersistableBundle bundle = clipData.getDescription().getExtras();
+ if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
+ details = new JumboClipDetails(clipData);
+ } else {
+ details = new StandardClipDetails(clipData);
+ }
+
+ return details;
+ }
+
+ public static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
+ Selection selection, Function<String, Uri> uriBuilder, Context context) {
+ ClipStorage storage = DocumentsApplication.getClipStorage(context);
+
+ List<Uri> uris = new ArrayList<>(selection.size());
+ for (String id : selection) {
+ uris.add(uriBuilder.apply(id));
+ }
+
+ return createClipDetails(opType, srcParent, uris, storage);
+ }
+
+ @VisibleForTesting
+ static ClipDetails createClipDetails(@OpType int opType, @Nullable Uri srcParent,
+ List<Uri> uris, ClipStorage storage) {
+ ClipDetails details = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
+ ? new JumboClipDetails(opType, srcParent, uris, storage)
+ : new StandardClipDetails(opType, srcParent, uris);
+
+ return details;
+ }
+
+ private static class JumboClipDetails extends ClipDetails {
+ private static final String TAG = "JumboClipDetails";
+
+ private final long mSelectionTag;
+ private final int mSelectionSize;
+
+ private transient ClipStorage.Reader mReader;
+
+ private JumboClipDetails(ClipData clipData) {
+ super(clipData);
+
+ PersistableBundle bundle = clipData.getDescription().getExtras();
+ mSelectionTag = bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
+ assert(mSelectionTag != ClipStorage.NO_SELECTION_TAG);
+
+ mSelectionSize = bundle.getInt(OP_JUMBO_SELECTION_SIZE);
+ assert(mSelectionSize > Shared.MAX_DOCS_IN_INTENT);
+ }
+
+ private JumboClipDetails(@OpType int opType, @Nullable Uri srcParent, Collection<Uri> uris,
+ ClipStorage storage) {
+ super(opType, srcParent);
+
+ mSelectionTag = storage.createTag();
+ new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
+ mSelectionSize = uris.size();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mSelectionSize;
+ }
+
+ @Override
+ public Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
+ if (mReader != null) {
+ throw new IllegalStateException(
+ "JumboClipDetails#getDocs() can only be called once.");
+ }
+
+ mReader = storage.createReader(mSelectionTag);
+
+ return mReader;
+ }
+
+ @Override
+ void dispose(ClipStorage storage) {
+ if (mReader != null) {
+ try {
+ mReader.close();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to close the reader.", e);
+ }
+ }
+ try {
+ storage.delete(mSelectionTag);
+ } catch(IOException e) {
+ Log.w(TAG, "Failed to delete clip with tag: " + mSelectionTag + ".", e);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("JumboClipDetails{");
+ super.appendTo(builder);
+ builder.append(", selectionTag=").append(mSelectionTag);
+ builder.append(", selectionSize=").append(mSelectionSize);
+ builder.append("}");
+ return builder.toString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ dest.writeLong(mSelectionTag);
+ dest.writeInt(mSelectionSize);
+ }
+
+ private JumboClipDetails(Parcel in) {
+ super(in);
+
+ mSelectionTag = in.readLong();
+ mSelectionSize = in.readInt();
+ }
+
+ public static final Parcelable.Creator<JumboClipDetails> CREATOR =
+ new Parcelable.Creator<JumboClipDetails>() {
+
+ @Override
+ public JumboClipDetails createFromParcel(Parcel source) {
+ return new JumboClipDetails(source);
+ }
+
+ @Override
+ public JumboClipDetails[] newArray(int size) {
+ return new JumboClipDetails[size];
+ }
+ };
+ }
+
+ @VisibleForTesting
+ public static class StandardClipDetails extends ClipDetails {
+ private final List<Uri> mDocs;
+
+ private StandardClipDetails(ClipData clipData) {
+ super(clipData);
+ mDocs = listDocs(clipData);
+ }
+
+ @VisibleForTesting
+ public StandardClipDetails(@OpType int opType, @Nullable Uri srcParent, List<Uri> docs) {
+ super(opType, srcParent);
+
+ mDocs = docs;
+ }
+
+ private List<Uri> listDocs(ClipData clipData) {
+ ArrayList<Uri> docs = new ArrayList<>(clipData.getItemCount());
+
+ for (int i = 0; i < clipData.getItemCount(); ++i) {
+ Uri uri = clipData.getItemAt(i).getUri();
+ assert(uri != null);
+ docs.add(uri);
+ }
+
+ return docs;
+ }
+
+ @Override
+ public int getItemCount() {
+ return mDocs.size();
+ }
+
+ @Override
+ public Iterable<Uri> getDocs(ClipStorage storage) {
+ return mDocs;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ builder.append("StandardClipDetails{");
+ super.appendTo(builder);
+ builder.append(", ").append("docs=").append(mDocs.toString());
+ builder.append("}");
+ return builder.toString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+
+ dest.writeTypedList(mDocs);
+ }
+
+ private StandardClipDetails(Parcel in) {
+ super(in);
+
+ mDocs = in.createTypedArrayList(Uri.CREATOR);
+ }
+
+ public static final Parcelable.Creator<StandardClipDetails> CREATOR =
+ new Parcelable.Creator<StandardClipDetails>() {
+
+ @Override
+ public StandardClipDetails createFromParcel(Parcel source) {
+ return new StandardClipDetails(source);
+ }
+
+ @Override
+ public StandardClipDetails[] newArray(int size) {
+ return new StandardClipDetails[size];
+ }
+ };
+ }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java b/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java
index 0167accc352d..5102718e8667 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java
@@ -17,16 +17,17 @@
package com.android.documentsui;
import android.net.Uri;
+import android.os.AsyncTask;
import android.support.annotation.VisibleForTesting;
+import android.util.Log;
-import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileOutputStream;
-import java.io.FileReader;
import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
+import java.nio.channels.FileLock;
+import java.util.Scanner;
/**
* Provides support for storing lists of documents identified by Uri.
@@ -36,9 +37,10 @@ import java.util.List;
*/
public final class ClipStorage {
- private static final String PRIMARY_SELECTION = "primary-selection.txt";
+ private static final String TAG = "ClipStorage";
+
private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
- private static final int NO_SELECTION_TAG = -1;
+ public static final long NO_SELECTION_TAG = -1;
private final File mOutDir;
@@ -51,46 +53,27 @@ public final class ClipStorage {
}
/**
- * Returns a writer. Callers must...
+ * Creates a clip tag.
*
- * <li>synchronize on the {@link ClipStorage} instance while writing to this writer.
- * <li>closed the write when finished.
+ * NOTE: this tag doesn't guarantee perfect uniqueness, but should work well unless user creates
+ * clips more than hundreds of times per second.
*/
- public Writer createWriter() throws IOException {
- File primary = new File(mOutDir, PRIMARY_SELECTION);
- return new Writer(new FileOutputStream(primary));
+ public long createTag() {
+ return System.currentTimeMillis();
}
/**
- * Saves primary uri list to persistent storage.
- * @return tag identifying the saved set.
+ * Returns a writer. Callers must close the writer when finished.
*/
- @VisibleForTesting
- public long savePrimary() throws IOException {
- File primary = new File(mOutDir, PRIMARY_SELECTION);
-
- if (!primary.exists()) {
- return NO_SELECTION_TAG;
- }
-
- long tag = System.currentTimeMillis();
- File dest = toTagFile(tag);
- primary.renameTo(dest);
-
- return tag;
+ public Writer createWriter(long tag) throws IOException {
+ File file = toTagFile(tag);
+ return new Writer(file);
}
@VisibleForTesting
- public List<Uri> read(long tag) throws IOException {
- List<Uri> uris = new ArrayList<>();
- File tagFile = toTagFile(tag);
- try (BufferedReader in = new BufferedReader(new FileReader(tagFile))) {
- String line = null;
- while ((line = in.readLine()) != null) {
- uris.add(Uri.parse(line));
- }
- }
- return uris;
+ public Reader createReader(long tag) throws IOException {
+ File file = toTagFile(tag);
+ return new Reader(file);
}
@VisibleForTesting
@@ -102,12 +85,87 @@ public final class ClipStorage {
return new File(mOutDir, String.valueOf(tag));
}
- public static final class Writer implements Closeable {
+ /**
+ * Provides initialization of the clip data storage directory.
+ */
+ static File prepareStorage(File cacheDir) {
+ File clipDir = getClipDir(cacheDir);
+ clipDir.mkdir();
+
+ assert(clipDir.isDirectory());
+ return clipDir;
+ }
+
+ public static boolean hasDocList(long tag) {
+ return tag != NO_SELECTION_TAG;
+ }
+
+ private static File getClipDir(File cacheDir) {
+ return new File(cacheDir, "clippings");
+ }
+
+ static final class Reader implements Iterable<Uri>, Closeable {
+
+ private final Scanner mScanner;
+ private final FileLock mLock;
+
+ private Reader(File file) throws IOException {
+ FileInputStream inStream = new FileInputStream(file);
+
+ // Lock the file here so it won't pass this line until the corresponding writer is done
+ // writing.
+ mLock = inStream.getChannel().lock(0L, Long.MAX_VALUE, true);
+
+ mScanner = new Scanner(inStream);
+ }
+
+ @Override
+ public Iterator iterator() {
+ return new Iterator(mScanner);
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (mLock != null) {
+ mLock.release();
+ }
+
+ if (mScanner != null) {
+ mScanner.close();
+ }
+ }
+ }
+
+ private static final class Iterator implements java.util.Iterator {
+ private final Scanner mScanner;
+
+ private Iterator(Scanner scanner) {
+ mScanner = scanner;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return mScanner.hasNextLine();
+ }
+
+ @Override
+ public Uri next() {
+ String line = mScanner.nextLine();
+ return Uri.parse(line);
+ }
+ }
+
+ private static final class Writer implements Closeable {
private final FileOutputStream mOut;
+ private final FileLock mLock;
+
+ private Writer(File file) throws IOException {
+ mOut = new FileOutputStream(file);
- public Writer(FileOutputStream out) {
- mOut = out;
+ // Lock the file here so copy tasks would wait until everything is flushed to disk
+ // before start to run.
+ mLock = mOut.getChannel().lock();
}
public void write(Uri uri) throws IOException {
@@ -117,20 +175,43 @@ public final class ClipStorage {
@Override
public void close() throws IOException {
- mOut.close();
+ if (mLock != null) {
+ mLock.release();
+ }
+
+ if (mOut != null) {
+ mOut.close();
+ }
}
}
/**
- * Provides initialization and cleanup of the clip data storage directory.
+ * An {@link AsyncTask} that persists doc uris in {@link ClipStorage}.
*/
- static File prepareStorage(File cacheDir) {
- File clipDir = new File(cacheDir, "clippings");
- if (clipDir.exists()) {
- Files.deleteRecursively(clipDir);
+ static final class PersistTask extends AsyncTask<Void, Void, Void> {
+
+ private final ClipStorage mClipStorage;
+ private final Iterable<Uri> mUris;
+ private final long mTag;
+
+ PersistTask(ClipStorage clipStorage, Iterable<Uri> uris, long tag) {
+ mClipStorage = clipStorage;
+ mUris = uris;
+ mTag = tag;
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try (ClipStorage.Writer writer = mClipStorage.createWriter(mTag)) {
+ for (Uri uri: mUris) {
+ assert(uri != null);
+ writer.write(uri);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
+ }
+
+ return null;
}
- assert(!clipDir.exists());
- clipDir.mkdir();
- return clipDir;
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
index 3d8ac2c936a6..4c103c42c2d5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
@@ -21,25 +21,23 @@ import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.SharedPreferences;
import android.net.Uri;
-import android.os.AsyncTask;
+import android.os.BaseBundle;
import android.os.PersistableBundle;
import android.provider.DocumentsContract;
import android.support.annotation.Nullable;
import android.util.Log;
-import com.android.documentsui.ClipStorage.Writer;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
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 com.android.documentsui.services.FileOperations;
import java.io.IOException;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -49,26 +47,49 @@ import java.util.function.Function;
* ClipboardManager wrapper class providing higher level logical
* support for dealing with Documents.
*/
-public final class DocumentClipper {
+public final class DocumentClipper implements ClipboardManager.OnPrimaryClipChangedListener {
private static final String TAG = "DocumentClipper";
- private static final String SRC_PARENT_KEY = "srcParent";
- private static final String OP_TYPE_KEY = "opType";
- private static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
+
+ 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";
+
+ // Use shared preference to store last seen primary clip tag, so that we can delete the file
+ // when we realize primary clip has been changed when we're not running.
+ private static final String PREF_NAME = "DocumentClipperPref";
+ private static final String LAST_PRIMARY_CLIP_TAG = "lastPrimaryClipTag";
private final Context mContext;
private final ClipStorage mClipStorage;
private final ClipboardManager mClipboard;
+ // Here we're tracking the last clipped tag ids so we can delete them later.
+ private long mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
+ private long mLastUnusedPrimaryClipTag = ClipStorage.NO_SELECTION_TAG;
+
+ private final SharedPreferences mPref;
+
DocumentClipper(Context context, ClipStorage storage) {
mContext = context;
mClipStorage = storage;
mClipboard = context.getSystemService(ClipboardManager.class);
+
+ mClipboard.addPrimaryClipChangedListener(this);
+
+ // Primary clips may be changed when we're not running, now it's time to clean up the
+ // remnant.
+ mPref = context.getSharedPreferences(PREF_NAME, 0);
+ mLastUnusedPrimaryClipTag =
+ mPref.getLong(LAST_PRIMARY_CLIP_TAG, ClipStorage.NO_SELECTION_TAG);
+ deleteLastUnusedPrimaryClip();
}
public boolean hasItemsToPaste() {
if (mClipboard.hasPrimaryClip()) {
ClipData clipData = mClipboard.getPrimaryClip();
+
int count = clipData.getItemCount();
if (count > 0) {
for (int i = 0; i < count; ++i) {
@@ -87,51 +108,27 @@ public final class DocumentClipper {
return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
}
- public ClipDetails getClipDetails(@Nullable ClipData clipData) {
- if (clipData == null) {
- return null;
- }
-
- String srcParent = clipData.getDescription().getExtras().getString(SRC_PARENT_KEY);
+ /**
+ * Returns {@link ClipData} representing the selection, or null if selection is empty,
+ * or cannot be converted.
+ *
+ * This is specialized for drag and drop so that we know which file to delete if nobody accepts
+ * the drop.
+ */
+ public @Nullable ClipData getClipDataForDrag(
+ Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+ ClipData data = getClipDataForDocuments(uriBuilder, selection, opType);
- ClipDetails clipDetails = new ClipDetails(
- clipData.getDescription().getExtras().getInt(OP_TYPE_KEY),
- getDocumentsFromClipData(clipData),
- createDocument((srcParent != null) ? Uri.parse(srcParent) : null));
+ mLastDragClipTag = getTag(data);
- return clipDetails;
- }
-
- private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
- assert(clipData != null);
-
- int count = clipData.getItemCount();
- if (count == 0) {
- return Collections.EMPTY_LIST;
- }
-
- final List<DocumentInfo> srcDocs = new ArrayList<>();
-
- for (int i = 0; i < count; ++i) {
- ClipData.Item item = clipData.getItemAt(i);
- Uri itemUri = item.getUri();
- 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;
+ return data;
}
/**
* Returns {@link ClipData} representing the selection, or null if selection is empty,
* or cannot be converted.
*/
- public @Nullable ClipData getClipDataForDocuments(
+ private @Nullable ClipData getClipDataForDocuments(
Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
assert(selection != null);
@@ -153,6 +150,7 @@ public final class DocumentClipper {
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<>();
@@ -161,15 +159,11 @@ public final class DocumentClipper {
PersistableBundle bundle = new PersistableBundle();
bundle.putInt(OP_TYPE_KEY, opType);
- int clipCount = 0;
for (String id : selection) {
assert(id != null);
Uri uri = uriBuilder.apply(id);
- if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
- DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
- clipItems.add(new ClipData.Item(uri));
- }
- clipCount++;
+ DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
+ clipItems.add(new ClipData.Item(uri));
}
ClipDescription description = new ClipDescription(
@@ -181,46 +175,51 @@ public final class DocumentClipper {
}
/**
- * Returns ClipData representing the list of docs, or null if docs is empty,
- * or docs cannot be converted.
+ * Returns ClipData representing the list of docs
*/
private @Nullable 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 ArrayList<ClipData.Item> clipItems = new ArrayList<>();
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());
- int clipCount = 0;
- synchronized (mClipStorage) {
- try (Writer writer = mClipStorage.createWriter()) {
- for (String id : selection) {
- assert(id != null);
- Uri uri = uriBuilder.apply(id);
- if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
- DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
- clipItems.add(new ClipData.Item(uri));
- }
- writer.write(uri);
- clipCount++;
- }
- } catch (IOException e) {
- Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
- return null;
- }
- }
+ // Creates a clip tag
+ long tag = mClipStorage.createTag();
+ bundle.putLong(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);
+ // Persists clip items
+ new ClipStorage.PersistTask(mClipStorage, uris, tag).execute();
+
return new ClipData(description, clipItems);
}
@@ -232,7 +231,7 @@ public final class DocumentClipper {
getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
assert(data != null);
- mClipboard.setPrimaryClip(data);
+ setPrimaryClip(data);
}
/**
@@ -250,20 +249,65 @@ public final class DocumentClipper {
PersistableBundle bundle = data.getDescription().getExtras();
bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
+ setPrimaryClip(data);
+ }
+
+ private void setPrimaryClip(ClipData data) {
+ deleteLastPrimaryClip();
+
+ long tag = getTag(data);
+ setLastUnusedPrimaryClipTag(tag);
+
mClipboard.setPrimaryClip(data);
}
- private DocumentInfo createDocument(Uri uri) {
- DocumentInfo doc = null;
- if (isDocumentUri(uri)) {
- ContentResolver resolver = mContext.getContentResolver();
- try {
- doc = DocumentInfo.fromUri(resolver, uri);
- } catch (Exception e) {
- Log.e(TAG, e.getMessage());
- }
+ /**
+ * Sets this primary tag to both class variable and shared preference.
+ */
+ private void setLastUnusedPrimaryClipTag(long tag) {
+ mLastUnusedPrimaryClipTag = tag;
+ mPref.edit().putLong(LAST_PRIMARY_CLIP_TAG, tag).commit();
+ }
+
+ /**
+ * This is a good chance for us to remove previous clip file for cut/copy because we know a new
+ * primary clip is set.
+ */
+ @Override
+ public void onPrimaryClipChanged() {
+ deleteLastUnusedPrimaryClip();
+ }
+
+ private void deleteLastUnusedPrimaryClip() {
+ ClipData primary = mClipboard.getPrimaryClip();
+ long primaryTag = getTag(primary);
+
+ // onPrimaryClipChanged is also called after we call setPrimaryClip(), so make sure we don't
+ // delete the clip file we just created.
+ if (mLastUnusedPrimaryClipTag != primaryTag) {
+ deleteLastPrimaryClip();
+ }
+ }
+
+ private void deleteLastPrimaryClip() {
+ deleteClip(mLastUnusedPrimaryClipTag);
+ setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
+ }
+
+ /**
+ * Deletes the last seen drag clip file.
+ */
+ public void deleteDragClip() {
+ deleteClip(mLastDragClipTag);
+ mLastDragClipTag = ClipStorage.NO_SELECTION_TAG;
+ }
+
+ private void deleteClip(long tag) {
+ try {
+ mClipStorage.delete(tag);
+ } catch (IOException e) {
+ Log.w(TAG, "Error deleting clip file with tag: " + tag, e);
}
- return doc;
}
/**
@@ -279,6 +323,10 @@ public final class DocumentClipper {
DocumentStack docStack,
FileOperations.Callback callback) {
+ // The primary clip has been claimed by a file operation. It's now the operation's duty
+ // to make sure the clip file is deleted after use.
+ setLastUnusedPrimaryClipTag(ClipStorage.NO_SELECTION_TAG);
+
copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
}
@@ -301,65 +349,28 @@ public final class DocumentClipper {
return;
}
- new AsyncTask<Void, Void, ClipDetails>() {
-
- @Override
- protected ClipDetails doInBackground(Void... params) {
- return getClipDetails(clipData);
- }
+ ClipDetails details = ClipDetails.createClipDetails(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);
+ if (!canCopy(destination)) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0);
return;
}
- if (docs.isEmpty()) {
- callback.onOperationResult(FileOperations.Callback.STATUS_ACCEPTED, operationType, 0);
+ if (details.getItemCount() == 0) {
+ callback.onOperationResult(
+ FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 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);
- }
+
+ // Pass root here so that we can perform "download" root check when
+ dstStack.root = docStack.root;
+
+ FileOperations.start(mContext, details, dstStack, callback);
}
/**
@@ -370,32 +381,26 @@ public final class DocumentClipper {
*
* @return true if the list of files can be copied to destination.
*/
- private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
+ private static boolean canCopy(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;
- public final @Nullable DocumentInfo parent;
-
- ClipDetails(@OpType int opType, List<DocumentInfo> docs, @Nullable DocumentInfo parent) {
- this.opType = opType;
- this.docs = docs;
- this.parent = parent;
+ /**
+ * Obtains tag from {@link ClipData}. Returns {@link ClipStorage#NO_SELECTION_TAG}
+ * if it's not a jumbo clip.
+ */
+ private static long getTag(@Nullable ClipData data) {
+ if (data == null) {
+ return ClipStorage.NO_SELECTION_TAG;
}
+
+ ClipDescription description = data.getDescription();
+ BaseBundle bundle = description.getExtras();
+ return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
}
+
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
index 2b2d1f40060b..3d3902d9968b 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
@@ -28,14 +28,13 @@ import android.net.Uri;
import android.os.RemoteException;
import android.text.format.DateUtils;
-import java.io.File;
-
public class DocumentsApplication extends Application {
private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
private RootsCache mRoots;
private ThumbnailCache mThumbnailCache;
+ private ClipStorage mClipStorage;
private DocumentClipper mClipper;
public static RootsCache getRootsCache(Context context) {
@@ -62,6 +61,10 @@ public class DocumentsApplication extends Application {
return ((DocumentsApplication) context.getApplicationContext()).mClipper;
}
+ public static ClipStorage getClipStorage(Context context) {
+ return ((DocumentsApplication) context.getApplicationContext()).mClipStorage;
+ }
+
@Override
public void onCreate() {
super.onCreate();
@@ -74,7 +77,8 @@ public class DocumentsApplication extends Application {
mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);
- mClipper = createClipper(this.getApplicationContext());
+ mClipStorage = new ClipStorage(ClipStorage.prepareStorage(getCacheDir()));
+ mClipper = new DocumentClipper(this, mClipStorage);
final IntentFilter packageFilter = new IntentFilter();
packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -89,12 +93,6 @@ public class DocumentsApplication extends Application {
registerReceiver(mCacheReceiver, localeFilter);
}
- private static DocumentClipper createClipper(Context context) {
- // prepare storage handles initialization and cleanup of the clip directory.
- File clipDir = ClipStorage.prepareStorage(context.getCacheDir());
- return new DocumentClipper(context, new ClipStorage(clipDir));
- }
-
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java
index 4274efaed5d8..c3a82d779555 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Snackbars.java
@@ -24,6 +24,12 @@ import android.view.View;
public final class Snackbars {
private Snackbars() {}
+ public static final void showDocumentsClipped(Activity activity, int docCount) {
+ String msg = Shared.getQuantityString(
+ activity, R.plurals.clipboard_files_clipped, docCount);
+ Snackbars.makeSnackbar(activity, msg, Snackbar.LENGTH_SHORT).show();
+ }
+
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();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/State.java b/packages/DocumentsUI/src/com/android/documentsui/State.java
index f239eb45552b..9cdf1a804ced 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/State.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/State.java
@@ -25,7 +25,6 @@ import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
-import com.android.documentsui.dirlist.MultiSelectManager.Selection;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.DurableUtils;
@@ -123,9 +122,6 @@ public class State implements android.os.Parcelable {
/** Instance state for every shown directory */
public HashMap<String, SparseArray<Parcelable>> dirState = new HashMap<>();
- /** Currently copying file */
- public List<DocumentInfo> selectedDocumentsForCopy = new ArrayList<>();
-
/** Name of the package that started DocsUI */
public List<String> excludedAuthorities = new ArrayList<>();
@@ -199,7 +195,6 @@ public class State implements android.os.Parcelable {
out.writeInt(external ? 1 : 0);
DurableUtils.writeToParcel(out, stack);
out.writeMap(dirState);
- out.writeList(selectedDocumentsForCopy);
out.writeList(excludedAuthorities);
out.writeInt(openableOnly ? 1 : 0);
out.writeInt(mStackTouched ? 1 : 0);
@@ -229,7 +224,6 @@ public class State implements android.os.Parcelable {
state.external = in.readInt() != 0;
DurableUtils.readFromParcel(in, state.stack);
in.readMap(state.dirState, loader);
- in.readList(state.selectedDocumentsForCopy, loader);
in.readList(state.excludedAuthorities, loader);
state.openableOnly = in.readInt() != 0;
state.mStackTouched = in.readInt() != 0;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java
deleted file mode 100644
index 3aefffbb2f13..000000000000
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * 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.dirlist;
-
-import android.app.Activity;
-import android.os.AsyncTask;
-import android.support.design.widget.Snackbar;
-
-import com.android.documentsui.R;
-import com.android.documentsui.Shared;
-import com.android.documentsui.Snackbars;
-
-/**
- * AsyncTask that performs a supplied runnable (presumably doing some clippy thing)in background,
- * then shows a toast reciting how many fantastic things have been clipped.
- */
-final class ClipTask extends AsyncTask<Void, Void, Void> {
-
- private Runnable mOperation;
- private int mSelectionSize;
- private Activity mActivity;
-
- ClipTask(Activity activity, Runnable operation, int selectionSize) {
- mActivity = activity;
- mOperation = operation;
- mSelectionSize = selectionSize;
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- // Clip operation varies (cut or past) and has different inputs.
- // To increase sharing we accept the no ins/outs operation as a plain runnable.
- mOperation.run();
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- String msg = Shared.getQuantityString(
- mActivity,
- R.plurals.clipboard_files_clipped,
- mSelectionSize);
-
- Snackbars.makeSnackbar(mActivity, msg, Snackbar.LENGTH_SHORT)
- .show();
- }
-}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java
index e8361a1c9f5b..40ee14db2ce3 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryDragListener.java
@@ -31,8 +31,16 @@ class DirectoryDragListener extends ItemDragListener<DirectoryFragment> {
public boolean onDrag(View v, DragEvent event) {
final boolean result = super.onDrag(v, event);
- if (event.getAction() == DragEvent.ACTION_DRAG_ENDED && event.getResult()) {
- mDragHost.clearSelection();
+ if (event.getAction() == DragEvent.ACTION_DRAG_ENDED) {
+ // getResult() is true if drag was accepted
+ if (event.getResult()) {
+ mDragHost.clearSelection();
+ } else {
+ // When drag starts we might write a new clip file to disk.
+ // No drop event happens, remove clip file here. This may be called multiple times,
+ // but it should be OK because deletion is idempotent and cheap.
+ mDragHost.deleteDragClipFile();
+ }
}
return result;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 6fcf24ce647e..afdcdf1cb228 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -72,10 +72,10 @@ import android.widget.TextView;
import android.widget.Toolbar;
import com.android.documentsui.BaseActivity;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.DirectoryLoader;
import com.android.documentsui.DirectoryResult;
import com.android.documentsui.DocumentClipper;
-import com.android.documentsui.DocumentClipper.ClipDetails;
import com.android.documentsui.DocumentsActivity;
import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.Events;
@@ -96,7 +96,6 @@ import com.android.documentsui.State;
import com.android.documentsui.State.ViewMode;
import com.android.documentsui.dirlist.MultiSelectManager.Selection;
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;
@@ -171,6 +170,9 @@ public class DirectoryFragment extends Fragment
// Note, we use !null to indicate that selection was restored (from rotation).
// So don't fiddle with this field unless you've got the bigger picture in mind.
private @Nullable Selection mRestoredSelection = null;
+ // Here we save the clip details of moveTo/copyTo actions when picker shows up.
+ // This will be written to saved instance.
+ private @Nullable ClipDetails mDetailsForCopy;
private boolean mSearchMode = false;
private @Nullable BandController mBandController;
@@ -200,6 +202,9 @@ public class DirectoryFragment extends Fragment
case FileOperationService.OPERATION_COPY:
Snackbars.showCopy(getActivity(), docCount);
break;
+ case FileOperationService.OPERATION_DELETE:
+ // We don't show anything for deletion.
+ break;
default:
throw new UnsupportedOperationException("Unsupported Operation: " + opType);
}
@@ -264,6 +269,7 @@ public class DirectoryFragment extends Fragment
mQuery = args.getString(Shared.EXTRA_QUERY);
mType = args.getInt(Shared.EXTRA_TYPE);
mSearchMode = args.getBoolean(Shared.EXTRA_SEARCH_MODE);
+ mDetailsForCopy = args.getParcelable(FileOperationService.EXTRA_CLIP_DETAILS);
// Restore any selection we may have squirreled away in retained state.
@Nullable RetainedState retained = getBaseActivity().getRetainedState();
@@ -353,6 +359,7 @@ public class DirectoryFragment extends Fragment
outState.putParcelable(Shared.EXTRA_DOC, mDocument);
outState.putString(Shared.EXTRA_QUERY, mQuery);
outState.putBoolean(Shared.EXTRA_SEARCH_MODE, mSearchMode);
+ outState.putParcelable(FileOperationService.EXTRA_CLIP_DETAILS, mDetailsForCopy);
}
@Override
@@ -392,22 +399,21 @@ public class DirectoryFragment extends Fragment
}
private void handleCopyResult(int resultCode, Intent data) {
+
+ ClipDetails details = mDetailsForCopy;
+ mDetailsForCopy = null;
+
if (resultCode == Activity.RESULT_CANCELED || data == null) {
// User pressed the back button or otherwise cancelled the destination pick. Don't
// proceed with the copy.
+ details.dispose(getContext());
return;
}
- @OpType int operationType = data.getIntExtra(
- FileOperationService.EXTRA_OPERATION,
- FileOperationService.OPERATION_COPY);
-
FileOperations.start(
getContext(),
- getDisplayState().selectedDocumentsForCopy,
- getDisplayState().stack.peek(),
- (DocumentStack) data.getParcelableExtra(Shared.EXTRA_STACK),
- operationType,
+ details,
+ data.getParcelableExtra(Shared.EXTRA_STACK),
mFileOpCallback);
}
@@ -984,8 +990,14 @@ public class DirectoryFragment extends Fragment
Log.w(TAG, "Action mode is null before deleting documents.");
}
- FileOperations.delete(
- getActivity(), docs, srcParent, getDisplayState().stack);
+ ClipDetails details = ClipDetails.createClipDetails(
+ FileOperationService.OPERATION_DELETE,
+ srcParent.derivedUri,
+ selected,
+ mModel::getItemUri,
+ getContext());
+ FileOperations.start(getActivity(), details,
+ getDisplayState().stack, mFileOpCallback);
}
})
.setNegativeButton(android.R.string.cancel, null)
@@ -1009,6 +1021,9 @@ public class DirectoryFragment extends Fragment
getActivity(),
DocumentsActivity.class);
+ Uri srcParent = getDisplayState().stack.peek().derivedUri;
+ mDetailsForCopy = ClipDetails.createClipDetails(
+ mode, srcParent, selected, mModel::getItemUri, getContext());
// Relay any config overrides bits present in the original intent.
Intent original = getActivity().getIntent();
@@ -1028,15 +1043,11 @@ public class DirectoryFragment extends Fragment
new GetDocumentsTask() {
@Override
void onDocumentsReady(List<DocumentInfo> docs) {
- // TODO: Can this move to Fragment bundle state?
- getDisplayState().selectedDocumentsForCopy = docs;
-
// Determine if there is a directory in the set of documents
// to be copied? Why? Directory creation isn't supported by some roots
// (like Downloads). This informs DocumentsActivity (the "picker")
// to restrict available roots to just those with support.
intent.putExtra(Shared.EXTRA_DIRECTORY_COPY, hasDirectory(docs));
- intent.putExtra(FileOperationService.EXTRA_OPERATION, mode);
// This just identifies the type of request...we'll check it
// when we reveive a response.
@@ -1164,13 +1175,9 @@ public class DirectoryFragment extends Fragment
}
mSelectionManager.clearSelection();
- // Clips the docs in the background, then displays a message
- new ClipTask(
- getActivity(),
- () -> {
- mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
- },
- selection.size()).execute();
+ mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
+
+ Snackbars.showDocumentsClipped(getActivity(), selection.size());
}
public void cutSelectedToClipboard() {
@@ -1182,16 +1189,9 @@ public class DirectoryFragment extends Fragment
}
mSelectionManager.clearSelection();
- // Clips the docs in the background, then displays a message
- new ClipTask(
- getActivity(),
- () -> {
- mClipper.clipDocumentsForCut(
- mModel::getItemUri,
- selection,
- getDisplayState().stack.peek());
- },
- selection.size()).execute();
+ mClipper.clipDocumentsForCut(mModel::getItemUri, selection, getDisplayState().stack.peek());
+
+ Snackbars.showDocumentsClipped(getActivity(), selection.size());
}
public void pasteFromClipboard() {
@@ -1273,15 +1273,19 @@ public class DirectoryFragment extends Fragment
activity.setRootsDrawerOpen(false);
}
- public boolean handleDropEvent(View v, DragEvent event) {
+ void deleteDragClipFile() {
+ mClipper.deleteDragClip();
+ }
+
+ boolean handleDropEvent(View v, DragEvent event) {
BaseActivity activity = (BaseActivity) getActivity();
activity.setRootsDrawerOpen(false);
ClipData clipData = event.getClipData();
assert (clipData != null);
- ClipDetails clipDetails = mClipper.getClipDetails(clipData);
- assert(clipDetails.opType == FileOperationService.OPERATION_COPY);
+ assert(ClipDetails.createClipDetails(clipData).getOpType()
+ == FileOperationService.OPERATION_COPY);
// Don't copy from the cwd into the cwd. Note: this currently doesn't work for
// multi-window drag, because localState isn't carried over from one process to
@@ -1478,7 +1482,7 @@ public class DirectoryFragment extends Fragment
// the current code layout and framework assumptions don't support
// this. So for now, we could end up doing a bunch of i/o on main thread.
v.startDragAndDrop(
- mClipper.getClipDataForDocuments(
+ mClipper.getClipDataForDrag(
mModel::getItemUri,
selection,
FileOperationService.OPERATION_COPY),
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index cde9e92141ad..fac8667aea8f 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -29,13 +29,13 @@ import static com.android.documentsui.model.DocumentInfo.getCursorString;
import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
-import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
import android.annotation.StringRes;
import android.app.Notification;
import android.app.Notification.Builder;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
@@ -50,11 +50,12 @@ import android.text.format.DateUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
-import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.model.RootInfo;
import libcore.io.IoUtils;
@@ -83,30 +84,18 @@ class CopyJob extends Job {
private long mRemainingTime;
/**
- * Copies files to a destination identified by {@code destination}.
* @see @link {@link Job} constructor for most param descriptions.
*
- * @param srcs List of files to be copied.
+ * @param details clip details containing source file list
*/
CopyJob(Context service, Context appContext, Listener listener,
- String id, DocumentStack stack, List<DocumentInfo> srcs) {
- super(service, appContext, listener, OPERATION_COPY, id, stack);
+ String id, DocumentStack destination, ClipDetails details) {
+ super(service, appContext, listener, id, destination, details);
- assert(!srcs.isEmpty());
- this.mSrcs = srcs;
- }
+ assert(details.getItemCount() > 0);
- /**
- * @see @link {@link Job} constructor for most param descriptions.
- *
- * @param srcs List of files to be copied.
- */
- CopyJob(Context service, Context appContext, Listener listener,
- @OpType int opType, String id, DocumentStack destination, List<DocumentInfo> srcs) {
- super(service, appContext, listener, opType, id, destination);
-
- assert(!srcs.isEmpty());
- this.mSrcs = srcs;
+ // delay the initialization of it to setUp() because it may be IO extensive.
+ mSrcs = new ArrayList<>(details.getItemCount());
}
@Override
@@ -167,7 +156,7 @@ class CopyJob extends Job {
// mBytesCopied is modified in worker thread, but this method is called in monitor thread,
// so take a snapshot of mBytesCopied to make sure the updated estimate is consistent.
final long bytesCopied = mBytesCopied;
- final long sampleDuration = elapsedTime - mSampleTime;
+ final long sampleDuration = Math.max(elapsedTime - mSampleTime, 1L); // avoid dividing 0
final long sampleSpeed = ((bytesCopied - mBytesCopiedSample) * 1000) / sampleDuration;
if (mSpeed == 0) {
mSpeed = sampleSpeed;
@@ -215,8 +204,18 @@ class CopyJob extends Job {
}
@Override
- void start() {
- mStartTime = elapsedRealtime();
+ boolean setUp() {
+
+ try {
+ buildDocumentList();
+ } catch (ResourceException e) {
+ Log.e(TAG, "Failed to get the list of docs.", e);
+ return false;
+ }
+
+ if (isCanceled()) {
+ return false;
+ }
try {
mBatchSize = calculateSize(mSrcs);
@@ -225,6 +224,12 @@ class CopyJob extends Job {
mBatchSize = -1;
}
+ return true;
+ }
+
+ @Override
+ void start() {
+ mStartTime = elapsedRealtime();
DocumentInfo srcInfo;
DocumentInfo dstInfo = stack.peek();
for (int i = 0; i < mSrcs.size() && !isCanceled(); ++i) {
@@ -249,6 +254,33 @@ class CopyJob extends Job {
Metrics.logFileOperation(service, operationType, mSrcs, dstInfo);
}
+ private void buildDocumentList() throws ResourceException {
+ try {
+ final ContentResolver resolver = appContext.getContentResolver();
+ final Iterable<Uri> uris = details.getDocs(appContext);
+ for (Uri uri : uris) {
+ DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
+ if (canCopy(doc, stack.root)) {
+ mSrcs.add(doc);
+ } else {
+ onFileFailed(doc);
+ }
+
+ if (isCanceled()) {
+ return;
+ }
+ }
+ } catch(IOException e) {
+ failedFileCount += details.getItemCount();
+ throw new ResourceException("Failed to open the list of docs to copy.", e);
+ }
+ }
+
+ private static boolean canCopy(DocumentInfo doc, RootInfo root) {
+ // Can't copy folders to downloads, because we don't show folders there.
+ return !root.isDownloads() || !doc.isDirectory();
+ }
+
@Override
boolean hasWarnings() {
return !convertedFiles.isEmpty();
@@ -553,6 +585,10 @@ class CopyJob extends Job {
} else {
result += src.size;
}
+
+ if (isCanceled()) {
+ return result;
+ }
}
return result;
}
@@ -562,7 +598,7 @@ class CopyJob extends Job {
*
* @throws ResourceException
*/
- private static long calculateFileSizesRecursively(
+ private long calculateFileSizesRecursively(
ContentProviderClient client, Uri uri) throws ResourceException {
final String authority = uri.getAuthority();
final Uri queryUri = buildChildDocumentsUri(authority, getDocumentId(uri));
@@ -576,7 +612,7 @@ class CopyJob extends Job {
Cursor cursor = null;
try {
cursor = client.query(queryUri, queryColumns, null, null, null);
- while (cursor.moveToNext()) {
+ while (cursor.moveToNext() && !isCanceled()) {
if (Document.MIME_TYPE_DIR.equals(
getCursorString(cursor, Document.COLUMN_MIME_TYPE))) {
// Recurse into directories.
@@ -623,7 +659,7 @@ class CopyJob extends Job {
.append("CopyJob")
.append("{")
.append("id=" + id)
- .append(", srcs=" + mSrcs)
+ .append(", details=" + details)
.append(", destination=" + stack)
.append("}")
.toString();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
index 8e27d6aad6d5..f5bc85e49a39 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
@@ -17,25 +17,27 @@
package com.android.documentsui.services;
import static com.android.documentsui.Shared.DEBUG;
-import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
import android.app.Notification;
import android.app.Notification.Builder;
+import android.content.ContentResolver;
import android.content.Context;
+import android.net.Uri;
import android.util.Log;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.Metrics;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
+import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
final class DeleteJob extends Job {
private static final String TAG = "DeleteJob";
- private List<DocumentInfo> mSrcs;
- final DocumentInfo mSrcParent;
private volatile int mDocsProcessed = 0;
@@ -46,14 +48,11 @@ final class DeleteJob extends Job {
*
* @see @link {@link Job} constructor for most param descriptions.
*
- * @param srcs List of files to delete.
- * @param srcParent Parent of all source files.
+ * @param details details that contains files to be deleted and their parent
*/
DeleteJob(Context service, Context appContext, Listener listener,
- String id, DocumentStack stack, List<DocumentInfo> srcs, DocumentInfo srcParent) {
- super(service, appContext, listener, OPERATION_DELETE, id, stack);
- this.mSrcs = srcs;
- this.mSrcParent = srcParent;
+ String id, DocumentStack stack, ClipDetails details) {
+ super(service, appContext, listener, id, stack, details);
}
@Override
@@ -72,9 +71,9 @@ final class DeleteJob extends Job {
@Override
public Notification getProgressNotification() {
- mProgressBuilder.setProgress(mSrcs.size(), mDocsProcessed, false);
+ mProgressBuilder.setProgress(details.getItemCount(), mDocsProcessed, false);
String format = service.getString(R.string.delete_progress);
- mProgressBuilder.setSubText(String.format(format, mDocsProcessed, mSrcs.size()));
+ mProgressBuilder.setSubText(String.format(format, mDocsProcessed, details.getItemCount()));
mProgressBuilder.setContentText(null);
@@ -94,23 +93,37 @@ final class DeleteJob extends Job {
@Override
void start() {
- for (DocumentInfo doc : mSrcs) {
- if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
- try {
- deleteDocument(doc, mSrcParent);
-
- if (isCanceled()) {
- // Canceled, dump the rest of the work. Deleted docs are not recoverable.
- return;
+ try {
+ final List<DocumentInfo> srcs = new ArrayList<>(details.getItemCount());
+
+ final Iterable<Uri> uris = details.getDocs(appContext);
+
+ final ContentResolver resolver = appContext.getContentResolver();
+ final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
+ for (Uri uri : uris) {
+ DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
+ srcs.add(doc);
+
+ if (DEBUG) Log.d(TAG, "Deleting document @ " + doc.derivedUri);
+ try {
+ deleteDocument(doc, srcParent);
+
+ if (isCanceled()) {
+ // Canceled, dump the rest of the work. Deleted docs are not recoverable.
+ return;
+ }
+ } catch (ResourceException e) {
+ Log.e(TAG, "Failed to delete document @ " + doc.derivedUri, e);
+ onFileFailed(doc);
}
- } catch (ResourceException e) {
- Log.e(TAG, "Failed to delete document @ " + doc.derivedUri, e);
- onFileFailed(doc);
- }
- ++mDocsProcessed;
+ ++mDocsProcessed;
+ }
+ Metrics.logFileOperation(service, operationType, srcs, null);
+ } catch(IOException e) {
+ Log.e(TAG, "Failed to get list of docs or parent source.", e);
+ failedFileCount += details.getItemCount();
}
- Metrics.logFileOperation(service, operationType, mSrcs, null);
}
@Override
@@ -119,8 +132,7 @@ final class DeleteJob extends Job {
.append("DeleteJob")
.append("{")
.append("id=" + id)
- .append(", srcs=" + mSrcs)
- .append(", srcParent=" + mSrcParent)
+ .append(", details=" + details)
.append(", location=" + stack)
.append("}")
.toString();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
index a3bff90d7b7d..fec005094371 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
@@ -29,8 +29,8 @@ import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.Shared;
-import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.Job.Factory;
@@ -58,13 +58,10 @@ public class FileOperationService extends Service implements Job.Listener {
public static final String EXTRA_JOB_ID = "com.android.documentsui.JOB_ID";
public static final String EXTRA_OPERATION = "com.android.documentsui.OPERATION";
public static final String EXTRA_CANCEL = "com.android.documentsui.CANCEL";
- public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
+ public static final String EXTRA_CLIP_DETAILS = "com.android.documentsui.SRC_CLIP_DETAIL";
public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
- // This extra is used only for moving and deleting. Currently it's not the case,
- // but in the future those files may be from multiple different parents. In
- // such case, this needs to be replaced with pairs of parent and child.
- public static final String EXTRA_SRC_PARENT = "com.android.documentsui.SRC_PARENT";
+ public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
@IntDef(flag = true, value = {
OPERATION_UNKNOWN,
@@ -145,6 +142,7 @@ public class FileOperationService extends Service implements Job.Listener {
executor = null;
deletionExecutor = null;
handler = null;
+
if (DEBUG) Log.d(TAG, "Destroyed.");
}
@@ -154,35 +152,33 @@ public class FileOperationService extends Service implements Job.Listener {
// checkArgument(flags == 0); // retry and redeliver are not supported.
String jobId = intent.getStringExtra(EXTRA_JOB_ID);
- @OpType int operationType = intent.getIntExtra(EXTRA_OPERATION, OPERATION_UNKNOWN);
assert(jobId != null);
+ if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
+
if (intent.hasExtra(EXTRA_CANCEL)) {
handleCancel(intent);
} else {
- assert(operationType != OPERATION_UNKNOWN);
- handleOperation(intent, serviceId, jobId, operationType);
+ ClipDetails details = intent.getParcelableExtra(EXTRA_CLIP_DETAILS);
+ assert(details.getOpType() != OPERATION_UNKNOWN);
+ handleOperation(intent, jobId, details);
}
- return START_NOT_STICKY;
- }
-
- private void handleOperation(Intent intent, int serviceId, String jobId, int operationType) {
- if (DEBUG) Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
-
// Track the service supplied id so we can stop the service once we're out of work to do.
mLastServiceId = serviceId;
+ return START_NOT_STICKY;
+ }
+
+ private void handleOperation(Intent intent, String jobId, ClipDetails details) {
synchronized (mRunning) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
- List<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
- DocumentInfo srcParent = intent.getParcelableExtra(EXTRA_SRC_PARENT);
DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
- Job job = createJob(operationType, jobId, srcs, srcParent, stack);
+ Job job = createJob(jobId, details, stack);
if (job == null) {
return;
@@ -192,7 +188,7 @@ public class FileOperationService extends Service implements Job.Listener {
assert (job != null);
if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
- Future<?> future = getExecutorService(operationType).submit(job);
+ Future<?> future = getExecutorService(details.getOpType()).submit(job);
mRunning.put(jobId, new JobRecord(job, future));
}
}
@@ -236,32 +232,26 @@ public class FileOperationService extends Service implements Job.Listener {
*/
@GuardedBy("mRunning")
private @Nullable Job createJob(
- @OpType int operationType, String id, List<DocumentInfo> srcs, DocumentInfo srcParent,
- DocumentStack stack) {
+ String id, ClipDetails details, DocumentStack stack) {
- if (srcs.isEmpty()) {
- Log.w(TAG, "Ignoring job request with empty srcs list. Id: " + id);
- return null;
- }
+ assert(details.getItemCount() > 0);
if (mRunning.containsKey(id)) {
Log.w(TAG, "Duplicate job id: " + id
- + ". Ignoring job request for srcs: " + srcs + ", stack: " + stack + ".");
+ + ". Ignoring job request for details: " + details + ", stack: " + stack + ".");
return null;
}
- switch (operationType) {
+ switch (details.getOpType()) {
case OPERATION_COPY:
return jobFactory.createCopy(
- this, getApplicationContext(), this, id, stack, srcs);
+ this, getApplicationContext(), this, id, stack, details);
case OPERATION_MOVE:
return jobFactory.createMove(
- this, getApplicationContext(), this, id, stack, srcs,
- srcParent);
+ this, getApplicationContext(), this, id, stack, details);
case OPERATION_DELETE:
return jobFactory.createDelete(
- this, getApplicationContext(), this, id, stack, srcs,
- srcParent);
+ this, getApplicationContext(), this, id, stack, details);
default:
throw new UnsupportedOperationException();
}
@@ -341,7 +331,7 @@ public class FileOperationService extends Service implements Job.Listener {
mNotificationManager.cancel(job.id, NOTIFICATION_ID_PROGRESS);
if (job.hasFailures()) {
- Log.e(TAG, "Job failed on files: " + job.failedFiles.size() + ".");
+ Log.e(TAG, "Job failed on files: " + job.failedFileCount + ".");
mNotificationManager.notify(
job.id, NOTIFICATION_ID_FAILURE, job.getFailureNotification());
}
@@ -376,7 +366,6 @@ public class FileOperationService extends Service implements Job.Listener {
* we poll states of jobs.
*/
private static final class JobMonitor implements Runnable {
- private static final long INITIAL_PROGRESS_DELAY_MILLIS = 10L;
private static final long PROGRESS_INTERVAL_MILLIS = 500L;
private final Job mJob;
@@ -390,8 +379,7 @@ public class FileOperationService extends Service implements Job.Listener {
}
private void start() {
- // Delay the first update to avoid dividing by 0 when calculate speed
- mHandler.postDelayed(this, INITIAL_PROGRESS_DELAY_MILLIS);
+ mHandler.post(this);
}
@Override
@@ -402,8 +390,11 @@ public class FileOperationService extends Service implements Job.Listener {
return;
}
- mNotificationManager.notify(
- mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
+ // Only job in set up state has progress bar
+ if (mJob.getState() == Job.STATE_SET_UP) {
+ mNotificationManager.notify(
+ mJob.id, NOTIFICATION_ID_PROGRESS, mJob.getProgressNotification());
+ }
mHandler.postDelayed(this, PROGRESS_INTERVAL_MILLIS);
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
index 3de814526822..034c0d7c30f1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
@@ -17,17 +17,12 @@
package com.android.documentsui.services;
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.services.FileOperationService.EXTRA_CANCEL;
import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
-import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
-import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
-import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_PARENT;
-import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
-import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
-import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
+import static com.android.documentsui.services.FileOperationService.EXTRA_CLIP_DETAILS;
import android.annotation.IntDef;
import android.app.Activity;
@@ -37,13 +32,12 @@ import android.os.Parcelable;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
-import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.ClipDetails;
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;
/**
* Helper functions for starting various file operations.
@@ -63,41 +57,20 @@ public final class FileOperations {
/**
* Tries to start the activity. Returns the job id.
*/
- public static String start(Context context, List<DocumentInfo> srcDocs, DocumentStack stack,
- @OpType int operationType, Callback callback) {
+ public static String start(Context context, ClipDetails details,
+ DocumentStack stack, Callback callback) {
if (DEBUG) Log.d(TAG, "Handling generic 'start' call.");
- switch (operationType) {
- case OPERATION_COPY:
- return FileOperations.copy(context, srcDocs, stack, callback);
- case OPERATION_MOVE:
- throw new IllegalArgumentException("Moving requires providing the source parent.");
- case OPERATION_DELETE:
- throw new UnsupportedOperationException("Delete isn't currently supported.");
- default:
- throw new UnsupportedOperationException("Unknown operation: " + operationType);
- }
- }
+ String jobId = createJobId();
+ Intent intent = createBaseIntent(context, jobId, details, stack);
- /**
- * Tries to start the activity. Returns the job id.
- */
- public static String start(Context context, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
- DocumentStack stack, @OpType int operationType, Callback callback) {
+ callback.onOperationResult(
+ Callback.STATUS_ACCEPTED, details.getOpType(), details.getItemCount());
- if (DEBUG) Log.d(TAG, "Handling generic 'start' call.");
+ context.startService(intent);
- switch (operationType) {
- case OPERATION_COPY:
- return FileOperations.copy(context, srcDocs, stack, callback);
- case OPERATION_MOVE:
- return FileOperations.move(context, srcDocs, srcParent, stack, callback);
- case OPERATION_DELETE:
- throw new UnsupportedOperationException("Delete isn't currently supported.");
- default:
- throw new UnsupportedOperationException("Unknown operation: " + operationType);
- }
+ return jobId;
}
@VisibleForTesting
@@ -111,107 +84,22 @@ public final class FileOperations {
activity.startService(intent);
}
- @VisibleForTesting
- 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, context, jobId, srcDocs, destination);
-
- callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_COPY, srcDocs.size());
-
- context.startService(intent);
-
- return jobId;
- }
-
- /**
- * Starts the service for a move operation.
- *
- * @param jobId A unique jobid for this job.
- * Use {@link #createJobId} if you don't have one handy.
- * @param srcDocs A list of src files to copy.
- * @param srcParent Parent of all the source documents.
- * @param destination The move destination stack.
- */
- 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, context, jobId, srcDocs, srcParent,
- destination);
-
- callback.onOperationResult(Callback.STATUS_ACCEPTED, OPERATION_MOVE, srcDocs.size());
-
- context.startService(intent);
-
- return jobId;
- }
-
- /**
- * Starts the service for a delete operation.
- *
- * @param jobId A unique jobid for this job.
- * Use {@link #createJobId} if you don't have one handy.
- * @param srcDocs A list of src files to delete.
- * @param srcParent Parent of all the source documents.
- * @return Id of the job.
- */
- public static String delete(
- Activity activity, List<DocumentInfo> srcDocs, DocumentInfo srcParent,
- DocumentStack location) {
- String jobId = createJobId();
- if (DEBUG) Log.d(TAG, "Initiating 'delete' operation id " + jobId + ".");
-
- Intent intent = createBaseIntent(OPERATION_DELETE, activity, jobId, srcDocs, srcParent,
- location);
- activity.startService(intent);
-
- return jobId;
- }
-
/**
* Starts the service for an operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
- * @param srcDocs A list of src files for an operation.
+ * @param details the clip details that contains source files and their parent
* @return Id of the job.
*/
public static Intent createBaseIntent(
- @OpType int operationType, Context context, String jobId, List<DocumentInfo> srcDocs,
+ Context context, String jobId, ClipDetails details,
DocumentStack localeStack) {
Intent intent = new Intent(context, FileOperationService.class);
intent.putExtra(EXTRA_JOB_ID, jobId);
- intent.putParcelableArrayListExtra(EXTRA_SRC_LIST, asArrayList(srcDocs));
- intent.putExtra(EXTRA_STACK, (Parcelable) localeStack);
- intent.putExtra(EXTRA_OPERATION, operationType);
-
- return intent;
- }
-
- /**
- * Starts the service for an operation.
- *
- * @param jobId A unique jobid for this job.
- * Use {@link #createJobId} if you don't have one handy.
- * @param srcDocs A list of src files to copy.
- * @param srcParent Parent of all the source documents.
- * @return Id of the job.
- */
- public static Intent createBaseIntent(
- @OpType int operationType, Context context, String jobId,
- List<DocumentInfo> srcDocs, DocumentInfo srcParent, DocumentStack localeStack) {
-
- Intent intent = new Intent(context, FileOperationService.class);
- intent.putExtra(EXTRA_JOB_ID, jobId);
- intent.putParcelableArrayListExtra(EXTRA_SRC_LIST, asArrayList(srcDocs));
- intent.putExtra(EXTRA_SRC_PARENT, srcParent);
+ intent.putExtra(EXTRA_CLIP_DETAILS, details);
intent.putExtra(EXTRA_STACK, (Parcelable) localeStack);
- intent.putExtra(EXTRA_OPERATION, operationType);
return intent;
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
index fc3a73171add..0b4735f3a0bb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
@@ -22,6 +22,9 @@ import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG
import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
import static com.android.documentsui.services.FileOperationService.EXTRA_SRC_LIST;
+import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
+import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
+import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import static com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN;
import android.annotation.DrawableRes;
@@ -40,6 +43,7 @@ import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.util.Log;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.FilesActivity;
import com.android.documentsui.Metrics;
import com.android.documentsui.OperationDialogFragment;
@@ -53,7 +57,6 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
/**
@@ -64,16 +67,17 @@ abstract public class Job implements Runnable {
private static final String TAG = "Job";
@Retention(RetentionPolicy.SOURCE)
- @IntDef({STATE_CREATED, STATE_STARTED, STATE_COMPLETED, STATE_CANCELED})
+ @IntDef({STATE_CREATED, STATE_STARTED, STATE_SET_UP, STATE_COMPLETED, STATE_CANCELED})
@interface State {}
static final int STATE_CREATED = 0;
static final int STATE_STARTED = 1;
- static final int STATE_COMPLETED = 2;
+ static final int STATE_SET_UP = 2;
+ static final int STATE_COMPLETED = 3;
/**
* A job is in canceled state as long as {@link #cancel()} is called on it, even after it is
* completed.
*/
- static final int STATE_CANCELED = 3;
+ static final int STATE_CANCELED = 4;
static final String INTENT_TAG_WARNING = "warning";
static final String INTENT_TAG_FAILURE = "failure";
@@ -87,7 +91,9 @@ abstract public class Job implements Runnable {
final @OpType int operationType;
final String id;
final DocumentStack stack;
+ final ClipDetails details;
+ int failedFileCount = 0;
final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
final Notification.Builder mProgressBuilder;
@@ -97,8 +103,6 @@ abstract public class Job implements Runnable {
/**
* A simple progressable job, much like an AsyncTask, but with support
* for providing various related notification, progress and navigation information.
- * @param operationType
- *
* @param service The service context in which this job is running.
* @param appContext The context of the invoking application. This is usually
* just {@code getApplicationContext()}.
@@ -107,19 +111,21 @@ abstract public class Job implements Runnable {
* @param stack The documents stack context relating to this request. This is the
* destination in the Files app where the user will be take when the
* navigation intent is invoked (presumably from notification).
+ * @param details details that contains {@link FileOperationService.OpType}
*/
Job(Context service, Context appContext, Listener listener,
- @OpType int operationType, String id, DocumentStack stack) {
+ String id, DocumentStack stack, ClipDetails details) {
- assert(operationType != OPERATION_UNKNOWN);
+ assert(details.getOpType() != OPERATION_UNKNOWN);
this.service = service;
this.appContext = appContext;
this.listener = listener;
- this.operationType = operationType;
+ this.operationType = details.getOpType();
this.id = id;
this.stack = stack;
+ this.details = details;
mProgressBuilder = createProgressBuilder();
}
@@ -134,18 +140,29 @@ abstract public class Job implements Runnable {
mState = STATE_STARTED;
listener.onStart(this);
try {
- start();
+ boolean result = setUp();
+ if (result && !isCanceled()) {
+ mState = STATE_SET_UP;
+ start();
+ }
} catch (RuntimeException e) {
// No exceptions should be thrown here, as all calls to the provider must be
// handled within Job implementations. However, just in case catch them here.
Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Metrics.logFileOperationErrors(service, operationType, failedFiles);
} finally {
- mState = (mState == STATE_STARTED) ? STATE_COMPLETED : mState;
+ mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
listener.onFinished(this);
+
+ // NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
+ // at this point, user won't be able to paste it to anywhere else because the underlying
+ details.dispose(appContext);
}
}
+ boolean setUp() {
+ return true;
+ }
abstract void start();
abstract Notification getSetupNotification();
@@ -201,11 +218,12 @@ abstract public class Job implements Runnable {
}
void onFileFailed(DocumentInfo file) {
+ ++failedFileCount;
failedFiles.add(file);
}
final boolean hasFailures() {
- return !failedFiles.isEmpty();
+ return failedFileCount > 0;
}
boolean hasWarnings() {
@@ -242,7 +260,7 @@ abstract public class Job implements Runnable {
final Notification.Builder errorBuilder = new Notification.Builder(service)
.setContentTitle(service.getResources().getQuantityString(titleId,
- failedFiles.size(), failedFiles.size()))
+ failedFileCount, failedFileCount))
.setContentText(service.getString(R.string.notification_touch_for_details))
.setContentIntent(PendingIntent.getActivity(appContext, 0, navigateIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT))
@@ -319,28 +337,29 @@ abstract public class Job implements Runnable {
static final Factory instance = new Factory();
Job createCopy(Context service, Context appContext, Listener listener,
- String id, DocumentStack stack, List<DocumentInfo> srcs) {
- assert(!srcs.isEmpty());
+ String id, DocumentStack stack, ClipDetails details) {
+ assert(details.getOpType() == OPERATION_COPY);
+ assert(details.getItemCount() > 0);
assert(stack.peek().isCreateSupported());
- return new CopyJob(service, appContext, listener, id, stack, srcs);
+ return new CopyJob(service, appContext, listener, id, stack, details);
}
Job createMove(Context service, Context appContext, Listener listener,
- String id, DocumentStack stack, List<DocumentInfo> srcs,
- DocumentInfo srcParent) {
- assert(!srcs.isEmpty());
+ String id, DocumentStack stack, ClipDetails details) {
+ assert(details.getOpType() == OPERATION_MOVE);
+ assert(details.getItemCount() > 0);
assert(stack.peek().isCreateSupported());
- return new MoveJob(service, appContext, listener, id, stack, srcs, srcParent);
+ return new MoveJob(service, appContext, listener, id, stack, details);
}
Job createDelete(Context service, Context appContext, Listener listener,
- String id, DocumentStack stack, List<DocumentInfo> srcs,
- DocumentInfo srcParent) {
- assert(!srcs.isEmpty());
+ String id, DocumentStack stack, ClipDetails details) {
+ assert(details.getOpType() == OPERATION_DELETE);
+ assert(details.getItemCount() > 0);
// stack is empty if we delete docs from recent.
// we can't currently delete from archives.
assert(stack.isEmpty() || stack.peek().isDirectory());
- return new DeleteJob(service, appContext, listener, id, stack, srcs, srcParent);
+ return new DeleteJob(service, appContext, listener, id, stack, details);
}
}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
index 111817132fa1..75c4dc065823 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
@@ -17,28 +17,29 @@
package com.android.documentsui.services;
import static com.android.documentsui.Shared.DEBUG;
-import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
import android.app.Notification;
import android.app.Notification.Builder;
+import android.content.ContentResolver;
import android.content.Context;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.util.Log;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
-import java.util.List;
+import java.io.FileNotFoundException;
// TODO: Stop extending CopyJob.
final class MoveJob extends CopyJob {
private static final String TAG = "MoveJob";
- final DocumentInfo mSrcParent;
+ DocumentInfo mSrcParent;
/**
* Moves files to a destination identified by {@code destination}.
@@ -47,13 +48,11 @@ final class MoveJob extends CopyJob {
*
* @see @link {@link Job} constructor for most param descriptions.
*
- * @param srcs List of files to be moved.
- * @param srcParent Parent of all source files.
+ * @param details {@link ClipDetails} that contains list of files to be moved and their parent
*/
MoveJob(Context service, Context appContext, Listener listener,
- String id, DocumentStack destination, List<DocumentInfo> srcs, DocumentInfo srcParent) {
- super(service, appContext, listener, OPERATION_MOVE, id, destination, srcs);
- this.mSrcParent = srcParent;
+ String id, DocumentStack destination, ClipDetails details) {
+ super(service, appContext, listener, id, destination, details);
}
@Override
@@ -81,6 +80,20 @@ final class MoveJob extends CopyJob {
R.plurals.move_error_notification_title, R.drawable.ic_menu_copy);
}
+ @Override
+ public void start() {
+ final ContentResolver resolver = appContext.getContentResolver();
+ try {
+ mSrcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
+ } catch(FileNotFoundException e) {
+ Log.e(TAG, "Failed to create srcParent.", e);
+ failedFileCount += details.getItemCount();
+ return;
+ }
+
+ super.start();
+ }
+
void processDocument(DocumentInfo src, DocumentInfo srcParent, DocumentInfo dest)
throws ResourceException {
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java
new file mode 100644
index 000000000000..b0647b89c96b
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.DocumentsContract;
+import android.support.test.filters.MediumTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.services.FileOperationService;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.testing.TestScheduledExecutorService;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class ClipDetailsTest {
+
+ private static final String AUTHORITY = "foo";
+ private static final @OpType int OP_TYPE = FileOperationService.OPERATION_COPY;
+ private static final Uri SRC_PARENT =
+ DocumentsContract.buildDocumentUri(AUTHORITY, Integer.toString(0));
+ private static final List<Uri> SHORT_URI_LIST = createList(3);
+ private static final List<Uri> LONG_URI_LIST = createList(Shared.MAX_DOCS_IN_INTENT + 5);
+
+ @Rule
+ public TemporaryFolder folder = new TemporaryFolder();
+
+ private TestScheduledExecutorService mExecutor;
+ private ClipStorage mStorage;
+
+ @Before
+ public void setUp() {
+ mExecutor = new TestScheduledExecutorService();
+ AsyncTask.setDefaultExecutor(mExecutor);
+
+ mStorage = new ClipStorage(folder.getRoot());
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+
+ @Test
+ public void testOpTypeEquals_shortList() {
+ ClipDetails details = createDetailsWithShortList();
+
+ assertEquals(OP_TYPE, details.getOpType());
+ }
+
+ @Test
+ public void testOpTypeEquals_longList() {
+ ClipDetails details = createDetailsWithLongList();
+
+ assertEquals(OP_TYPE, details.getOpType());
+ }
+
+ @Test
+ public void testItemCountEquals_shortList() {
+ ClipDetails details = createDetailsWithShortList();
+
+ assertEquals(SHORT_URI_LIST.size(), details.getItemCount());
+ }
+
+ @Test
+ public void testItemCountEquals_longList() {
+ ClipDetails details = createDetailsWithLongList();
+
+ assertEquals(LONG_URI_LIST.size(), details.getItemCount());
+ }
+
+ @Test
+ public void testGetDocsEquals_shortList() throws Exception {
+ ClipDetails details = createDetailsWithShortList();
+
+ assertIterableEquals(SHORT_URI_LIST, details.getDocs(mStorage));
+ }
+
+ @Test
+ public void testGetDocsEquals_longList() throws Exception {
+ ClipDetails details = createDetailsWithLongList();
+
+ assertIterableEquals(LONG_URI_LIST, details.getDocs(mStorage));
+ }
+
+ @Test
+ public void testDispose_shortList() throws Exception {
+ ClipDetails details = createDetailsWithShortList();
+
+ details.dispose(mStorage);
+ }
+
+ @Test
+ public void testDispose_longList() throws Exception {
+ ClipDetails details = createDetailsWithLongList();
+
+ details.dispose(mStorage);
+ }
+
+ private ClipDetails createDetailsWithShortList() {
+ return ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, SHORT_URI_LIST, mStorage);
+ }
+
+ private ClipDetails createDetailsWithLongList() {
+ ClipDetails details =
+ ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, LONG_URI_LIST, mStorage);
+
+ mExecutor.runAll();
+
+ return details;
+ }
+
+ private void assertIterableEquals(Iterable<Uri> expected, Iterable<Uri> value) {
+ Iterator<Uri> expectedIter = expected.iterator();
+ Iterator<Uri> valueIter = value.iterator();
+
+ while (expectedIter.hasNext() && valueIter.hasNext()) {
+ assertEquals(expectedIter.next(), valueIter.next());
+ }
+
+ assertFalse(expectedIter.hasNext());
+ assertFalse(expectedIter.hasNext());
+ }
+
+ private static List<Uri> createList(int count) {
+ List<Uri> uris = new ArrayList<>(count);
+
+ for (int i = 0; i < count; ++i) {
+ uris.add(DocumentsContract.buildDocumentUri(AUTHORITY, Integer.toString(i)));
+ }
+
+ return uris;
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java
index 986ec793aaad..851000b80cbe 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java
@@ -21,23 +21,26 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.net.Uri;
+import android.os.AsyncTask;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
-import com.android.documentsui.ClipStorage.Writer;
+import com.android.documentsui.ClipStorage.Reader;
import com.android.documentsui.dirlist.TestModel;
+import com.android.documentsui.testing.TestScheduledExecutorService;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
+import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ClipStorageTest {
@@ -48,38 +51,52 @@ public class ClipStorageTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
+ private TestScheduledExecutorService mExecutor;
+
private ClipStorage mStorage;
private TestModel mModel;
+ private long mTag;
+
@Before
public void setUp() {
File clipDir = ClipStorage.prepareStorage(folder.getRoot());
mStorage = new ClipStorage(clipDir);
+
+ mExecutor = new TestScheduledExecutorService();
+ AsyncTask.setDefaultExecutor(mExecutor);
+
+ mTag = mStorage.createTag();
+ }
+
+ @AfterClass
+ public static void tearDownOnce() {
+ AsyncTask.setDefaultExecutor(AsyncTask.SERIAL_EXECUTOR);
}
@Test
- public void testWritePrimary() throws Exception {
- Writer writer = mStorage.createWriter();
- writeAll(TEST_URIS, writer);
+ public void testWrite() throws Exception {
+ writeAll(mTag, TEST_URIS);
}
@Test
public void testRead() throws Exception {
- Writer writer = mStorage.createWriter();
- writeAll(TEST_URIS, writer);
- long tag = mStorage.savePrimary();
- List<Uri> uris = mStorage.read(tag);
+ writeAll(mTag, TEST_URIS);
+ List<Uri> uris = new ArrayList<>();
+ try(Reader provider = mStorage.createReader(mTag)) {
+ for (Uri uri : provider) {
+ uris.add(uri);
+ }
+ }
assertEquals(TEST_URIS, uris);
}
@Test
public void testDelete() throws Exception {
- Writer writer = mStorage.createWriter();
- writeAll(TEST_URIS, writer);
- long tag = mStorage.savePrimary();
- mStorage.delete(tag);
+ writeAll(mTag, TEST_URIS);
+ mStorage.delete(mTag);
try {
- mStorage.read(tag);
+ mStorage.createReader(mTag);
} catch (IOException expected) {}
}
@@ -91,21 +108,9 @@ public class ClipStorageTest {
assertFalse(clipDir.equals(folder.getRoot()));
}
- @Test
- public void testPrepareStorage_DeletesPreviousClipFiles() throws Exception {
- File clipDir = ClipStorage.prepareStorage(folder.getRoot());
- new File(clipDir, "somefakefile.poodles").createNewFile();
- new File(clipDir, "yodles.yam").createNewFile();
-
- assertEquals(2, clipDir.listFiles().length);
- clipDir = ClipStorage.prepareStorage(folder.getRoot());
- assertEquals(0, clipDir.listFiles().length);
- }
-
- private static void writeAll(List<Uri> uris, Writer writer) throws IOException {
- for (Uri uri : uris) {
- writer.write(uri);
- }
+ private void writeAll(long tag, List<Uri> uris) {
+ new ClipStorage.PersistTask(mStorage, uris, tag).execute();
+ mExecutor.runAll();
}
private static List<Uri> createList(String... values) {
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
index eb8a061833b2..cd0593946da7 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -16,6 +16,8 @@
package com.android.documentsui.services;
+import static com.android.documentsui.services.FileOperationService.OPERATION_COPY;
+
import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
@@ -23,7 +25,6 @@ import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
import com.android.documentsui.model.DocumentInfo;
-import com.android.documentsui.model.DocumentStack;
import java.util.List;
@@ -110,7 +111,8 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
public void runNoCopyDirToSelfTest() throws Exception {
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
- createJob(newArrayList(testDir),
+ createJob(OPERATION_COPY,
+ newArrayList(testDir),
DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
testDir).run();
@@ -125,7 +127,8 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
Uri destDir = mDocs.createFolder(testDir, "theDescendent");
- createJob(newArrayList(testDir),
+ createJob(OPERATION_COPY,
+ newArrayList(testDir),
DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
destDir).run();
@@ -160,6 +163,6 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob
final T createJob(List<Uri> srcs) throws Exception {
Uri srcParent = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
- return createJob(srcs, srcParent, destination);
+ return createJob(OPERATION_COPY, srcs, srcParent, destination);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
index e559503f8b30..c3cbe3f14458 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
@@ -24,17 +24,17 @@ import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.RemoteException;
-import android.provider.DocumentsContract;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.DocumentsProviderHelper;
import com.android.documentsui.StubProvider;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.model.RootInfo;
-
-import com.google.common.collect.Lists;
+import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.testing.ClipDetailsFactory;
import java.util.List;
@@ -85,18 +85,16 @@ public abstract class AbstractJobTest<T extends Job> extends AndroidTestCase {
mDestRoot = mDocs.getRoot(ROOT_1_ID);
}
- final T createJob(List<Uri> srcs, Uri srcParent, Uri destination) throws Exception {
+ final T createJob(@OpType int opType, List<Uri> srcs, Uri srcParent, Uri destination)
+ throws Exception {
DocumentStack stack = new DocumentStack();
stack.push(DocumentInfo.fromUri(mResolver, destination));
+ stack.root = mSrcRoot;
- List<DocumentInfo> srcDocs = Lists.newArrayList();
- for (Uri src : srcs) {
- srcDocs.add(DocumentInfo.fromUri(mResolver, src));
- }
-
- return createJob(srcDocs, DocumentInfo.fromUri(mResolver, srcParent), stack);
+ ClipDetails details = ClipDetailsFactory.createClipDetails(opType, srcParent, srcs);
+ return createJob(details, stack);
}
- abstract T createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack destination)
+ abstract T createJob(ClipDetails details, DocumentStack destination)
throws Exception;
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
index bb7c01afb65e..eac06ca98001 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
@@ -22,11 +22,9 @@ import android.net.Uri;
import android.provider.DocumentsContract.Document;
import android.test.suitebuilder.annotation.MediumTest;
-import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
-import java.util.List;
-
@MediumTest
public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
@@ -78,10 +76,9 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
}
@Override
- // TODO: Stop passing srcParent here, as it's not used for copying.
- CopyJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
+ CopyJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new CopyJob(
- mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs);
+ mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
index 722df75593bd..050c7ea5929b 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
@@ -16,13 +16,15 @@
package com.android.documentsui.services;
+import static com.android.documentsui.services.FileOperationService.OPERATION_DELETE;
+
import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
-import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
import java.util.List;
@@ -49,15 +51,14 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> {
*/
private final DeleteJob createJob(List<Uri> srcs, Uri srcParent) throws Exception {
Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
- return createJob(srcs, srcParent, stack);
+ return createJob(OPERATION_DELETE, srcs, srcParent, stack);
}
- @Override
// TODO: Remove inheritance, as stack is not used for deleting, nor srcParent.
- DeleteJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
+ @Override
+ DeleteJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new DeleteJob(
- mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs,
- srcParent);
+ mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
index 9d6e1d7e26e8..e16d5ae52909 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
@@ -29,10 +29,13 @@ import android.net.Uri;
import android.test.ServiceTestCase;
import android.test.suitebuilder.annotation.MediumTest;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
import com.android.documentsui.services.Job.Listener;
+import com.android.documentsui.testing.ClipDetailsFactory;
import com.android.documentsui.testing.TestHandler;
+import com.android.documentsui.testing.TestScheduledExecutorService;
import java.util.ArrayList;
import java.util.List;
@@ -40,6 +43,8 @@ import java.util.List;
@MediumTest
public class FileOperationServiceTest extends ServiceTestCase<FileOperationService> {
+ private static final Uri SRC_PARENT =
+ Uri.parse("content://com.android.documentsui.testing/parent");
private static final DocumentInfo ALPHA_DOC = createDoc("alpha");
private static final DocumentInfo BETA_DOC = createDoc("alpha");
private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
@@ -90,7 +95,11 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
}
public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception {
- startService(createCopyIntent(new ArrayList<DocumentInfo>(), BETA_DOC));
+ try {
+ startService(createCopyIntent(new ArrayList<>(), BETA_DOC));
+ } catch(AssertionError e) {
+ // Expected AssertionError
+ }
startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
mJobFactory.assertJobsCreated(1);
@@ -219,13 +228,29 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
DocumentStack stack = new DocumentStack();
stack.push(dest);
- return createBaseIntent(OPERATION_COPY, getContext(), createJobId(), files, stack);
+ List<Uri> uris = new ArrayList<>(files.size());
+ for (DocumentInfo file: files) {
+ uris.add(file.derivedUri);
+ }
+
+ ClipDetails details =
+ ClipDetailsFactory.createClipDetails(OPERATION_COPY, SRC_PARENT, uris);
+
+ return createBaseIntent(getContext(), createJobId(), details, stack);
}
private Intent createDeleteIntent(ArrayList<DocumentInfo> files) {
DocumentStack stack = new DocumentStack();
- return createBaseIntent(OPERATION_DELETE, getContext(), createJobId(), files, stack);
+ List<Uri> uris = new ArrayList<>(files.size());
+ for (DocumentInfo file: files) {
+ uris.add(file.derivedUri);
+ }
+
+ ClipDetails details =
+ ClipDetailsFactory.createClipDetails(OPERATION_DELETE, SRC_PARENT, uris);
+
+ return createBaseIntent(getContext(), createJobId(), details, stack);
}
private static DocumentInfo createDoc(String name) {
@@ -291,28 +316,28 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi
@Override
Job createCopy(Context service, Context appContext, Listener listener, String id,
- DocumentStack stack, List<DocumentInfo> srcs) {
+ DocumentStack stack, ClipDetails details) {
- if (srcs.isEmpty()) {
+ if (details.getItemCount() == 0) {
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(
- service, appContext, listener, OPERATION_COPY, id, stack, mJobRunnable);
+ service, appContext, listener, id, stack, details, mJobRunnable);
copyJobs.add(job);
return job;
}
@Override
Job createDelete(Context service, Context appContext, Listener listener, String id,
- DocumentStack stack, List<DocumentInfo> srcs, DocumentInfo srcParent) {
+ DocumentStack stack, ClipDetails details) {
- if (srcs.isEmpty()) {
+ if (details.getItemCount() == 0) {
throw new RuntimeException("Empty srcs not supported!");
}
TestJob job = new TestJob(
- service, appContext, listener, OPERATION_DELETE, id, stack, mJobRunnable);
+ service, appContext, listener, id, stack, details, mJobRunnable);
deleteJobs.add(job);
return job;
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
index 24181d62e208..fd5c92a0f6e8 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
@@ -20,14 +20,11 @@ import static com.google.common.collect.Lists.newArrayList;
import android.net.Uri;
import android.provider.DocumentsContract.Document;
-import android.provider.DocumentsContract;
import android.test.suitebuilder.annotation.MediumTest;
-import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.model.DocumentStack;
-import java.util.List;
-
@MediumTest
public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
@@ -110,10 +107,9 @@ public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
// TODO: Add test cases for moving when multi-parented.
@Override
- MoveJob createJob(List<DocumentInfo> srcs, DocumentInfo srcParent, DocumentStack stack)
+ MoveJob createJob(ClipDetails details, DocumentStack stack)
throws Exception {
return new MoveJob(
- mContext, mContext, mJobListener, FileOperations.createJobId(), stack, srcs,
- srcParent);
+ mContext, mContext, mJobListener, FileOperations.createJobId(), stack, details);
}
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
index 9104ff02638f..a7e1d665d008 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
@@ -22,12 +22,14 @@ import static junit.framework.Assert.assertTrue;
import android.app.Notification;
import android.app.Notification.Builder;
import android.content.Context;
-import android.icu.text.NumberFormat;
+import com.android.documentsui.ClipDetails;
import com.android.documentsui.R;
import com.android.documentsui.model.DocumentInfo;
import com.android.documentsui.model.DocumentStack;
+import java.text.NumberFormat;
+
public class TestJob extends Job {
private boolean mStarted;
@@ -37,8 +39,8 @@ public class TestJob extends Job {
TestJob(
Context service, Context appContext, Listener listener,
- int operationType, String id, DocumentStack stack, Runnable startRunnable) {
- super(service, appContext, listener, operationType, id, stack);
+ String id, DocumentStack stack, ClipDetails details, Runnable startRunnable) {
+ super(service, appContext, listener, id, stack, details);
mStartRunnable = startRunnable;
}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java
new file mode 100644
index 000000000000..d8335281dbae
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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.net.Uri;
+
+import com.android.documentsui.ClipDetails;
+import com.android.documentsui.services.FileOperationService.OpType;
+
+import java.util.List;
+
+public final class ClipDetailsFactory {
+ private ClipDetailsFactory() {}
+
+ public static ClipDetails createClipDetails(@OpType int opType, Uri srcParent, List<Uri> docs) {
+ return new ClipDetails.StandardClipDetails(opType, srcParent, docs);
+ }
+}
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestScheduledExecutorService.java
index 7bbe70acacdd..f5001ee41d64 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestScheduledExecutorService.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestScheduledExecutorService.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.documentsui.services;
+package com.android.documentsui.testing;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.fail;
@@ -47,7 +47,7 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
return new ArrayList<>();
}
- void assertShutdown() {
+ public void assertShutdown() {
if (!shutdown) {
fail("Executor wasn't shut down.");
}
@@ -109,7 +109,7 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
@Override
public void execute(Runnable command) {
- throw new UnsupportedOperationException();
+ schedule(command, 0, TimeUnit.MILLISECONDS);
}
@Override
@@ -136,13 +136,13 @@ public class TestScheduledExecutorService implements ScheduledExecutorService {
throw new UnsupportedOperationException();
}
- void runAll() {
+ public void runAll() {
for (TestFuture future : scheduled) {
future.runnable.run();
}
}
- void run(int taskIndex) {
+ public void run(int taskIndex) {
scheduled.get(taskIndex).runnable.run();
}