Merge "[multi-part] Eliminate 1k selection limit"
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java b/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java
deleted file mode 100644
index 6cd0353..0000000
--- a/packages/DocumentsUI/src/com/android/documentsui/ClipDetails.java
+++ /dev/null
@@ -1,343 +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;
-
-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/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
index 4c103c4..72413bd 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
@@ -32,6 +32,7 @@
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
@@ -349,28 +350,35 @@
             return;
         }
 
-        ClipDetails details = ClipDetails.createClipDetails(clipData);
+        PersistableBundle bundle = clipData.getDescription().getExtras();
+        @OpType int opType = getOpType(bundle);
+        UrisSupplier uris = UrisSupplier.create(clipData);
 
         if (!canCopy(destination)) {
             callback.onOperationResult(
-                    FileOperations.Callback.STATUS_REJECTED, details.getOpType(), 0);
+                    FileOperations.Callback.STATUS_REJECTED, opType, 0);
             return;
         }
 
-        if (details.getItemCount() == 0) {
+        if (uris.getItemCount() == 0) {
             callback.onOperationResult(
-                    FileOperations.Callback.STATUS_ACCEPTED, details.getOpType(), 0);
+                    FileOperations.Callback.STATUS_ACCEPTED, opType, 0);
             return;
         }
 
-        DocumentStack dstStack = new DocumentStack();
-        dstStack.push(destination);
-        dstStack.addAll(docStack);
+        DocumentStack dstStack = new DocumentStack(docStack, destination);
 
-        // Pass root here so that we can perform "download" root check when
-        dstStack.root = docStack.root;
+        String srcParentString = bundle.getString(SRC_PARENT_KEY);
+        Uri srcParent = srcParentString == null ? null : Uri.parse(srcParentString);
 
-        FileOperations.start(mContext, details, dstStack, callback);
+        FileOperation operation = new FileOperation.Builder()
+                .withOpType(opType)
+                .withSrcParent(srcParent)
+                .withDestination(dstStack)
+                .withSrcs(uris)
+                .build();
+
+        FileOperations.start(mContext, operation, callback);
     }
 
     /**
@@ -399,8 +407,24 @@
         }
 
         ClipDescription description = data.getDescription();
+        if (description == null) {
+            return ClipStorage.NO_SELECTION_TAG;
+        }
+
         BaseBundle bundle = description.getExtras();
+        if (bundle == null) {
+            return ClipStorage.NO_SELECTION_TAG;
+        }
+
         return bundle.getLong(OP_JUMBO_SELECTION_TAG, ClipStorage.NO_SELECTION_TAG);
     }
 
+    public static @OpType int getOpType(ClipData data) {
+        PersistableBundle bundle = data.getDescription().getExtras();
+        return getOpType(bundle);
+    }
+
+    private static @OpType int getOpType(PersistableBundle bundle) {
+        return bundle.getInt(OP_TYPE_KEY);
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
index b8559bc1..0a518cd 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java
@@ -41,7 +41,6 @@
 import android.support.design.widget.Snackbar;
 import android.util.Log;
 import android.view.Menu;
-import android.view.MenuItem;
 
 import com.android.documentsui.MenuManager.DirectoryDetails;
 import com.android.documentsui.RecentsProvider.RecentColumns;
@@ -156,7 +155,7 @@
             state.directoryCopy = intent.getBooleanExtra(
                     Shared.EXTRA_DIRECTORY_COPY, false);
             state.copyOperationSubType = intent.getIntExtra(
-                    FileOperationService.EXTRA_OPERATION,
+                    FileOperationService.EXTRA_OPERATION_TYPE,
                     FileOperationService.OPERATION_COPY);
         }
     }
@@ -386,7 +385,7 @@
             // Picking a copy destination is only used internally by us, so we
             // don't need to extend permissions to the caller.
             intent.putExtra(Shared.EXTRA_STACK, (Parcelable) mState.stack);
-            intent.putExtra(FileOperationService.EXTRA_OPERATION, mState.copyOperationSubType);
+            intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mState.copyOperationSubType);
         } else {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index f82bdf1..1edfffe 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -139,7 +139,7 @@
         // Only show it manually for the first time (icicle is null).
         if (icicle == null && dialogType != DIALOG_TYPE_UNKNOWN) {
             final int opType = intent.getIntExtra(
-                    FileOperationService.EXTRA_OPERATION,
+                    FileOperationService.EXTRA_OPERATION_TYPE,
                     FileOperationService.OPERATION_COPY);
             final ArrayList<DocumentInfo> srcList =
                     intent.getParcelableArrayListExtra(FileOperationService.EXTRA_SRC_LIST);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/OperationDialogFragment.java b/packages/DocumentsUI/src/com/android/documentsui/OperationDialogFragment.java
index 9a3f7a8..140baad 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/OperationDialogFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/OperationDialogFragment.java
@@ -60,7 +60,7 @@
             @OpType int operationType) {
         final Bundle args = new Bundle();
         args.putInt(FileOperationService.EXTRA_DIALOG_TYPE, dialogType);
-        args.putInt(FileOperationService.EXTRA_OPERATION, operationType);
+        args.putInt(FileOperationService.EXTRA_OPERATION_TYPE, operationType);
         args.putParcelableArrayList(FileOperationService.EXTRA_SRC_LIST, failedSrcList);
 
         final FragmentTransaction ft = fm.beginTransaction();
@@ -78,7 +78,7 @@
         final @DialogType int dialogType =
               getArguments().getInt(FileOperationService.EXTRA_DIALOG_TYPE);
         final @OpType int operationType =
-              getArguments().getInt(FileOperationService.EXTRA_OPERATION);
+              getArguments().getInt(FileOperationService.EXTRA_OPERATION_TYPE);
         final ArrayList<DocumentInfo> srcList = getArguments().getParcelableArrayList(
                 FileOperationService.EXTRA_SRC_LIST);
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/UrisSupplier.java b/packages/DocumentsUI/src/com/android/documentsui/UrisSupplier.java
new file mode 100644
index 0000000..c5d30aa
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/UrisSupplier.java
@@ -0,0 +1,275 @@
+/*
+ * 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 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.FileOperation;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+
+/**
+ * UrisSupplier provides doc uri list to {@link FileOperation}.
+ *
+ * <p>Under the hood it provides cross-process synchronization support such that its consumer doesn't
+ * need to explicitly synchronize its access.
+ */
+public abstract class UrisSupplier implements Parcelable {
+
+    public abstract int getItemCount();
+
+    /**
+     * Gets doc list. This may only be called once because it may read a file
+     * to get the list.
+     *
+     * @param context We need context to obtain {@link ClipStorage}. It can't be sent in a parcel.
+     */
+    public Iterable<Uri> getDocs(Context context) throws IOException {
+        return getDocs(DocumentsApplication.getClipStorage(context));
+    }
+
+    @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) {}
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static UrisSupplier create(ClipData clipData) {
+        UrisSupplier uris;
+        PersistableBundle bundle = clipData.getDescription().getExtras();
+        if (bundle.containsKey(OP_JUMBO_SELECTION_TAG)) {
+            uris = new JumboUrisSupplier(clipData);
+        } else {
+            uris = new StandardUrisSupplier(clipData);
+        }
+
+        return uris;
+    }
+
+    public static UrisSupplier create(
+            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 create(uris, storage);
+    }
+
+    @VisibleForTesting
+    static UrisSupplier create(List<Uri> uris, ClipStorage storage) {
+        UrisSupplier urisSupplier = (uris.size() > Shared.MAX_DOCS_IN_INTENT)
+                ? new JumboUrisSupplier(uris, storage)
+                : new StandardUrisSupplier(uris);
+
+        return urisSupplier;
+    }
+
+    private static class JumboUrisSupplier extends UrisSupplier {
+        private static final String TAG = "JumboUrisSupplier";
+
+        private final long mSelectionTag;
+        private final int mSelectionSize;
+
+        private final transient AtomicReference<ClipStorage.Reader> mReader =
+                new AtomicReference<>();
+
+        private JumboUrisSupplier(ClipData 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 JumboUrisSupplier(Collection<Uri> uris, ClipStorage storage) {
+            mSelectionTag = storage.createTag();
+            new ClipStorage.PersistTask(storage, uris, mSelectionTag).execute();
+            mSelectionSize = uris.size();
+        }
+
+        @Override
+        public int getItemCount() {
+            return mSelectionSize;
+        }
+
+        @Override
+        Iterable<Uri> getDocs(ClipStorage storage) throws IOException {
+            ClipStorage.Reader reader = mReader.getAndSet(storage.createReader(mSelectionTag));
+            if (reader != null) {
+                reader.close();
+                mReader.get().close();
+                throw new IllegalStateException("This method can only be called once.");
+            }
+
+            return mReader.get();
+        }
+
+        @Override
+        void dispose(ClipStorage storage) {
+            try {
+                ClipStorage.Reader reader = mReader.get();
+                if (reader != null) {
+                    reader.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("JumboUrisSupplier{");
+            builder.append("selectionTag=").append(mSelectionTag);
+            builder.append(", selectionSize=").append(mSelectionSize);
+            builder.append("}");
+            return builder.toString();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeLong(mSelectionTag);
+            dest.writeInt(mSelectionSize);
+        }
+
+        private JumboUrisSupplier(Parcel in) {
+            mSelectionTag = in.readLong();
+            mSelectionSize = in.readInt();
+        }
+
+        public static final Parcelable.Creator<JumboUrisSupplier> CREATOR =
+                new Parcelable.Creator<JumboUrisSupplier>() {
+
+                    @Override
+                    public JumboUrisSupplier createFromParcel(Parcel source) {
+                        return new JumboUrisSupplier(source);
+                    }
+
+                    @Override
+                    public JumboUrisSupplier[] newArray(int size) {
+                        return new JumboUrisSupplier[size];
+                    }
+                };
+    }
+
+    /**
+     * This class and its constructor is visible for testing to create test doubles of
+     * {@link UrisSupplier}.
+     */
+    @VisibleForTesting
+    public static class StandardUrisSupplier extends UrisSupplier {
+        private final List<Uri> mDocs;
+
+        private StandardUrisSupplier(ClipData clipData) {
+            mDocs = listDocs(clipData);
+        }
+
+        @VisibleForTesting
+        public StandardUrisSupplier(List<Uri> docs) {
+            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
+        Iterable<Uri> getDocs(ClipStorage storage) {
+            return mDocs;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("StandardUrisSupplier{");
+            builder.append("docs=").append(mDocs.toString());
+            builder.append("}");
+            return builder.toString();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeTypedList(mDocs);
+        }
+
+        private StandardUrisSupplier(Parcel in) {
+            mDocs = in.createTypedArrayList(Uri.CREATOR);
+        }
+
+        public static final Parcelable.Creator<StandardUrisSupplier> CREATOR =
+                new Parcelable.Creator<StandardUrisSupplier>() {
+
+                    @Override
+                    public StandardUrisSupplier createFromParcel(Parcel source) {
+                        return new StandardUrisSupplier(source);
+                    }
+
+                    @Override
+                    public StandardUrisSupplier[] newArray(int size) {
+                        return new StandardUrisSupplier[size];
+                    }
+                };
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index f96341a..86c6c99 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -72,9 +72,9 @@
 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.UrisSupplier;
 import com.android.documentsui.DocumentClipper;
 import com.android.documentsui.DocumentsActivity;
 import com.android.documentsui.DocumentsApplication;
@@ -97,6 +97,7 @@
 import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.RootInfo;
+import com.android.documentsui.services.FileOperation;
 import com.android.documentsui.services.FileOperationService;
 import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
@@ -172,7 +173,7 @@
     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 @Nullable FileOperation mPendingOperation;
     private boolean mSearchMode = false;
 
     private @Nullable BandController mBandController;
@@ -269,7 +270,7 @@
         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);
+        mPendingOperation = args.getParcelable(FileOperationService.EXTRA_OPERATION);
 
         // Restore any selection we may have squirreled away in retained state.
         @Nullable RetainedState retained = getBaseActivity().getRetainedState();
@@ -359,7 +360,7 @@
         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);
+        outState.putParcelable(FileOperationService.EXTRA_OPERATION, mPendingOperation);
     }
 
     @Override
@@ -400,21 +401,19 @@
 
     private void handleCopyResult(int resultCode, Intent data) {
 
-        ClipDetails details = mDetailsForCopy;
-        mDetailsForCopy = null;
+        FileOperation operation = mPendingOperation;
+        mPendingOperation = 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());
+            operation.dispose(getContext());
             return;
         }
 
-        FileOperations.start(
-                getContext(),
-                details,
-                data.getParcelableExtra(Shared.EXTRA_STACK),
-                mFileOpCallback);
+        operation.setDestination(data.getParcelableExtra(Shared.EXTRA_STACK));
+
+        FileOperations.start(getContext(), operation, mFileOpCallback);
     }
 
     protected boolean onDoubleTap(MotionInputEvent event) {
@@ -1010,14 +1009,19 @@
                                     Log.w(TAG, "Action mode is null before deleting documents.");
                                 }
 
