Merge "[multi-part] Eliminate 1k selection limit"
diff --git a/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java b/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java
new file mode 100644
index 0000000..0167acc
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/ClipStorage.java
@@ -0,0 +1,136 @@
+/*
+ * 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 android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides support for storing lists of documents identified by Uri.
+ *
+ * <li>Access to this object *must* be synchronized externally.
+ * <li>All calls to this class are I/O intensive and must be wrapped in an AsyncTask.
+ */
+public final class ClipStorage {
+
+    private static final String PRIMARY_SELECTION = "primary-selection.txt";
+    private static final byte[] LINE_SEPARATOR = System.lineSeparator().getBytes();
+    private static final int NO_SELECTION_TAG = -1;
+
+    private final File mOutDir;
+
+    /**
+     * @param outDir see {@link #prepareStorage(File)}.
+     */
+    public ClipStorage(File outDir) {
+        assert(outDir.isDirectory());
+        mOutDir = outDir;
+    }
+
+    /**
+     * Returns a writer. Callers must...
+     *
+     * <li>synchronize on the {@link ClipStorage} instance while writing to this writer.
+     * <li>closed the write when finished.
+     */
+    public Writer createWriter() throws IOException {
+        File primary = new File(mOutDir, PRIMARY_SELECTION);
+        return new Writer(new FileOutputStream(primary));
+    }
+
+    /**
+     * Saves primary uri list to persistent storage.
+     * @return tag identifying the saved set.
+     */
+    @VisibleForTesting
+    public long savePrimary() throws IOException {
+        File primary = new File(mOutDir, PRIMARY_SELECTION);
+
+        if (!primary.exists()) {
+            return NO_SELECTION_TAG;
+        }
+
+        long tag = System.currentTimeMillis();
+        File dest = toTagFile(tag);
+        primary.renameTo(dest);
+
+        return tag;
+    }
+
+    @VisibleForTesting
+    public List<Uri> read(long tag) throws IOException {
+        List<Uri> uris = new ArrayList<>();
+        File tagFile = toTagFile(tag);
+        try (BufferedReader in = new BufferedReader(new FileReader(tagFile))) {
+            String line = null;
+            while ((line = in.readLine()) != null) {
+                uris.add(Uri.parse(line));
+            }
+        }
+        return uris;
+    }
+
+    @VisibleForTesting
+    public void delete(long tag) throws IOException {
+        toTagFile(tag).delete();
+    }
+
+    private File toTagFile(long tag) {
+        return new File(mOutDir, String.valueOf(tag));
+    }
+
+    public static final class Writer implements Closeable {
+
+        private final FileOutputStream mOut;
+
+        public Writer(FileOutputStream out) {
+            mOut = out;
+        }
+
+        public void write(Uri uri) throws IOException {
+            mOut.write(uri.toString().getBytes());
+            mOut.write(LINE_SEPARATOR);
+        }
+
+        @Override
+        public void close() throws IOException {
+            mOut.close();
+        }
+    }
+
+    /**
+     * Provides initialization and cleanup of the clip data storage directory.
+     */
+    static File prepareStorage(File cacheDir) {
+        File clipDir = new File(cacheDir, "clippings");
+        if (clipDir.exists()) {
+            Files.deleteRecursively(clipDir);
+        }
+        assert(!clipDir.exists());
+        clipDir.mkdir();
+        return clipDir;
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
index cc9ab97..3d8ac2c 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentClipper.java
@@ -17,6 +17,7 @@
 package com.android.documentsui;
 
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ClipboardManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -27,6 +28,8 @@
 import android.support.annotation.Nullable;
 import android.util.Log;
 
+import com.android.documentsui.ClipStorage.Writer;
+import com.android.documentsui.dirlist.MultiSelectManager.Selection;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 import com.android.documentsui.model.RootInfo;
@@ -34,11 +37,13 @@
 import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
 
+import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
 
 /**
  * ClipboardManager wrapper class providing higher level logical
@@ -49,12 +54,15 @@
     private static final String TAG = "DocumentClipper";
     private static final String SRC_PARENT_KEY = "srcParent";
     private static final String OP_TYPE_KEY = "opType";
+    private static final String OP_JUMBO_SELECTION_SIZE = "jumboSelection-size";
 
-    private Context mContext;
-    private ClipboardManager mClipboard;
+    private final Context mContext;
+    private final ClipStorage mClipStorage;
+    private final ClipboardManager mClipboard;
 
-    DocumentClipper(Context context) {
+    DocumentClipper(Context context, ClipStorage storage) {
         mContext = context;
+        mClipStorage = storage;
         mClipboard = context.getSystemService(ClipboardManager.class);
     }
 
@@ -79,13 +87,6 @@
         return uri != null && DocumentsContract.isDocumentUri(mContext, uri);
     }
 
-    /**
-     * Returns details regarding the documents on the primary clipboard
-     */
-    public ClipDetails getClipDetails() {
-        return getClipDetails(mClipboard.getPrimaryClip());
-    }
-
     public ClipDetails getClipDetails(@Nullable ClipData clipData) {
         if (clipData == null) {
             return null;
@@ -127,54 +128,108 @@
     }
 
     /**
+     * Returns {@link ClipData} representing the selection, or null if selection is empty,
+     * or cannot be converted.
+     */
+    public @Nullable ClipData getClipDataForDocuments(
+        Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+
+        assert(selection != null);
+
+        if (selection.isEmpty()) {
+            Log.w(TAG, "Attempting to clip empty selection. Ignoring.");
+            return null;
+        }
+
+        return (selection.size() > Shared.MAX_DOCS_IN_INTENT)
+                ? createJumboClipData(uriBuilder, selection, opType)
+                : createStandardClipData(uriBuilder, selection, opType);
+    }
+
+    /**
+     * Returns ClipData representing the selection.
+     */
+    private @Nullable ClipData createStandardClipData(
+            Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
+
+        assert(!selection.isEmpty());
+
+        final ContentResolver resolver = mContext.getContentResolver();
+        final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
+        final Set<String> clipTypes = new HashSet<>();
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(OP_TYPE_KEY, opType);
+
+        int clipCount = 0;
+        for (String id : selection) {
+            assert(id != null);
+            Uri uri = uriBuilder.apply(id);
+            if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
+                DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
+                clipItems.add(new ClipData.Item(uri));
+            }
+            clipCount++;
+        }
+
+        ClipDescription description = new ClipDescription(
+                "", // Currently "label" is not displayed anywhere in the UI.
+                clipTypes.toArray(new String[0]));
+        description.setExtras(bundle);
+
+        return new ClipData(description, clipItems);
+    }
+
+    /**
      * Returns ClipData representing the list of docs, or null if docs is empty,
      * or docs cannot be converted.
      */
-    public @Nullable ClipData getClipDataForDocuments(List<DocumentInfo> docs, @OpType int opType) {
-        final ContentResolver resolver = mContext.getContentResolver();
-        final String[] mimeTypes = getMimeTypes(resolver, docs);
-        ClipData clipData = null;
-        for (DocumentInfo doc : docs) {
-            assert(doc != null);
-            assert(doc.derivedUri != null);
-            if (clipData == null) {
-                // TODO: figure out what this string should be.
-                // Currently it is not displayed anywhere in the UI, but this might change.
-                final String clipLabel = "";
-                clipData = new ClipData(clipLabel, mimeTypes, new ClipData.Item(doc.derivedUri));
-                PersistableBundle bundle = new PersistableBundle();
-                bundle.putInt(OP_TYPE_KEY, opType);
-                clipData.getDescription().setExtras(bundle);
-            } else {
-                // TODO: update list of mime types in ClipData.
-                clipData.addItem(new ClipData.Item(doc.derivedUri));
-            }
-        }
-        return clipData;
-    }
+    private @Nullable ClipData createJumboClipData(
+            Function<String, Uri> uriBuilder, Selection selection, @OpType int opType) {
 
-    private static String[] getMimeTypes(ContentResolver resolver, List<DocumentInfo> docs) {
-        final HashSet<String> mimeTypes = new HashSet<>();
-        for (DocumentInfo doc : docs) {
-            assert(doc != null);
-            assert(doc.derivedUri != null);
-            final Uri uri = doc.derivedUri;
-            if ("content".equals(uri.getScheme())) {
-                mimeTypes.add(resolver.getType(uri));
-                final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
-                if (streamTypes != null) {
-                    mimeTypes.addAll(Arrays.asList(streamTypes));
+        assert(!selection.isEmpty());
+
+        final ContentResolver resolver = mContext.getContentResolver();
+        final ArrayList<ClipData.Item> clipItems = new ArrayList<>();
+        final Set<String> clipTypes = new HashSet<>();
+
+        PersistableBundle bundle = new PersistableBundle();
+        bundle.putInt(OP_TYPE_KEY, opType);
+        bundle.putInt(OP_JUMBO_SELECTION_SIZE, selection.size());
+
+        int clipCount = 0;
+        synchronized (mClipStorage) {
+            try (Writer writer = mClipStorage.createWriter()) {
+                for (String id : selection) {
+                    assert(id != null);
+                    Uri uri = uriBuilder.apply(id);
+                    if (clipCount <= Shared.MAX_DOCS_IN_INTENT) {
+                        DocumentInfo.addMimeTypes(resolver, uri, clipTypes);
+                        clipItems.add(new ClipData.Item(uri));
+                    }
+                    writer.write(uri);
+                    clipCount++;
                 }
+            } catch (IOException e) {
+                Log.e(TAG, "Caught exception trying to write jumbo clip to disk.", e);
+                return null;
             }
         }
-        return mimeTypes.toArray(new String[0]);
+
+        ClipDescription description = new ClipDescription(
+                "", // Currently "label" is not displayed anywhere in the UI.
+                clipTypes.toArray(new String[0]));
+        description.setExtras(bundle);
+
+        return new ClipData(description, clipItems);
     }
 
     /**
      * Puts {@code ClipData} in a primary clipboard, describing a copy operation
      */
-    public void clipDocumentsForCopy(List<DocumentInfo> docs) {
-        ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_COPY);
+    public void clipDocumentsForCopy(Function<String, Uri> uriBuilder, Selection selection) {
+        ClipData data =
+                getClipDataForDocuments(uriBuilder, selection, FileOperationService.OPERATION_COPY);
         assert(data != null);
 
         mClipboard.setPrimaryClip(data);
@@ -183,24 +238,24 @@
     /**
      *  Puts {@Code ClipData} in a primary clipboard, describing a cut operation
      */
-    public void clipDocumentsForCut(List<DocumentInfo> docs, DocumentInfo srcParent) {
-        assert(docs != null);
-        assert(!docs.isEmpty());
-        assert(srcParent != null);
-        assert(srcParent.derivedUri != null);
+    public void clipDocumentsForCut(
+            Function<String, Uri> uriBuilder, Selection selection, DocumentInfo parent) {
+        assert(!selection.isEmpty());
+        assert(parent.derivedUri != null);
 
-        ClipData data = getClipDataForDocuments(docs, FileOperationService.OPERATION_MOVE);
+        ClipData data = getClipDataForDocuments(uriBuilder, selection,
+                FileOperationService.OPERATION_MOVE);
         assert(data != null);
 
         PersistableBundle bundle = data.getDescription().getExtras();
-        bundle.putString(SRC_PARENT_KEY, srcParent.derivedUri.toString());
+        bundle.putString(SRC_PARENT_KEY, parent.derivedUri.toString());
 
         mClipboard.setPrimaryClip(data);
     }
 
     private DocumentInfo createDocument(Uri uri) {
         DocumentInfo doc = null;
-        if (uri != null && DocumentsContract.isDocumentUri(mContext, uri)) {
+        if (isDocumentUri(uri)) {
             ContentResolver resolver = mContext.getContentResolver();
             try {
                 doc = DocumentInfo.fromUri(resolver, uri);
@@ -219,8 +274,11 @@
      * @param docStack the document stack to the destination folder,
      * @param callback callback to notify when operation finishes.
      */
-    public void copyFromClipboard(DocumentInfo destination, DocumentStack docStack,
+    public void copyFromClipboard(
+            DocumentInfo destination,
+            DocumentStack docStack,
             FileOperations.Callback callback) {
+
         copyFromClipData(destination, docStack, mClipboard.getPrimaryClip(), callback);
     }
 
@@ -232,8 +290,12 @@
      * @param clipData the clipData to copy from, or null to copy from clipboard
      * @param callback callback to notify when operation finishes
      */
-    public void copyFromClipData(final DocumentInfo destination, DocumentStack docStack,
-            @Nullable final ClipData clipData, final FileOperations.Callback callback) {
+    public void copyFromClipData(
+            final DocumentInfo destination,
+            DocumentStack docStack,
+            final @Nullable ClipData clipData,
+            final FileOperations.Callback callback) {
+
         if (clipData == null) {
             Log.i(TAG, "Received null clipData. Ignoring.");
             return;
@@ -308,7 +370,7 @@
      *
      * @return true if the list of files can be copied to destination.
      */
-    private boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
+    private static boolean canCopy(List<DocumentInfo> files, RootInfo root, DocumentInfo dest) {
         if (dest == null || !dest.isDirectory() || !dest.isCreateSupported()) {
             return false;
         }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
index 0c61501..2b2d1f4 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsApplication.java
@@ -28,13 +28,14 @@
 import android.os.RemoteException;
 import android.text.format.DateUtils;
 
+import java.io.File;
+
 public class DocumentsApplication extends Application {
     private static final long PROVIDER_ANR_TIMEOUT = 20 * DateUtils.SECOND_IN_MILLIS;
 
     private RootsCache mRoots;
 
     private ThumbnailCache mThumbnailCache;
-
     private DocumentClipper mClipper;
 
     public static RootsCache getRootsCache(Context context) {
@@ -73,7 +74,7 @@
 
         mThumbnailCache = new ThumbnailCache(memoryClassBytes / 4);
 
-        mClipper = new DocumentClipper(this);
+        mClipper = createClipper(this.getApplicationContext());
 
         final IntentFilter packageFilter = new IntentFilter();
         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
@@ -88,6 +89,12 @@
         registerReceiver(mCacheReceiver, localeFilter);
     }
 
+    private static DocumentClipper createClipper(Context context) {
+        // prepare storage handles initialization and cleanup of the clip directory.
+        File clipDir = ClipStorage.prepareStorage(context.getCacheDir());
+        return new DocumentClipper(context, new ClipStorage(clipDir));
+    }
+
     @Override
     public void onTrimMemory(int level) {
         super.onTrimMemory(level);
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Files.java b/packages/DocumentsUI/src/com/android/documentsui/Files.java
new file mode 100644
index 0000000..38f98be
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/Files.java
@@ -0,0 +1,38 @@
+/*
+ * 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 java.io.File;
+
+/**
+ * Utility class for working with {@link File} instances.
+ */
+public final class Files {
+
+    private Files() {}  // no initialization for utility classes.
+
+    public static void deleteRecursively(File file) {
+        if (file.exists()) {
+            if (file.isDirectory()) {
+                for (File child : file.listFiles()) {
+                    deleteRecursively(child);
+                }
+            }
+            file.delete();
+        }
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
index be20c0e..091ae6d 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/FilesActivity.java
@@ -487,7 +487,7 @@
         }
 
         @Override
-        protected Void run(Uri... params) {
+        public Void run(Uri... params) {
             final Uri uri = params[0];
 
             final RootsCache rootsCache = DocumentsApplication.getRootsCache(mOwner);
@@ -512,7 +512,7 @@
         }
 
         @Override
-        protected void finish(Void result) {
+        public void finish(Void result) {
             mOwner.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE);
         }
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/Shared.java b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
index 24755f3..2a81c48 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/Shared.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/Shared.java
@@ -110,7 +110,7 @@
     /**
      * Maximum number of items in a Binder transaction packet.
      */
-    public static final int MAX_DOCS_IN_INTENT = 1000;
+    public static final int MAX_DOCS_IN_INTENT = 500;
 
     private static final Collator sCollator;
 
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java
new file mode 100644
index 0000000..3aefffb
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/ClipTask.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.support.design.widget.Snackbar;
+
+import com.android.documentsui.R;
+import com.android.documentsui.Shared;
+import com.android.documentsui.Snackbars;
+
+/**
+ * AsyncTask that performs a supplied runnable (presumably doing some clippy thing)in background,
+ * then shows a toast reciting how many fantastic things have been clipped.
+ */
+final class ClipTask extends AsyncTask<Void, Void, Void> {
+
+    private Runnable mOperation;
+    private int mSelectionSize;
+    private Activity mActivity;
+
+    ClipTask(Activity activity, Runnable operation, int selectionSize) {
+        mActivity = activity;
+        mOperation = operation;
+        mSelectionSize = selectionSize;
+    }
+
+    @Override
+    protected Void doInBackground(Void... params) {
+        // Clip operation varies (cut or past) and has different inputs.
+        // To increase sharing we accept the no ins/outs operation as a plain runnable.
+        mOperation.run();
+        return null;
+    }
+
+    @Override
+    protected void onPostExecute(Void result) {
+        String msg = Shared.getQuantityString(
+                mActivity,
+                R.plurals.clipboard_files_clipped,
+                mSelectionSize);
+
+        Snackbars.makeSnackbar(mActivity, msg, Snackbar.LENGTH_SHORT)
+                .show();
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
index 7f35854..8e9bf3e 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -38,9 +38,6 @@
 import android.content.Intent;
 import android.content.Loader;
 import android.database.Cursor;
-import android.graphics.Canvas;
-import android.graphics.Point;
-import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
@@ -48,7 +45,6 @@
 import android.os.Parcelable;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
-import android.support.design.widget.Snackbar;
 import android.support.v13.view.DragStartHelper;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
@@ -109,8 +105,6 @@
 import com.android.documentsui.services.FileOperationService.OpType;
 import com.android.documentsui.services.FileOperations;
 
-import com.google.common.collect.Lists;
-
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -966,6 +960,7 @@
                     .setPositiveButton(
                          android.R.string.ok,
                          new DialogInterface.OnClickListener() {
+                            @Override
                             public void onClick(DialogInterface dialog, int id) {
                                 // Finish selection mode first which clears selection so we
                                 // don't end up trying to deselect deleted documents.
@@ -1156,19 +1151,15 @@
         if (selection.isEmpty()) {
             return;
         }
-
-        new GetDocumentsTask() {
-            @Override
-            void onDocumentsReady(List<DocumentInfo> docs) {
-                mClipper.clipDocumentsForCopy(docs);
-                Activity activity = getActivity();
-                Snackbars.makeSnackbar(activity,
-                        activity.getResources().getQuantityString(
-                                R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
-                        Snackbar.LENGTH_SHORT).show();
-            }
-        }.execute(selection);
         mSelectionManager.clearSelection();
+
+        // Clips the docs in the background, then displays a message
+        new ClipTask(
+                getActivity(),
+                () -> {
+                    mClipper.clipDocumentsForCopy(mModel::getItemUri, selection);
+                },
+                selection.size()).execute();
     }
 
     public void cutSelectedToClipboard() {
@@ -1178,21 +1169,18 @@
         if (selection.isEmpty()) {
             return;
         }
-
-        new GetDocumentsTask() {
-            @Override
-            void onDocumentsReady(List<DocumentInfo> docs) {
-                // We need the srcParent for move operations because we do a copy / delete
-                DocumentInfo currentDoc = getDisplayState().stack.peek();
-                mClipper.clipDocumentsForCut(docs, currentDoc);
-                Activity activity = getActivity();
-                Snackbars.makeSnackbar(activity,
-                        activity.getResources().getQuantityString(
-                                R.plurals.clipboard_files_clipped, docs.size(), docs.size()),
-                        Snackbar.LENGTH_SHORT).show();
-            }
-        }.execute(selection);
         mSelectionManager.clearSelection();
+
+        // Clips the docs in the background, then displays a message
+        new ClipTask(
+                getActivity(),
+                () -> {
+                    mClipper.clipDocumentsForCut(
+                            mModel::getItemUri,
+                            selection,
+                            getDisplayState().stack.peek());
+                },
+                selection.size()).execute();
     }
 
     public void pasteFromClipboard() {
@@ -1375,94 +1363,6 @@
         return null;
     }
 
-    private List<DocumentInfo> getDraggableDocuments(View currentItemView) {
-        String modelId = getModelId(currentItemView);
-        if (modelId == null) {
-            return Collections.EMPTY_LIST;
-        }
-
-        final List<DocumentInfo> selectedDocs =
-                mModel.getDocuments(mSelectionManager.getSelection());
-        if (!selectedDocs.isEmpty()) {
-            if (!isSelected(modelId)) {
-                // There is a selection that does not include the current item, drag nothing.
-                return Collections.EMPTY_LIST;
-            }
-            return selectedDocs;
-        }
-
-        final Cursor cursor = mModel.getItem(modelId);
-        if (cursor == null) {
-            Log.w(TAG, "Undraggable document. Can't obtain cursor for modelId " + modelId);
-            return Collections.EMPTY_LIST;
-        }
-
-        return Lists.newArrayList(
-                DocumentInfo.fromDirectoryCursor(cursor));
-    }
-
-    private static class DragShadowBuilder extends View.DragShadowBuilder {
-
-        private final Context mContext;
-        private final IconHelper mIconHelper;
-        private final LayoutInflater mInflater;
-        private final View mShadowView;
-        private final TextView mTitle;
-        private final ImageView mIcon;
-        private final int mWidth;
-        private final int mHeight;
-
-        public DragShadowBuilder(Context context, IconHelper iconHelper, List<DocumentInfo> docs) {
-            mContext = context;
-            mIconHelper = iconHelper;
-            mInflater = LayoutInflater.from(context);
-
-            mWidth = mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
-            mHeight= mContext.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
-
-            mShadowView = mInflater.inflate(R.layout.drag_shadow_layout, null);
-            mTitle = (TextView) mShadowView.findViewById(android.R.id.title);
-            mIcon = (ImageView) mShadowView.findViewById(android.R.id.icon);
-
-            mTitle.setText(getTitle(docs));
-            mIcon.setImageDrawable(getIcon(docs));
-        }
-
-        private Drawable getIcon(List<DocumentInfo> docs) {
-            if (docs.size() == 1) {
-                final DocumentInfo doc = docs.get(0);
-                return mIconHelper.getDocumentIcon(mContext, doc.authority, doc.documentId,
-                        doc.mimeType, doc.icon);
-            }
-            return mContext.getDrawable(R.drawable.ic_doc_generic);
-        }
-
-        private String getTitle(List<DocumentInfo> docs) {
-            if (docs.size() == 1) {
-                final DocumentInfo doc = docs.get(0);
-                return doc.displayName;
-            }
-            return Shared.getQuantityString(mContext, R.plurals.elements_dragged, docs.size());
-        }
-
-        @Override
-        public void onProvideShadowMetrics(
-                Point shadowSize, Point shadowTouchPoint) {
-            shadowSize.set(mWidth, mHeight);
-            shadowTouchPoint.set(mWidth, mHeight);
-        }
-
-        @Override
-        public void onDrawShadow(Canvas canvas) {
-            Rect r = canvas.getClipBounds();
-            // Calling measure is necessary in order for all child views to get correctly laid out.
-            mShadowView.measure(
-                    View.MeasureSpec.makeMeasureSpec(r.right- r.left, View.MeasureSpec.EXACTLY),
-                    View.MeasureSpec.makeMeasureSpec(r.top- r.bottom, View.MeasureSpec.EXACTLY));
-            mShadowView.layout(r.left, r.top, r.right, r.bottom);
-            mShadowView.draw(canvas);
-        }
-    }
     /**
      * Abstract task providing support for loading documents *off*
      * the main thread. And if it isn't obvious, creating a list
@@ -1615,29 +1515,68 @@
         }
     }
 
+    private Drawable getDragIcon(Selection selection) {
+        if (selection.size() == 1) {
+            DocumentInfo doc = getSingleSelectedDocument(selection);
+            return mIconHelper.getDocumentIcon(getContext(), doc);
+        }
+        return getContext().getDrawable(R.drawable.ic_doc_generic);
+    }
+
+    private String getDragTitle(Selection selection) {
+        assert (!selection.isEmpty());
+        if (selection.size() == 1) {
+            DocumentInfo doc = getSingleSelectedDocument(selection);
+            return doc.displayName;
+        }
+
+        return Shared.getQuantityString(getContext(), R.plurals.elements_dragged, selection.size());
+    }
+
+    private DocumentInfo getSingleSelectedDocument(Selection selection) {
+        assert (selection.size() == 1);
+        final List<DocumentInfo> docs = mModel.getDocuments(mSelectionManager.getSelection());
+        assert (docs.size() == 1);
+        return docs.get(0);
+    }
+
     private DragStartHelper.OnDragStartListener mOnDragStartListener =
             new DragStartHelper.OnDragStartListener() {
-        @Override
-        public boolean onDragStart(View v, DragStartHelper helper) {
-            if (isSelected(getModelId(v))) {
-                List<DocumentInfo> docs = getDraggableDocuments(v);
-                if (docs.isEmpty()) {
-                    return false;
-                }
-                v.startDragAndDrop(
-                        mClipper.getClipDataForDocuments(docs,
-                                FileOperationService.OPERATION_COPY),
-                        new DragShadowBuilder(getActivity(), mIconHelper, docs),
-                        getDisplayState().stack.peek(),
-                        View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_GLOBAL_URI_READ |
-                                View.DRAG_FLAG_GLOBAL_URI_WRITE
-                );
-                return true;
-            }
+                @Override
+                public boolean onDragStart(View v, DragStartHelper helper) {
+                    Selection selection = mSelectionManager.getSelection();
 
-            return false;
-        }
-    };
+                    if (v == null) {
+                        Log.d(TAG, "Ignoring drag event, null view");
+                        return false;
+                    }
+                    if (!isSelected(getModelId(v))) {
+                        Log.d(TAG, "Ignoring drag event, unselected view.");
+                        return false;
+                    }
+
+                    // NOTE: Preparation of the ClipData object can require a lot of time
+                    // and ideally should be done in the background. Unfortunately
+                    // the current code layout and framework assumptions don't support
+                    // this. So for now, we could end up doing a bunch of i/o on main thread.
+                    v.startDragAndDrop(
+                            mClipper.getClipDataForDocuments(
+                                    mModel::getItemUri,
+                                    selection,
+                                    FileOperationService.OPERATION_COPY),
+                            new DragShadowBuilder(
+                                    getActivity(),
+                                    getDragTitle(selection),
+                                    getDragIcon(selection)),
+                            getDisplayState().stack.peek(),
+                            View.DRAG_FLAG_GLOBAL
+                                    | View.DRAG_FLAG_GLOBAL_URI_READ
+                                    | View.DRAG_FLAG_GLOBAL_URI_WRITE);
+
+                    return true;
+                }
+            };
+
 
     private DragStartHelper mDragHelper = new DragStartHelper(null, mOnDragStartListener);
 
@@ -1892,7 +1831,7 @@
         updateLayout(state.derivedMode);
 
         if (mRestoredSelection != null) {
-            mSelectionManager.setItemsSelected(mRestoredSelection.getAll(), true);
+            mSelectionManager.restoreSelection(mRestoredSelection);
             // Note, we'll take care of cleaning up retained selection
             // in the selection handler where we already have some
             // specialized code to handle when selection was restored.
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/DragShadowBuilder.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DragShadowBuilder.java
new file mode 100644
index 0000000..c7d7a64
--- /dev/null
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/DragShadowBuilder.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui.dirlist;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.documentsui.R;
+
+final class DragShadowBuilder extends View.DragShadowBuilder {
+
+    private final View mShadowView;
+    private final TextView mTitle;
+    private final ImageView mIcon;
+    private final int mWidth;
+    private final int mHeight;
+
+    public DragShadowBuilder(Context context, String title, Drawable icon) {
+        mWidth = context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_width);
+        mHeight= context.getResources().getDimensionPixelSize(R.dimen.drag_shadow_height);
+
+        mShadowView = LayoutInflater.from(context).inflate(R.layout.drag_shadow_layout, null);
+        mTitle = (TextView) mShadowView.findViewById(android.R.id.title);
+        mIcon = (ImageView) mShadowView.findViewById(android.R.id.icon);
+
+        mTitle.setText(title);
+        mIcon.setImageDrawable(icon);
+    }
+
+    @Override
+    public void onProvideShadowMetrics(
+            Point shadowSize, Point shadowTouchPoint) {
+        shadowSize.set(mWidth, mHeight);
+        shadowTouchPoint.set(mWidth, mHeight);
+    }
+
+    @Override
+    public void onDrawShadow(Canvas canvas) {
+        Rect r = canvas.getClipBounds();
+        // Calling measure is necessary in order for all child views to get correctly laid out.
+        mShadowView.measure(
+                View.MeasureSpec.makeMeasureSpec(r.right- r.left, View.MeasureSpec.EXACTLY),
+                View.MeasureSpec.makeMeasureSpec(r.top- r.bottom, View.MeasureSpec.EXACTLY));
+        mShadowView.layout(r.left, r.top, r.right, r.bottom);
+        mShadowView.draw(canvas);
+    }
+}
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java
index 0c73992..ec72314 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/IconHelper.java
@@ -47,6 +47,7 @@
 import com.android.documentsui.State.ViewMode;
 import com.android.documentsui.ThumbnailCache;
 import com.android.documentsui.ThumbnailCache.Result;
+import com.android.documentsui.model.DocumentInfo;
 
 import java.util.function.BiConsumer;
 
@@ -293,22 +294,20 @@
         view.setAlpha(0f);
     }
 
-    /**
-     * Gets a mime icon or package icon for a file.
-     *
-     * @param context
-     * @param authority The authority string of the file.
-     * @param id The document ID of the file.
-     * @param mimeType The mime type of the file.
-     * @param icon The custom icon (if any) of the file.
-     * @return
-     */
-    public Drawable getDocumentIcon(Context context, String authority, String id,
-            String mimeType, int icon) {
+    private Drawable getDocumentIcon(
+        Context context, String authority, String id, String mimeType, int icon) {
         if (icon != 0) {
             return IconUtils.loadPackageIcon(context, authority, icon);
         } else {
             return IconUtils.loadMimeIcon(context, mimeType, authority, id, mMode);
         }
     }
+
+    /**
+     * Returns a mime icon or package icon for a {@link DocumentInfo}.
+     */
+    public Drawable getDocumentIcon(Context context, DocumentInfo doc) {
+        return getDocumentIcon(
+                context, doc.authority, doc.documentId, doc.mimeType, doc.icon);
+    }
 }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
index 0a2960f..5c15228 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/Model.java
@@ -25,6 +25,7 @@
 
 import android.database.Cursor;
 import android.database.MergeCursor;
+import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
@@ -39,7 +40,6 @@
 import com.android.documentsui.model.DocumentInfo;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -389,15 +389,15 @@
         return mIsLoading;
     }
 
-    List<DocumentInfo> getDocuments(Selection items) {
-        final int size = (items != null) ? items.size() : 0;
+    List<DocumentInfo> getDocuments(Selection selection) {
+        final int size = (selection != null) ? selection.size() : 0;
 
         final List<DocumentInfo> docs =  new ArrayList<>(size);
-        for (String modelId: items.getAll()) {
+        // NOTE: That as this now iterates over only final (non-provisional) selection.
+        for (String modelId: selection) {
             final Cursor cursor = getItem(modelId);
             if (cursor == null) {
-                Log.w(TAG,
-                        "Skipping document. Unabled to obtain cursor for modelId: " + modelId);
+                Log.w(TAG, "Skipping document. Unabled to obtain cursor for modelId: " + modelId);
                 continue;
             }
             docs.add(DocumentInfo.fromDirectoryCursor(cursor));
@@ -405,6 +405,11 @@
         return docs;
     }
 
+    public Uri getItemUri(String modelId) {
+        final Cursor cursor = getItem(modelId);
+        return DocumentInfo.getUri(cursor);
+    }
+
     void addUpdateListener(UpdateListener listener) {
         mUpdateListeners.add(listener);
     }
diff --git a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
index 497887c..eeefac0 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/dirlist/MultiSelectManager.java
@@ -34,6 +34,7 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -184,10 +185,12 @@
     }
 
     /**
-     * Updates selection to include items in {@code selection}.
+     * Returns an unordered array of selected positions, including any
+     * provisional selection currently in effect.
      */
-    public void updateSelection(Selection selection) {
-        setItemsSelected(selection.getAll(), true);
+    public void restoreSelection(Selection other) {
+        setItemsSelected(other.mSelection, true);
+        // NOTE: We intentionally don't restore provisional selection. It's provisional.
     }
 
     /**
@@ -233,7 +236,10 @@
         Selection oldSelection = getSelection(new Selection());
         mSelection.clear();
 
-        for (String id: oldSelection.getAll()) {
+        for (String id: oldSelection.mSelection) {
+            notifyItemStateChanged(id, false);
+        }
+        for (String id: oldSelection.mProvisionalSelection) {
             notifyItemStateChanged(id, false);
         }
     }
@@ -600,7 +606,7 @@
      * Object representing the current selection. Provides read only access
      * public access, and private write access.
      */
-    public static final class Selection implements Parcelable {
+    public static final class Selection implements Iterable<String>, Parcelable {
 
         // This class tracks selected items by managing two sets: the saved selection, and the total
         // selection. Saved selections are those which have been completed by tapping an item or by
@@ -640,19 +646,18 @@
         }
 
         /**
-         * Returns an unordered array of selected positions, including any
-         * provisional selection currently in effect.
+         * Returns an {@link Iterator} that iterators over the selection, *excluding*
+         * any provisional selection.
+         *
+         * {@inheritDoc}
          */
-        public List<String> getAll() {
-            ArrayList<String> selection =
-                    new ArrayList<String>(mSelection.size() + mProvisionalSelection.size());
-            selection.addAll(mSelection);
-            selection.addAll(mProvisionalSelection);
-            return selection;
+        @Override
+        public Iterator<String> iterator() {
+            return mSelection.iterator();
         }
 
         /**
-         * @return size of the selection.
+         * @return size of the selection including both final and provisional selected items.
          */
         public int size() {
             return mSelection.size() + mProvisionalSelection.size();
@@ -833,6 +838,7 @@
             return 0;
         }
 
+        @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeString(mDirectoryKey);
             dest.writeStringList(new ArrayList<>(mSelection));
diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
index 3a86a51..b54c9bb 100644
--- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
+++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java
@@ -30,14 +30,16 @@
 import com.android.documentsui.DocumentsApplication;
 import com.android.documentsui.RootCursorWrapper;
 
-import libcore.io.IoUtils;
-
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.net.ProtocolException;
+import java.util.Arrays;
 import java.util.Objects;
+import java.util.Set;
+
+import libcore.io.IoUtils;
 
 /**
  * Representation of a {@link Document}.
@@ -263,10 +265,12 @@
         return (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0;
     }
 
+    @Override
     public int hashCode() {
         return derivedUri.hashCode() + mimeType.hashCode();
     }
 
+    @Override
     public boolean equals(Object o) {
         if (o == null) {
             return false;
@@ -323,4 +327,21 @@
         fnfe.initCause(t);
         throw fnfe;
     }
+
+    public static Uri getUri(Cursor cursor) {
+        return DocumentsContract.buildDocumentUri(
+            getCursorString(cursor, RootCursorWrapper.COLUMN_AUTHORITY),
+            getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
+    }
+
+    public static void addMimeTypes(ContentResolver resolver, Uri uri, Set<String> mimeTypes) {
+        assert(uri != null);
+        if ("content".equals(uri.getScheme())) {
+            mimeTypes.add(resolver.getType(uri));
+            final String[] streamTypes = resolver.getStreamTypes(uri, "*/*");
+            if (streamTypes != null) {
+                mimeTypes.addAll(Arrays.asList(streamTypes));
+            }
+        }
+    }
 }
diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java
new file mode 100644
index 0000000..986ec79
--- /dev/null
+++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ClipStorageTest.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.documentsui;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.net.Uri;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.documentsui.ClipStorage.Writer;
+import com.android.documentsui.dirlist.TestModel;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ClipStorageTest {
+    private static final List<Uri> TEST_URIS = createList(
+            "content://ham/fancy",
+            "content://poodle/monkey/giraffe");
+
+    @Rule
+    public TemporaryFolder folder = new TemporaryFolder();
+
+    private ClipStorage mStorage;
+    private TestModel mModel;
+
+    @Before
+    public void setUp() {
+        File clipDir = ClipStorage.prepareStorage(folder.getRoot());
+        mStorage = new ClipStorage(clipDir);
+    }
+
+    @Test
+    public void testWritePrimary() throws Exception {
+        Writer writer = mStorage.createWriter();
+        writeAll(TEST_URIS, writer);
+    }
+
+    @Test
+    public void testRead() throws Exception {
+        Writer writer = mStorage.createWriter();
+        writeAll(TEST_URIS, writer);
+        long tag = mStorage.savePrimary();
+        List<Uri> uris = mStorage.read(tag);
+        assertEquals(TEST_URIS, uris);
+    }
+
+    @Test
+    public void testDelete() throws Exception {
+        Writer writer = mStorage.createWriter();
+        writeAll(TEST_URIS, writer);
+        long tag = mStorage.savePrimary();
+        mStorage.delete(tag);
+        try {
+            mStorage.read(tag);
+        } catch (IOException expected) {}
+    }
+
+    @Test
+    public void testPrepareStorage_CreatesDir() throws Exception {
+        File clipDir = ClipStorage.prepareStorage(folder.getRoot());
+        assertTrue(clipDir.exists());
+        assertTrue(clipDir.isDirectory());
+        assertFalse(clipDir.equals(folder.getRoot()));
+    }
+
+    @Test
+    public void testPrepareStorage_DeletesPreviousClipFiles() throws Exception {
+        File clipDir = ClipStorage.prepareStorage(folder.getRoot());
+        new File(clipDir, "somefakefile.poodles").createNewFile();
+        new File(clipDir, "yodles.yam").createNewFile();
+
+        assertEquals(2, clipDir.listFiles().length);
+        clipDir = ClipStorage.prepareStorage(folder.getRoot());
+        assertEquals(0, clipDir.listFiles().length);
+    }
+
+    private static void writeAll(List<Uri> uris, Writer writer) throws IOException {
+        for (Uri uri : uris) {
+            writer.write(uri);
+        }
+    }
+
+    private static List<Uri> createList(String... values) {
+        List<Uri> uris = new ArrayList<>(values.length);
+        for (int i = 0; i < values.length; i++) {
+            uris.add(i, Uri.parse(values[i]));
+        }
+        return uris;
+    }
+}