-                                ClipDetails details = ClipDetails.createClipDetails(
-                                        FileOperationService.OPERATION_DELETE,
-                                        srcParent.derivedUri,
+                                UrisSupplier srcs = UrisSupplier.create(
                                         selected,
                                         mModel::getItemUri,
                                         getContext());
-                                FileOperations.start(getActivity(), details,
-                                        getDisplayState().stack, mFileOpCallback);
+
+                                FileOperation operation = new FileOperation.Builder()
+                                        .withOpType(FileOperationService.OPERATION_DELETE)
+                                        .withDestination(getDisplayState().stack)
+                                        .withSrcs(srcs)
+                                        .withSrcParent(srcParent.derivedUri)
+                                        .build();
+
+                                FileOperations.start(getActivity(), operation, mFileOpCallback);
                             }
                         })
                     .setNegativeButton(android.R.string.cancel, null)
@@ -1041,9 +1045,15 @@
                 getActivity(),
                 DocumentsActivity.class);
 
+        UrisSupplier srcs =
+                UrisSupplier.create(selected, mModel::getItemUri, getContext());
+
         Uri srcParent = getDisplayState().stack.peek().derivedUri;
-        mDetailsForCopy = ClipDetails.createClipDetails(
-                mode, srcParent, selected, mModel::getItemUri, getContext());
+        mPendingOperation = new FileOperation.Builder()
+                .withOpType(mode)
+                .withSrcParent(srcParent)
+                .withSrcs(srcs)
+                .build();
 
         // Relay any config overrides bits present in the original intent.
         Intent original = getActivity().getIntent();
@@ -1068,6 +1078,7 @@
                 // (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_TYPE, mode);
 
                 // This just identifies the type of request...we'll check it
                 // when we reveive a response.
@@ -1304,8 +1315,7 @@
         ClipData clipData = event.getClipData();
         assert (clipData != null);
 
-        assert(ClipDetails.createClipDetails(clipData).getOpType()
-                == FileOperationService.OPERATION_COPY);
+        assert(DocumentClipper.getOpType(clipData) == 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
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
index 34bd696..c4f4dc1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentStack.java
@@ -39,6 +39,24 @@
 
     public RootInfo root;
 
+    public DocumentStack() {};
+
+    /**
+     * Makes a new copy, and pushes all docs to the new copy in the same order as they're passed
+     * as parameters, i.e. the last document will be at the top of the stack.
+     *
+     * @param src
+     * @param docs
+     */
+    public DocumentStack(DocumentStack src, DocumentInfo... docs) {
+        super(src);
+        for (DocumentInfo doc : docs) {
+            push(doc);
+        }
+
+        root = src.root;
+    }
+
     public String getTitle() {
         if (size() == 1 && root != null) {
             return root.title;
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
index fac8667..390656c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/CopyJob.java
@@ -27,8 +27,9 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 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_OPERATION_TYPE;
 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;
@@ -50,12 +51,13 @@
 import android.util.Log;
 import android.webkit.MimeTypeMap;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 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.model.RootInfo;
+import com.android.documentsui.services.FileOperationService.OpType;
 
 import libcore.io.IoUtils;
 
@@ -85,17 +87,20 @@
 
     /**
      * @see @link {@link Job} constructor for most param descriptions.
-     *
-     * @param details clip details containing source file list
      */
-    CopyJob(Context service, Context appContext, Listener listener,
-            String id, DocumentStack destination, ClipDetails details) {
-        super(service, appContext, listener, id, destination, details);
+    CopyJob(Context service, Listener listener, String id, DocumentStack destination,
+            UrisSupplier srcs) {
+        this(service, listener, id, OPERATION_COPY, destination, srcs);
+    }
 
-        assert(details.getItemCount() > 0);
+    CopyJob(Context service, Listener listener, String id, @OpType int opType,
+            DocumentStack destination, UrisSupplier srcs) {
+        super(service, listener, id, opType, destination, srcs);
+
+        assert(srcs.getItemCount() > 0);
 
         // delay the initialization of it to setUp() because it may be IO extensive.
-        mSrcs = new ArrayList<>(details.getItemCount());
+        mSrcs = new ArrayList<>(srcs.getItemCount());
     }
 
     @Override
@@ -184,7 +189,7 @@
     Notification getWarningNotification() {
         final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_WARNING);
         navigateIntent.putExtra(EXTRA_DIALOG_TYPE, DIALOG_TYPE_CONVERTED);
-        navigateIntent.putExtra(EXTRA_OPERATION, operationType);
+        navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
 
         navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, convertedFiles);
 
@@ -257,7 +262,7 @@
     private void buildDocumentList() throws ResourceException {
         try {
             final ContentResolver resolver = appContext.getContentResolver();
-            final Iterable<Uri> uris = details.getDocs(appContext);
+            final Iterable<Uri> uris = srcs.getDocs(appContext);
             for (Uri uri : uris) {
                 DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
                 if (canCopy(doc, stack.root)) {
@@ -271,7 +276,7 @@
                 }
             }
         } catch(IOException e) {
-            failedFileCount += details.getItemCount();
+            failedFileCount += srcs.getItemCount();
             throw new ResourceException("Failed to open the list of docs to copy.", e);
         }
     }
@@ -659,7 +664,7 @@
                 .append("CopyJob")
                 .append("{")
                 .append("id=" + id)
-                .append(", details=" + details)
+                .append(", docs=" + srcs)
                 .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 f5bc85e..f6202c5 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/DeleteJob.java
@@ -17,6 +17,7 @@
 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;
@@ -25,7 +26,7 @@
 import android.net.Uri;
 import android.util.Log;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.R;
 import com.android.documentsui.model.DocumentInfo;
@@ -41,18 +42,18 @@
 
     private volatile int mDocsProcessed = 0;
 
+    Uri mSrcParent;
     /**
      * Moves files to a destination identified by {@code destination}.
      * Performs most work by delegating to CopyJob, then deleting
      * a file after it has been copied.
      *
      * @see @link {@link Job} constructor for most param descriptions.
-     *
-     * @param details details that contains files to be deleted and their parent
      */
-    DeleteJob(Context service, Context appContext, Listener listener,
-            String id, DocumentStack stack, ClipDetails details) {
-        super(service, appContext, listener, id, stack, details);
+    DeleteJob(Context service, Listener listener, String id, Uri srcParent, DocumentStack stack,
+            UrisSupplier srcs) {
+        super(service, listener, id, OPERATION_DELETE, stack, srcs);
+        mSrcParent = srcParent;
     }
 
     @Override
@@ -71,9 +72,9 @@
 
     @Override
     public Notification getProgressNotification() {
-        mProgressBuilder.setProgress(details.getItemCount(), mDocsProcessed, false);
+        mProgressBuilder.setProgress(srcs.getItemCount(), mDocsProcessed, false);
         String format = service.getString(R.string.delete_progress);
-        mProgressBuilder.setSubText(String.format(format, mDocsProcessed, details.getItemCount()));
+        mProgressBuilder.setSubText(String.format(format, mDocsProcessed, srcs.getItemCount()));
 
         mProgressBuilder.setContentText(null);
 
@@ -94,12 +95,12 @@
     @Override
     void start() {
         try {
-            final List<DocumentInfo> srcs = new ArrayList<>(details.getItemCount());
+            final List<DocumentInfo> srcs = new ArrayList<>(this.srcs.getItemCount());
 
-            final Iterable<Uri> uris = details.getDocs(appContext);
+            final Iterable<Uri> uris = this.srcs.getDocs(appContext);
 
             final ContentResolver resolver = appContext.getContentResolver();
-            final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
+            final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, mSrcParent);
             for (Uri uri : uris) {
                 DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
                 srcs.add(doc);
@@ -122,7 +123,7 @@
             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();
+            failedFileCount += srcs.getItemCount();
         }
     }
 
@@ -132,7 +133,8 @@
                 .append("DeleteJob")
                 .append("{")
                 .append("id=" + id)
-                .append(", details=" + details)
+                .append(", docs=" + srcs)
+                .append(", srcParent=" + mSrcParent)
                 .append(", location=" + stack)
                 .append("}")
                 .toString();
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperation.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperation.java
new file mode 100644
index 0000000..ce63864
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperation.java
@@ -0,0 +1,240 @@
+/*
+ * 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.services;
+
+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.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.documentsui.UrisSupplier;
+import com.android.documentsui.model.DocumentStack;
+import com.android.documentsui.services.FileOperationService.OpType;
+
+/**
+ * FileOperation describes a file operation, such as move/copy/delete etc.
+ */
+public abstract class FileOperation implements Parcelable {
+    private final @OpType int mOpType;
+
+    private final UrisSupplier mSrcs;
+    private DocumentStack mDestination;
+
+    @VisibleForTesting
+    FileOperation(@OpType int opType, UrisSupplier srcs, DocumentStack destination) {
+        assert(opType != OPERATION_UNKNOWN);
+        assert(srcs.getItemCount() > 0);
+
+        mOpType = opType;
+        mSrcs = srcs;
+        mDestination = destination;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public @OpType int getOpType() {
+        return mOpType;
+    }
+
+    public UrisSupplier getSrc() {
+        return mSrcs;
+    }
+
+    public DocumentStack getDestination() {
+        return mDestination;
+    }
+
+    public void setDestination(DocumentStack destination) {
+        mDestination = destination;
+    }
+
+    public void dispose(Context context) {
+        mSrcs.dispose(context);
+    }
+
+    abstract Job createJob(Context service, Job.Listener listener, String id);
+
+    private void appendInfoTo(StringBuilder builder) {
+        builder.append("opType=").append(mOpType);
+        builder.append(", srcs=").append(mSrcs.toString());
+        builder.append(", destination=").append(mDestination.toString());
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flag) {
+        out.writeInt(mOpType);
+        out.writeParcelable(mSrcs, flag);
+        out.writeParcelable(mDestination, flag);
+    }
+
+    private FileOperation(Parcel in) {
+        mOpType = in.readInt();
+        mSrcs = in.readParcelable(FileOperation.class.getClassLoader());
+        mDestination = in.readParcelable(FileOperation.class.getClassLoader());
+    }
+
+    public static class CopyOperation extends FileOperation {
+        private CopyOperation(UrisSupplier srcs, DocumentStack destination) {
+            super(OPERATION_COPY, srcs, destination);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+
+            builder.append("CopyOperation{");
+            super.appendInfoTo(builder);
+            builder.append("}");
+
+            return builder.toString();
+        }
+
+        CopyJob createJob(Context service, Job.Listener listener, String id) {
+            return new CopyJob(service, listener, id, getDestination(), getSrc());
+        }
+
+        private CopyOperation(Parcel in) {
+            super(in);
+        }
+
+        public static final Parcelable.Creator<CopyOperation> CREATOR =
+                new Parcelable.Creator<CopyOperation>() {
+
+                    @Override
+                    public CopyOperation createFromParcel(Parcel source) {
+                        return new CopyOperation(source);
+                    }
+
+                    @Override
+                    public CopyOperation[] newArray(int size) {
+                        return new CopyOperation[size];
+                    }
+                };
+    }
+
+    public static class MoveDeleteOperation extends FileOperation {
+        private final Uri mSrcParent;
+
+        private MoveDeleteOperation(
+                @OpType int opType, UrisSupplier srcs, Uri srcParent, DocumentStack destination) {
+            super(opType, srcs, destination);
+
+            assert(srcParent != null);
+            mSrcParent = srcParent;
+        }
+
+        @Override
+        Job createJob(Context service, Job.Listener listener, String id) {
+            switch(getOpType()) {
+                case OPERATION_MOVE:
+                    return new MoveJob(
+                            service, listener, id, mSrcParent, getDestination(), getSrc());
+                case OPERATION_DELETE:
+                    return new DeleteJob(
+                            service, listener, id, mSrcParent, getDestination(), getSrc());
+                default:
+                    throw new UnsupportedOperationException("Unsupported op type: " + getOpType());
+            }
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+
+            builder.append("MoveDeleteOperation{");
+            super.appendInfoTo(builder);
+            builder.append(", srcParent=").append(mSrcParent.toString());
+            builder.append("}");
+
+            return builder.toString();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flag) {
+            super.writeToParcel(out, flag);
+            out.writeParcelable(mSrcParent, flag);
+        }
+
+        private MoveDeleteOperation(Parcel in) {
+            super(in);
+            mSrcParent = in.readParcelable(null);
+        }
+
+        public static final Parcelable.Creator<MoveDeleteOperation> CREATOR =
+                new Parcelable.Creator<MoveDeleteOperation>() {
+
+
+            @Override
+            public MoveDeleteOperation createFromParcel(Parcel source) {
+                return new MoveDeleteOperation(source);
+            }
+
+            @Override
+            public MoveDeleteOperation[] newArray(int size) {
+                return new MoveDeleteOperation[size];
+            }
+        };
+    }
+
+    public static class Builder {
+        private @OpType int mOpType;
+        private Uri mSrcParent;
+        private UrisSupplier mSrcs;
+        private DocumentStack mDestination;
+
+        public Builder withOpType(@OpType int opType) {
+            mOpType = opType;
+            return this;
+        }
+
+        public Builder withSrcParent(Uri srcParent) {
+            mSrcParent = srcParent;
+            return this;
+        }
+
+        public Builder withSrcs(UrisSupplier srcs) {
+            mSrcs = srcs;
+            return this;
+        }
+
+        public Builder withDestination(DocumentStack destination) {
+            mDestination = destination;
+            return this;
+        }
+
+        public FileOperation build() {
+            switch (mOpType) {
+                case OPERATION_COPY:
+                    return new CopyOperation(mSrcs, mDestination);
+                case OPERATION_MOVE:
+                case OPERATION_DELETE:
+                    return new MoveDeleteOperation(mOpType, mSrcs, mSrcParent, mDestination);
+                default:
+                    throw new UnsupportedOperationException("Unsupported op type: " + mOpType);
+            }
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
index fec0050..b61c1c9 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperationService.java
@@ -25,15 +25,9 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.PowerManager;
-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.DocumentStack;
-import com.android.documentsui.services.Job.Factory;
-
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -55,13 +49,17 @@
 
     public static final String TAG = "FileOperationService";
 
+    // Extra used for OperationDialogFragment, Notifications and picking copy destination.
+    public static final String EXTRA_OPERATION_TYPE = "com.android.documentsui.OPERATION_TYPE";
+
+    // Extras used for OperationDialogFragment...
+    public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
+    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
+
+    // Extras used to start or cancel a file operation...
     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_CLIP_DETAILS = "com.android.documentsui.SRC_CLIP_DETAIL";
-    public static final String EXTRA_DIALOG_TYPE = "com.android.documentsui.DIALOG_TYPE";
-
-    public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
 
     @IntDef(flag = true, value = {
             OPERATION_UNKNOWN,
@@ -86,7 +84,6 @@
 
     // Use a separate thread pool to prioritize deletions.
     @VisibleForTesting ExecutorService deletionExecutor;
-    @VisibleForTesting Factory jobFactory;
 
     // Use a handler to schedule monitor tasks.
     @VisibleForTesting Handler handler;
@@ -111,10 +108,6 @@
             deletionExecutor = Executors.newCachedThreadPool();
         }
 
-        if (jobFactory == null) {
-            jobFactory = Job.Factory.instance;
-        }
-
         if (handler == null) {
             // Monitor tasks are small enough to schedule them on main thread.
             handler = new Handler();
@@ -159,9 +152,8 @@
         if (intent.hasExtra(EXTRA_CANCEL)) {
             handleCancel(intent);
         } else {
-            ClipDetails details = intent.getParcelableExtra(EXTRA_CLIP_DETAILS);
-            assert(details.getOpType() != OPERATION_UNKNOWN);
-            handleOperation(intent, jobId, details);
+            FileOperation operation = intent.getParcelableExtra(EXTRA_OPERATION);
+            handleOperation(jobId, operation);
         }
 
         // Track the service supplied id so we can stop the service once we're out of work to do.
@@ -170,15 +162,19 @@
         return START_NOT_STICKY;
     }
 
-    private void handleOperation(Intent intent, String jobId, ClipDetails details) {
+    private void handleOperation(String jobId, FileOperation operation) {
         synchronized (mRunning) {
             if (mWakeLock == null) {
                 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
             }
 
-            DocumentStack stack = intent.getParcelableExtra(Shared.EXTRA_STACK);
+            if (mRunning.containsKey(jobId)) {
+                Log.w(TAG, "Duplicate job id: " + jobId
+                        + ". Ignoring job request for operation: " + operation + ".");
+                return;
+            }
 
-            Job job = createJob(jobId, details, stack);
+            Job job = operation.createJob(this, this, jobId);
 
             if (job == null) {
                 return;
@@ -188,7 +184,7 @@
 
             assert (job != null);
             if (DEBUG) Log.d(TAG, "Scheduling job " + job.id + ".");
-            Future<?> future = getExecutorService(details.getOpType()).submit(job);
+            Future<?> future = getExecutorService(operation.getOpType()).submit(job);
             mRunning.put(jobId, new JobRecord(job, future));
         }
     }
@@ -226,37 +222,6 @@
         // TODO: Guarantee the job is being finalized
     }
 
-    /**
-     * Creates a new job. Returns null if a job with {@code id} already exists.
-     * @return
-     */
-    @GuardedBy("mRunning")
-    private @Nullable Job createJob(
-            String id, ClipDetails details, DocumentStack stack) {
-
-        assert(details.getItemCount() > 0);
-
-        if (mRunning.containsKey(id)) {
-            Log.w(TAG, "Duplicate job id: " + id
-                    + ". Ignoring job request for details: " + details + ", stack: " + stack + ".");
-            return null;
-        }
-
-        switch (details.getOpType()) {
-            case OPERATION_COPY:
-                return jobFactory.createCopy(
-                        this, getApplicationContext(), this, id, stack, details);
-            case OPERATION_MOVE:
-                return jobFactory.createMove(
-                        this, getApplicationContext(), this, id, stack, details);
-            case OPERATION_DELETE:
-                return jobFactory.createDelete(
-                        this, getApplicationContext(), this, id, stack, details);
-            default:
-                throw new UnsupportedOperationException();
-        }
-    }
-
     private ExecutorService getExecutorService(@OpType int operationType) {
         switch (operationType) {
             case OPERATION_COPY:
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
index 034c0d7..01956a1 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/FileOperations.java
@@ -19,21 +19,17 @@
 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.services.FileOperationService.EXTRA_CANCEL;
 import static com.android.documentsui.services.FileOperationService.EXTRA_JOB_ID;
-import static com.android.documentsui.services.FileOperationService.EXTRA_CLIP_DETAILS;
+import static com.android.documentsui.services.FileOperationService.EXTRA_OPERATION;
 
 import android.annotation.IntDef;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
-import android.os.Parcelable;
 import android.support.annotation.VisibleForTesting;
 import android.util.Log;
 
-import com.android.documentsui.ClipDetails;
-import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.services.FileOperationService.OpType;
 
 import java.lang.annotation.Retention;
@@ -57,16 +53,15 @@
     /**
      * Tries to start the activity. Returns the job id.
      */
-    public static String start(Context context, ClipDetails details,
-            DocumentStack stack, Callback callback) {
+    public static String start(Context context, FileOperation operation, Callback callback) {
 
         if (DEBUG) Log.d(TAG, "Handling generic 'start' call.");
 
         String jobId = createJobId();
-        Intent intent = createBaseIntent(context, jobId, details, stack);
+        Intent intent = createBaseIntent(context, jobId, operation);
 
-        callback.onOperationResult(
-                Callback.STATUS_ACCEPTED, details.getOpType(), details.getItemCount());
+        callback.onOperationResult(Callback.STATUS_ACCEPTED, operation.getOpType(),
+                operation.getSrc().getItemCount());
 
         context.startService(intent);
 
@@ -89,17 +84,14 @@
      *
      * @param jobId A unique jobid for this job.
      *     Use {@link #createJobId} if you don't have one handy.
-     * @param details the clip details that contains source files and their parent
      * @return Id of the job.
      */
     public static Intent createBaseIntent(
-            Context context, String jobId, ClipDetails details,
-            DocumentStack localeStack) {
+            Context context, String jobId, FileOperation operation) {
 
         Intent intent = new Intent(context, FileOperationService.class);
         intent.putExtra(EXTRA_JOB_ID, jobId);
-        intent.putExtra(EXTRA_CLIP_DETAILS, details);
-        intent.putExtra(EXTRA_STACK, (Parcelable) localeStack);
+        intent.putExtra(EXTRA_OPERATION, operation);
 
         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 0b4735f..29e0210 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/Job.java
@@ -20,11 +20,8 @@
 import static com.android.documentsui.services.FileOperationService.EXTRA_CANCEL;
 import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
 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_OPERATION_TYPE;
 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;
@@ -43,7 +40,7 @@
 import android.provider.DocumentsContract;
 import android.util.Log;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 import com.android.documentsui.FilesActivity;
 import com.android.documentsui.Metrics;
 import com.android.documentsui.OperationDialogFragment;
@@ -91,7 +88,7 @@
     final @OpType int operationType;
     final String id;
     final DocumentStack stack;
-    final ClipDetails details;
+    final UrisSupplier srcs;
 
     int failedFileCount = 0;
     final ArrayList<DocumentInfo> failedFiles = new ArrayList<>();
@@ -104,28 +101,26 @@
      * A simple progressable job, much like an AsyncTask, but with support
      * for providing various related notification, progress and navigation information.
      * @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()}.
      * @param listener
      * @param id Arbitrary string ID
      * @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}
+     * @param srcs the list of docs to operate on
      */
-    Job(Context service, Context appContext, Listener listener,
-            String id, DocumentStack stack, ClipDetails details) {
+    Job(Context service, Listener listener, String id,
+            @OpType int opType, DocumentStack stack, UrisSupplier srcs) {
 
-        assert(details.getOpType() != OPERATION_UNKNOWN);
+        assert(opType != OPERATION_UNKNOWN);
 
         this.service = service;
-        this.appContext = appContext;
+        this.appContext = service.getApplicationContext();
         this.listener = listener;
-        this.operationType = details.getOpType();
+        this.operationType = opType;
 
         this.id = id;
         this.stack = stack;
-        this.details = details;
+        this.srcs = srcs;
 
         mProgressBuilder = createProgressBuilder();
     }
@@ -156,7 +151,7 @@
 
             // 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);
+            srcs.dispose(appContext);
         }
     }
 
@@ -255,7 +250,7 @@
     Notification getFailureNotification(@PluralsRes int titleId, @DrawableRes int icon) {
         final Intent navigateIntent = buildNavigateIntent(INTENT_TAG_FAILURE);
         navigateIntent.putExtra(EXTRA_DIALOG_TYPE, OperationDialogFragment.DIALOG_TYPE_FAILURE);
-        navigateIntent.putExtra(EXTRA_OPERATION, operationType);
+        navigateIntent.putExtra(EXTRA_OPERATION_TYPE, operationType);
         navigateIntent.putParcelableArrayListExtra(EXTRA_SRC_LIST, failedFiles);
 
         final Notification.Builder errorBuilder = new Notification.Builder(service)
@@ -330,40 +325,6 @@
     }
 
     /**
-     * Factory class that facilitates our testing FileOperationService.
-     */
-    static class Factory {
-
-        static final Factory instance = new Factory();
-
-        Job createCopy(Context service, Context appContext, Listener listener,
-                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, details);
-        }
-
-        Job createMove(Context service, Context appContext, Listener listener,
-                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, details);
-        }
-
-        Job createDelete(Context service, Context appContext, Listener listener,
-                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, details);
-        }
-    }
-
-    /**
      * Listener interface employed by the service that owns us as well as tests.
      */
     interface Listener {
diff --git a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
index 75c4dc0..5e9d5cc 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/services/MoveJob.java
@@ -17,17 +17,19 @@
 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.net.Uri;
 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.UrisSupplier;
 import com.android.documentsui.R;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
@@ -39,6 +41,7 @@
 
     private static final String TAG = "MoveJob";
 
+    Uri mSrcParentUri;
     DocumentInfo mSrcParent;
 
     /**
@@ -47,12 +50,11 @@
      * a file after it has been copied.
      *
      * @see @link {@link Job} constructor for most param descriptions.
-     *
-     * @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, ClipDetails details) {
-        super(service, appContext, listener, id, destination, details);
+    MoveJob(Context service, Listener listener,
+            String id, Uri srcParent, DocumentStack destination, UrisSupplier srcs) {
+        super(service, listener, id, OPERATION_MOVE, destination, srcs);
+        mSrcParentUri = srcParent;
     }
 
     @Override
@@ -81,16 +83,21 @@
     }
 
     @Override
-    public void start() {
+    public boolean setUp() {
         final ContentResolver resolver = appContext.getContentResolver();
         try {
-            mSrcParent = DocumentInfo.fromUri(resolver, details.getSrcParent());
+            mSrcParent = DocumentInfo.fromUri(resolver, mSrcParentUri);
         } catch(FileNotFoundException e) {
             Log.e(TAG, "Failed to create srcParent.", e);
-            failedFileCount += details.getItemCount();
-            return;
+            failedFileCount += srcs.getItemCount();
+            return false;
         }
 
+        return super.setUp();
+    }
+
+    @Override
+    public void start() {
         super.start();
     }
 
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/UrisSupplierTest.java
similarity index 64%
rename from packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java
rename to packages/DocumentsUI/tests/src/com/android/documentsui/UrisSupplierTest.java
index b0647b8..719f0e2 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipDetailsTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/UrisSupplierTest.java
@@ -25,8 +25,6 @@
 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;
@@ -42,12 +40,9 @@
 
 @RunWith(AndroidJUnit4.class)
 @MediumTest
-public class ClipDetailsTest {
+public class UrisSupplierTest {
 
     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);
 
@@ -71,72 +66,58 @@
     }
 
     @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();
+        UrisSupplier uris = createWithShortList();
 
-        assertEquals(SHORT_URI_LIST.size(), details.getItemCount());
+        assertEquals(SHORT_URI_LIST.size(), uris.getItemCount());
     }
 
     @Test
     public void testItemCountEquals_longList() {
-        ClipDetails details = createDetailsWithLongList();
+        UrisSupplier uris = createWithLongList();
 
-        assertEquals(LONG_URI_LIST.size(), details.getItemCount());
+        assertEquals(LONG_URI_LIST.size(), uris.getItemCount());
     }
 
     @Test
     public void testGetDocsEquals_shortList() throws Exception {
-        ClipDetails details = createDetailsWithShortList();
+        UrisSupplier uris = createWithShortList();
 
-        assertIterableEquals(SHORT_URI_LIST, details.getDocs(mStorage));
+        assertIterableEquals(SHORT_URI_LIST, uris.getDocs(mStorage));
     }
 
     @Test
     public void testGetDocsEquals_longList() throws Exception {
-        ClipDetails details = createDetailsWithLongList();
+        UrisSupplier uris = createWithLongList();
 
-        assertIterableEquals(LONG_URI_LIST, details.getDocs(mStorage));
+        assertIterableEquals(LONG_URI_LIST, uris.getDocs(mStorage));
     }
 
     @Test
     public void testDispose_shortList() throws Exception {
-        ClipDetails details = createDetailsWithShortList();
+        UrisSupplier uris = createWithShortList();
 
-        details.dispose(mStorage);
+        uris.dispose(mStorage);
     }
 
     @Test
     public void testDispose_longList() throws Exception {
-        ClipDetails details = createDetailsWithLongList();
+        UrisSupplier uris = createWithLongList();
 
-        details.dispose(mStorage);
+        uris.dispose(mStorage);
     }
 
-    private ClipDetails createDetailsWithShortList() {
-        return ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, SHORT_URI_LIST, mStorage);
+    private UrisSupplier createWithShortList() {
+        return UrisSupplier.create(SHORT_URI_LIST, mStorage);
     }
 
-    private ClipDetails createDetailsWithLongList() {
-        ClipDetails details =
-                ClipDetails.createClipDetails(OP_TYPE, SRC_PARENT, LONG_URI_LIST, mStorage);
+    private UrisSupplier createWithLongList() {
+        UrisSupplier uris =
+                UrisSupplier.create(LONG_URI_LIST, mStorage);
 
         mExecutor.runAll();
 
-        return details;
+        return uris;
     }
 
     private void assertIterableEquals(Iterable<Uri> expected, Iterable<Uri> value) {
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 cd05939..2560f2c 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -16,8 +16,6 @@
 
 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;
@@ -25,12 +23,19 @@
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.documentsui.model.DocumentInfo;
+import com.android.documentsui.services.FileOperationService.OpType;
 
 import java.util.List;
 
 @MediumTest
 public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJobTest<T> {
 
+    private final @OpType int mOpType;
+
+    AbstractCopyJobTest(@OpType int opType) {
+        mOpType = opType;
+    }
+
     public void runCopyFilesTest() throws Exception {
         Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
         mDocs.writeDocument(testFile1, HAM_BYTES);
@@ -111,7 +116,7 @@
     public void runNoCopyDirToSelfTest() throws Exception {
         Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
 
-        createJob(OPERATION_COPY,
+        createJob(mOpType,
                 newArrayList(testDir),
                 DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
                 testDir).run();
@@ -127,7 +132,7 @@
         Uri testDir = mDocs.createFolder(mSrcRoot, "someDir");
         Uri destDir = mDocs.createFolder(testDir, "theDescendent");
 
-        createJob(OPERATION_COPY,
+        createJob(mOpType,
                 newArrayList(testDir),
                 DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId),
                 destDir).run();
@@ -163,6 +168,6 @@
     final T createJob(List<Uri> srcs) throws Exception {
         Uri srcParent = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
         Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
-        return createJob(OPERATION_COPY, srcs, srcParent, destination);
+        return createJob(mOpType, 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 c3cbe3f..053942b 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/AbstractJobTest.java
@@ -27,14 +27,14 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 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.android.documentsui.services.FileOperationService.OpType;
-import com.android.documentsui.testing.ClipDetailsFactory;
+import com.android.documentsui.testing.DocsProviders;
 
 import java.util.List;
 
@@ -91,10 +91,13 @@
         stack.push(DocumentInfo.fromUri(mResolver, destination));
         stack.root = mSrcRoot;
 
-        ClipDetails details = ClipDetailsFactory.createClipDetails(opType, srcParent, srcs);
-        return createJob(details, stack);
+        UrisSupplier urisSupplier = DocsProviders.createDocsProvider(srcs);
+        FileOperation operation = new FileOperation.Builder()
+                .withOpType(opType)
+                .withSrcs(urisSupplier)
+                .withDestination(stack)
+                .withSrcParent(srcParent)
+                .build();
+        return (T) operation.createJob(mContext, mJobListener, FileOperations.createJobId());
     }
-
-    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 eac06ca..64211c2 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/CopyJobTest.java
@@ -16,18 +16,21 @@
 
 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;
 import android.provider.DocumentsContract.Document;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.ClipDetails;
-import com.android.documentsui.model.DocumentStack;
-
 @MediumTest
 public class CopyJobTest extends AbstractCopyJobTest<CopyJob> {
 
+    public CopyJobTest() {
+        super(OPERATION_COPY);
+    }
+
     public void testCopyFiles() throws Exception {
         runCopyFilesTest();
     }
@@ -74,11 +77,4 @@
     public void testCopyFileWithReadErrors() throws Exception {
         runCopyFileWithReadErrorsTest();
     }
-
-    @Override
-    CopyJob createJob(ClipDetails details, DocumentStack stack)
-            throws Exception {
-        return new CopyJob(
-                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 050c7ea..9dbe7ce 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/DeleteJobTest.java
@@ -24,9 +24,6 @@
 import android.provider.DocumentsContract;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.ClipDetails;
-import com.android.documentsui.model.DocumentStack;
-
 import java.util.List;
 
 @MediumTest
@@ -53,12 +50,4 @@
         Uri stack = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
         return createJob(OPERATION_DELETE, srcs, srcParent, stack);
     }
-
-    // TODO: Remove inheritance, as stack is not used for deleting, nor srcParent.
-    @Override
-    DeleteJob createJob(ClipDetails details, DocumentStack stack)
-            throws Exception {
-        return new DeleteJob(
-                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 e16d5ae..b49d15d 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/FileOperationServiceTest.java
@@ -18,6 +18,7 @@
 
 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.OpType;
 import static com.android.documentsui.services.FileOperations.createBaseIntent;
 import static com.android.documentsui.services.FileOperations.createJobId;
 
@@ -26,14 +27,15 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.test.ServiceTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 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.DocsProviders;
 import com.android.documentsui.testing.TestHandler;
 import com.android.documentsui.testing.TestScheduledExecutorService;
 
@@ -50,11 +52,13 @@
     private static final DocumentInfo GAMMA_DOC = createDoc("gamma");
     private static final DocumentInfo DELTA_DOC = createDoc("delta");
 
+    private final List<TestJob> mCopyJobs = new ArrayList<>();
+    private final List<TestJob> mDeleteJobs = new ArrayList<>();
+
     private FileOperationService mService;
     private TestScheduledExecutorService mExecutor;
     private TestScheduledExecutorService mDeletionExecutor;
     private TestHandler mHandler;
-    private TestJobFactory mJobFactory;
 
     public FileOperationServiceTest() {
         super(FileOperationService.class);
@@ -68,7 +72,9 @@
         mExecutor = new TestScheduledExecutorService();
         mDeletionExecutor = new TestScheduledExecutorService();
         mHandler = new TestHandler();
-        mJobFactory = new TestJobFactory();
+
+        mCopyJobs.clear();
+        mDeleteJobs.clear();
 
         // Install test doubles.
         mService = getService();
@@ -81,9 +87,13 @@
 
         assertNull(mService.handler);
         mService.handler = mHandler;
+    }
 
-        assertNull(mService.jobFactory);
-        mService.jobFactory = mJobFactory;
+    @Override
+    protected void tearDown() {
+        // There are lots of progress notifications generated in this test case.
+        // Dismiss all of them here.
+        mHandler.dispatchAllMessages();
     }
 
     public void testRunsCopyJobs() throws Exception {
@@ -91,7 +101,7 @@
         startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
 
         mExecutor.runAll();
-        mJobFactory.assertAllCopyJobsStarted();
+        assertAllCopyJobsStarted();
     }
 
     public void testRunsCopyJobs_AfterExceptionInJobCreation() throws Exception {
@@ -102,20 +112,20 @@
         }
         startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
 
-        mJobFactory.assertJobsCreated(1);
+        assertJobsCreated(1);
 
         mExecutor.runAll();
-        mJobFactory.assertAllCopyJobsStarted();
+        assertAllCopyJobsStarted();
     }
 
     public void testRunsCopyJobs_AfterFailure() throws Exception {
         startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
         startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
 
-        mJobFactory.copyJobs.get(0).fail(ALPHA_DOC);
+        mCopyJobs.get(0).fail(ALPHA_DOC);
 
         mExecutor.runAll();
-        mJobFactory.assertAllCopyJobsStarted();
+        assertAllCopyJobsStarted();
     }
 
     public void testRunsCopyJobs_notRunsDeleteJobs() throws Exception {
@@ -123,14 +133,14 @@
         startService(createDeleteIntent(newArrayList(GAMMA_DOC)));
 
         mExecutor.runAll();
-        mJobFactory.assertNoDeleteJobsStarted();
+        assertNoDeleteJobsStarted();
     }
 
     public void testRunsDeleteJobs() throws Exception {
         startService(createDeleteIntent(newArrayList(ALPHA_DOC)));
 
         mDeletionExecutor.runAll();
-        mJobFactory.assertAllDeleteJobsStarted();
+        assertAllDeleteJobsStarted();
     }
 
     public void testRunsDeleteJobs_NotRunsCopyJobs() throws Exception {
@@ -138,7 +148,7 @@
         startService(createDeleteIntent(newArrayList(GAMMA_DOC)));
 
         mDeletionExecutor.runAll();
-        mJobFactory.assertNoCopyJobsStarted();
+        assertNoCopyJobsStarted();
     }
 
     public void testUpdatesNotification() throws Exception {
@@ -148,7 +158,7 @@
         // Assert monitoring continues until job is done
         assertTrue(mHandler.hasScheduledMessage());
         // Two notifications -- one for setup; one for progress
-        assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications());
+        assertEquals(2, mCopyJobs.get(0).getNumOfNotifications());
     }
 
     public void testStopsUpdatingNotificationAfterFinished() throws Exception {
@@ -160,7 +170,7 @@
         assertFalse(mHandler.hasScheduledMessage());
 
         // Assert no more notification is generated after finish.
-        assertEquals(2, mJobFactory.copyJobs.get(0).getNumOfNotifications());
+        assertEquals(2, mCopyJobs.get(0).getNumOfNotifications());
 
     }
 
@@ -202,7 +212,7 @@
         startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
         startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
 
-        mJobFactory.copyJobs.get(0).fail(ALPHA_DOC);
+        mCopyJobs.get(0).fail(ALPHA_DOC);
 
         mExecutor.runAll();
         shutdownService();
@@ -214,8 +224,8 @@
         startService(createCopyIntent(newArrayList(ALPHA_DOC), BETA_DOC));
         startService(createCopyIntent(newArrayList(GAMMA_DOC), DELTA_DOC));
 
-        mJobFactory.copyJobs.get(0).fail(ALPHA_DOC);
-        mJobFactory.copyJobs.get(1).fail(GAMMA_DOC);
+        mCopyJobs.get(0).fail(ALPHA_DOC);
+        mCopyJobs.get(1).fail(GAMMA_DOC);
 
         mExecutor.runAll();
         shutdownService();
@@ -233,10 +243,10 @@
             uris.add(file.derivedUri);
         }
 
-        ClipDetails details =
-                ClipDetailsFactory.createClipDetails(OPERATION_COPY, SRC_PARENT, uris);
+        UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris);
+        TestFileOperation operation = new TestFileOperation(OPERATION_COPY, urisSupplier, stack);
 
-        return createBaseIntent(getContext(), createJobId(), details, stack);
+        return createBaseIntent(getContext(), createJobId(), operation);
     }
 
     private Intent createDeleteIntent(ArrayList<DocumentInfo> files) {
@@ -247,10 +257,10 @@
             uris.add(file.derivedUri);
         }
 
-        ClipDetails details =
-                ClipDetailsFactory.createClipDetails(OPERATION_DELETE, SRC_PARENT, uris);
+        UrisSupplier urisSupplier = DocsProviders.createDocsProvider(uris);
+        TestFileOperation operation = new TestFileOperation(OPERATION_DELETE, urisSupplier, stack);
 
-        return createBaseIntent(getContext(), createJobId(), details, stack);
+        return createBaseIntent(getContext(), createJobId(), operation);
     }
 
     private static DocumentInfo createDoc(String name) {
@@ -264,6 +274,33 @@
         return createDoc(uri);
     }
 
+    void assertAllCopyJobsStarted() {
+        for (TestJob job : mCopyJobs) {
+            job.assertStarted();
+        }
+    }
+
+    void assertAllDeleteJobsStarted() {
+        for (TestJob job : mDeleteJobs) {
+            job.assertStarted();
+        }
+    }
+
+    void assertNoCopyJobsStarted() {
+        for (TestJob job : mCopyJobs) {
+            job.assertNotStarted();
+        }
+    }
+
+    void assertNoDeleteJobsStarted() {
+        for (TestJob job : mDeleteJobs) {
+            job.assertNotStarted();
+        }
+    }
+
+    void assertJobsCreated(int expected) {
+        assertEquals(expected, mCopyJobs.size() + mDeleteJobs.size());
+    }
     private static DocumentInfo createDoc(Uri destination) {
         DocumentInfo destDoc = new DocumentInfo();
         destDoc.derivedUri = destination;
@@ -275,72 +312,56 @@
         mDeletionExecutor.assertShutdown();
     }
 
-    private final class TestJobFactory extends Job.Factory {
+    private final class TestFileOperation extends FileOperation {
 
-        private final List<TestJob> copyJobs = new ArrayList<>();
-        private final List<TestJob> deleteJobs = new ArrayList<>();
-
-        private Runnable mJobRunnable = () -> {
+        private final Runnable mJobRunnable = () -> {
             // The following statement is executed concurrently to Job.start() in real situation.
             // Call it in TestJob.start() to mimic this behavior.
             mHandler.dispatchNextMessage();
         };
+        private final @OpType int mOpType;
+        private final UrisSupplier mSrcs;
+        private final DocumentStack mDestination;
 
-        void assertAllCopyJobsStarted() {
-            for (TestJob job : copyJobs) {
-                job.assertStarted();
-            }
-        }
-
-        void assertAllDeleteJobsStarted() {
-            for (TestJob job : deleteJobs) {
-                job.assertStarted();
-            }
-        }
-
-        void assertNoCopyJobsStarted() {
-            for (TestJob job : copyJobs) {
-                job.assertNotStarted();
-            }
-        }
-
-        void assertNoDeleteJobsStarted() {
-            for (TestJob job : deleteJobs) {
-                job.assertNotStarted();
-            }
-        }
-
-        void assertJobsCreated(int expected) {
-            assertEquals(expected, copyJobs.size() + deleteJobs.size());
+        private TestFileOperation(
+                @OpType int opType, UrisSupplier srcs, DocumentStack destination) {
+            super(opType, srcs, destination);
+            mOpType = opType;
+            mSrcs = srcs;
+            mDestination = destination;
         }
 
         @Override
-        Job createCopy(Context service, Context appContext, Listener listener, String id,
-                DocumentStack stack, ClipDetails details) {
+        public Job createJob(Context service, Job.Listener listener, String id) {
+            TestJob job =
+                    new TestJob(service, listener, id, mOpType, mDestination, mSrcs, mJobRunnable);
 
-            if (details.getItemCount() == 0) {
-                throw new RuntimeException("Empty srcs not supported!");
+            if (mOpType == OPERATION_COPY) {
+                mCopyJobs.add(job);
             }
 
-            TestJob job = new TestJob(
-                    service, appContext, listener, id, stack, details, mJobRunnable);
-            copyJobs.add(job);
+            if (mOpType == OPERATION_DELETE) {
+                mDeleteJobs.add(job);
+            }
+
             return job;
         }
 
-        @Override
-        Job createDelete(Context service, Context appContext, Listener listener, String id,
-                DocumentStack stack, ClipDetails details) {
+        /**
+         * CREATOR is required for Parcelables, but we never pass this class via parcel.
+         */
+        public Parcelable.Creator<TestFileOperation> CREATOR =
+                new Parcelable.Creator<TestFileOperation>() {
 
-            if (details.getItemCount() == 0) {
-                throw new RuntimeException("Empty srcs not supported!");
+            @Override
+            public TestFileOperation createFromParcel(Parcel source) {
+                throw new UnsupportedOperationException("Can't create from a parcel.");
             }
 
-            TestJob job = new TestJob(
-                    service, appContext, listener, id, stack, details, mJobRunnable);
-            deleteJobs.add(job);
-
-            return job;
-        }
+            @Override
+            public TestFileOperation[] newArray(int size) {
+                throw new UnsupportedOperationException("Can't create a new array.");
+            }
+        };
     }
 }
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 fd5c92a..56d96cc 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/MoveJobTest.java
@@ -16,18 +16,21 @@
 
 package com.android.documentsui.services;
 
+import static com.android.documentsui.services.FileOperationService.OPERATION_MOVE;
+
 import static com.google.common.collect.Lists.newArrayList;
 
 import android.net.Uri;
 import android.provider.DocumentsContract.Document;
 import android.test.suitebuilder.annotation.MediumTest;
 
-import com.android.documentsui.ClipDetails;
-import com.android.documentsui.model.DocumentStack;
-
 @MediumTest
 public class MoveJobTest extends AbstractCopyJobTest<MoveJob> {
 
+    public MoveJobTest() {
+        super(OPERATION_MOVE);
+    }
+
     public void testMoveFiles() throws Exception {
         runCopyFilesTest();
 
@@ -105,11 +108,4 @@
     }
 
     // TODO: Add test cases for moving when multi-parented.
-
-    @Override
-    MoveJob createJob(ClipDetails details, DocumentStack stack)
-            throws Exception {
-        return new MoveJob(
-                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 a7e1d66..0c273c0 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/services/TestJob.java
@@ -23,10 +23,11 @@
 import android.app.Notification.Builder;
 import android.content.Context;
 
-import com.android.documentsui.ClipDetails;
+import com.android.documentsui.UrisSupplier;
 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 java.text.NumberFormat;
 
@@ -38,9 +39,9 @@
     private int mNumOfNotifications = 0;
 
     TestJob(
-            Context service, Context appContext, Listener listener,
-            String id, DocumentStack stack, ClipDetails details, Runnable startRunnable) {
-        super(service, appContext, listener, id, stack, details);
+            Context service, Listener listener, String id,
+            @OpType int opType, DocumentStack stack, UrisSupplier srcs, Runnable startRunnable) {
+        super(service, listener, id, opType, stack, srcs);
 
         mStartRunnable = startRunnable;
     }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DocsProviders.java
similarity index 66%
rename from packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java
rename to packages/DocumentsUI/tests/src/com/android/documentsui/testing/DocsProviders.java
index d833528..d438892 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/ClipDetailsFactory.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DocsProviders.java
@@ -18,15 +18,14 @@
 
 import android.net.Uri;
 
-import com.android.documentsui.ClipDetails;
-import com.android.documentsui.services.FileOperationService.OpType;
+import com.android.documentsui.UrisSupplier;
 
 import java.util.List;
 
-public final class ClipDetailsFactory {
-    private ClipDetailsFactory() {}
+public final class DocsProviders {
+    private DocsProviders() {}
 
-    public static ClipDetails createClipDetails(@OpType int opType, Uri srcParent, List<Uri> docs) {
-        return new ClipDetails.StandardClipDetails(opType,  srcParent, docs);
+    public static UrisSupplier createDocsProvider(List<Uri> docs) {
+        return new UrisSupplier.StandardUrisSupplier(docs);
     }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestHandler.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestHandler.java
index c18ef1f..143ec71 100644
--- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestHandler.java
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestHandler.java
@@ -41,6 +41,12 @@
         mTimer.fastForwardToNextTask();
     }
 
+    public void dispatchAllMessages() {
+        while (hasScheduledMessage()) {
+            dispatchNextMessage();
+        }
+    }
+
     @Override
     public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
         msg.setTarget(this);