Add a move feature to DocumentsUI.

Add a menu item (protected behind a system property) for moving files.
Add an extra to the copy intent for transfer mode (copy/move).
Add code to CopyService to delete files after copy when in move mode.
Add tests.

BUG=20559838

Change-Id: I983f57a528327d1e7a12982b599094aad2c856ed
diff --git a/res/menu/mode_directory.xml b/res/menu/mode_directory.xml
index 4b89823..09d3a93 100644
--- a/res/menu/mode_directory.xml
+++ b/res/menu/mode_directory.xml
@@ -37,4 +37,8 @@
         android:id="@+id/menu_copy"
         android:title="@string/menu_copy"
         android:showAsAction="never" />
+    <item
+        android:id="@+id/menu_move"
+        android:title="@string/menu_move"
+        android:showAsAction="never" />
 </menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5281087..28e3b40 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -50,6 +50,8 @@
     <string name="menu_select_all">Select All</string>
     <!-- Menu item title that copies the selected documents [CHAR LIMIT=24] -->
     <string name="menu_copy">Copy to\u2026</string>
+    <!-- Menu item title that moves the selected documents [CHAR LIMIT=24] -->
+    <string name="menu_move">Move to\u2026</string>
 
     <!-- Menu item that reveals internal storage built into the device [CHAR LIMIT=24] -->
     <string name="menu_advanced_show" product="nosdcard">Show internal storage</string>
@@ -124,6 +126,10 @@
         <item quantity="one">Copying <xliff:g id="count" example="1">%1$d</xliff:g> file.</item>
         <item quantity="other">Copying <xliff:g id="count" example="3">%1$d</xliff:g> files.</item>
     </plurals>
+    <plurals name="move_begin">
+        <item quantity="one">Moving <xliff:g id="count" example="1">%1$d</xliff:g> file.</item>
+        <item quantity="other">Moving <xliff:g id="count" example="3">%1$d</xliff:g> files.</item>
+    </plurals>
     <!-- Text shown on the copy notification while DocumentsUI performs setup in preparation for copying files [CHAR LIMIT=32] -->
     <string name="copy_preparing">Preparing for copy\u2026</string>
     <!-- Title of the copy error notification [CHAR LIMIT=48] -->
diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java
index cb21131..b6b2ab8 100644
--- a/src/com/android/documentsui/BaseActivity.java
+++ b/src/com/android/documentsui/BaseActivity.java
@@ -267,14 +267,16 @@
         /** Derived after loader */
         public int derivedSortOrder = SORT_ORDER_DISPLAY_NAME;
 
-        public boolean allowMultiple = false;
-        public boolean showSize = false;
-        public boolean localOnly = false;
-        public boolean forceAdvanced = false;
-        public boolean showAdvanced = false;
-        public boolean stackTouched = false;
-        public boolean restored = false;
-        public boolean directoryCopy = false;
+        public boolean allowMultiple;
+        public boolean showSize;
+        public boolean localOnly ;
+        public boolean forceAdvanced ;
+        public boolean showAdvanced ;
+        public boolean stackTouched ;
+        public boolean restored ;
+        public boolean directoryCopy ;
+        /** Transfer mode for file copy/move operations. */
+        public int transferMode;
 
         /** Current user navigation stack; empty implies recents. */
         public DocumentStack stack = new DocumentStack();
diff --git a/src/com/android/documentsui/CopyService.java b/src/com/android/documentsui/CopyService.java
index 202402f..1c97b85 100644
--- a/src/com/android/documentsui/CopyService.java
+++ b/src/com/android/documentsui/CopyService.java
@@ -61,6 +61,11 @@
     public static final String EXTRA_SRC_LIST = "com.android.documentsui.SRC_LIST";
     public static final String EXTRA_STACK = "com.android.documentsui.STACK";
     public static final String EXTRA_FAILURE = "com.android.documentsui.FAILURE";
+    public static final String EXTRA_TRANSFER_MODE = "com.android.documentsui.TRANSFER_MODE";
+
+    public static final int TRANSFER_MODE_NONE = 0;
+    public static final int TRANSFER_MODE_COPY = 1;
+    public static final int TRANSFER_MODE_MOVE = 2;
 
     // TODO: Move it to a shared file when more operations are implemented.
     public static final int FAILURE_COPY = 1;
@@ -101,15 +106,19 @@
      * @param srcDocs A list of src files to copy.
      * @param dstStack The copy destination stack.
      */
-    public static void start(Context context, List<DocumentInfo> srcDocs, DocumentStack dstStack) {
+    public static void start(Context context, List<DocumentInfo> srcDocs, DocumentStack dstStack,
+            int mode) {
         final Resources res = context.getResources();
         final Intent copyIntent = new Intent(context, CopyService.class);
         copyIntent.putParcelableArrayListExtra(
                 EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(srcDocs));
         copyIntent.putExtra(EXTRA_STACK, (Parcelable) dstStack);
+        copyIntent.putExtra(EXTRA_TRANSFER_MODE, mode);
 
+        int toastMessage = (mode == TRANSFER_MODE_COPY) ? R.plurals.copy_begin
+                : R.plurals.move_begin;
         Toast.makeText(context,
-                res.getQuantityString(R.plurals.copy_begin, srcDocs.size(), srcDocs.size()),
+                res.getQuantityString(toastMessage, srcDocs.size(), srcDocs.size()),
                 Toast.LENGTH_SHORT).show();
         context.startService(copyIntent);
     }
@@ -131,6 +140,8 @@
 
         final ArrayList<DocumentInfo> srcs = intent.getParcelableArrayListExtra(EXTRA_SRC_LIST);
         final DocumentStack stack = intent.getParcelableExtra(EXTRA_STACK);
+        // Copy by default.
+        final int transferMode = intent.getIntExtra(EXTRA_TRANSFER_MODE, TRANSFER_MODE_COPY);
 
         try {
             // Acquire content providers.
@@ -142,7 +153,7 @@
             setupCopyJob(srcs, stack);
 
             for (int i = 0; i < srcs.size() && !mIsCancelled; ++i) {
-                copy(srcs.get(i), stack.peek());
+                copy(srcs.get(i), stack.peek(), transferMode);
             }
         } catch (Exception e) {
             // Catch-all to prevent any copy errors from wedging the app.
@@ -173,8 +184,6 @@
                         .setAutoCancel(true);
                 mNotificationManager.notify(mJobId, 0, errorBuilder.build());
             }
-
-            // TODO: Display a toast if the copy was cancelled.
         }
     }
 
@@ -377,7 +386,8 @@
      * @param dstDirInfo The destination directory.
      * @throws RemoteException
      */
-    private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo) throws RemoteException {
+    private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo, int mode)
+            throws RemoteException {
         final Uri dstUri = DocumentsContract.createDocument(mDstClient, dstDirInfo.derivedUri,
                 srcInfo.mimeType, srcInfo.displayName);
         if (dstUri == null) {
@@ -388,9 +398,9 @@
         }
 
         if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) {
-            copyDirectoryHelper(srcInfo.derivedUri, dstUri);
+            copyDirectoryHelper(srcInfo.derivedUri, dstUri, mode);
         } else {
-            copyFileHelper(srcInfo.derivedUri, dstUri);
+            copyFileHelper(srcInfo.derivedUri, dstUri, mode);
         }
     }
 
@@ -403,7 +413,8 @@
      * @param dstDirUri URI of the directory to copy to. Must be created beforehand.
      * @throws RemoteException
      */
-    private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri) throws RemoteException {
+    private void copyDirectoryHelper(Uri srcDirUri, Uri dstDirUri, int mode)
+            throws RemoteException {
         // Recurse into directories. Copy children into the new subdirectory.
         final String queryColumns[] = new String[] {
                 Document.COLUMN_DISPLAY_NAME,
@@ -424,9 +435,20 @@
                 final Uri childUri = DocumentsContract.buildDocumentUri(srcDirUri.getAuthority(),
                         getCursorString(cursor, Document.COLUMN_DOCUMENT_ID));
                 if (Document.MIME_TYPE_DIR.equals(childMimeType)) {
-                    copyDirectoryHelper(childUri, dstUri);
+                    copyDirectoryHelper(childUri, dstUri, mode);
                 } else {
-                    copyFileHelper(childUri, dstUri);
+                    copyFileHelper(childUri, dstUri, mode);
+                }
+            }
+            if (mode == TRANSFER_MODE_MOVE) {
+                try {
+                    DocumentsContract.deleteDocument(mSrcClient, srcDirUri);
+                } catch (RemoteException e) {
+                    // RemoteExceptions usually signal that the connection is dead, so there's no
+                    // point attempting to continue. Propagate the exception up so the copy job is
+                    // cancelled.
+                    Log.w(TAG, "Failed to clean up after move: " + srcDirUri, e);
+                    throw e;
                 }
             }
         } finally {
@@ -441,7 +463,8 @@
      * @param dstUri URI of the *file* to copy to. Must be created beforehand.
      * @throws RemoteException
      */
-    private void copyFileHelper(Uri srcUri, Uri dstUri) throws RemoteException {
+    private void copyFileHelper(Uri srcUri, Uri dstUri, int mode)
+            throws RemoteException {
         // Copy an individual file.
         CancellationSignal canceller = new CancellationSignal();
         ParcelFileDescriptor srcFile = null;
@@ -484,7 +507,7 @@
                 mFailedFiles.add(DocumentInfo.fromUri(getContentResolver(), srcUri));
             } catch (FileNotFoundException ignore) {
                 Log.w(TAG, "Source file gone: " + srcUri, copyError);
-              // The source file is gone.
+                // The source file is gone.
             }
         }
 
@@ -494,11 +517,19 @@
             try {
                 DocumentsContract.deleteDocument(mDstClient, dstUri);
             } catch (RemoteException e) {
-                Log.w(TAG, "Failed to clean up: " + srcUri, e);
+                Log.w(TAG, "Failed to clean up after copy error: " + dstUri, e);
                 // RemoteExceptions usually signal that the connection is dead, so there's no point
                 // attempting to continue. Propagate the exception up so the copy job is cancelled.
                 throw e;
             }
+        } else if (mode == TRANSFER_MODE_MOVE) {
+            // Clean up src files after a successful move.
+            try {
+                DocumentsContract.deleteDocument(mSrcClient, srcUri);
+            } catch (RemoteException e) {
+                Log.w(TAG, "Failed to clean up after move: " + srcUri, e);
+                throw e;
+            }
         }
     }
 }
diff --git a/src/com/android/documentsui/DirectoryFragment.java b/src/com/android/documentsui/DirectoryFragment.java
index 7d737ca..05dd16c 100644
--- a/src/com/android/documentsui/DirectoryFragment.java
+++ b/src/com/android/documentsui/DirectoryFragment.java
@@ -28,6 +28,7 @@
 import static com.android.documentsui.model.DocumentInfo.getCursorInt;
 import static com.android.documentsui.model.DocumentInfo.getCursorLong;
 import static com.android.documentsui.model.DocumentInfo.getCursorString;
+
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.Fragment;
@@ -53,6 +54,7 @@
 import android.os.CancellationSignal;
 import android.os.OperationCanceledException;
 import android.os.Parcelable;
+import android.os.SystemProperties;
 import android.provider.DocumentsContract;
 import android.provider.DocumentsContract.Document;
 import android.text.format.DateUtils;
@@ -355,7 +357,8 @@
         }
 
         CopyService.start(getActivity(), getDisplayState(this).selectedDocumentsForCopy,
-                (DocumentStack) data.getParcelableExtra(CopyService.EXTRA_STACK));
+                (DocumentStack) data.getParcelableExtra(CopyService.EXTRA_STACK),
+                data.getIntExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_NONE));
     }
 
     @Override
@@ -488,6 +491,7 @@
             final MenuItem share = menu.findItem(R.id.menu_share);
             final MenuItem delete = menu.findItem(R.id.menu_delete);
             final MenuItem copy = menu.findItem(R.id.menu_copy);
+            final MenuItem move = menu.findItem(R.id.menu_move);
 
             final boolean manageOrBrowse = (state.action == ACTION_MANAGE
                     || state.action == ACTION_BROWSE || state.action == ACTION_BROWSE_ALL);
@@ -497,7 +501,7 @@
             delete.setVisible(manageOrBrowse);
             // Disable copying from the Recents view.
             copy.setVisible(manageOrBrowse && mType != TYPE_RECENT_OPEN);
-
+            move.setVisible(SystemProperties.getBoolean("debug.documentsui.enable_move", false));
             return true;
         }
 
@@ -522,7 +526,12 @@
                 return true;
 
             } else if (id == R.id.menu_copy) {
-                onCopyDocuments(docs);
+                onTransferDocuments(docs, CopyService.TRANSFER_MODE_COPY);
+                mode.finish();
+                return true;
+
+            } else if (id == R.id.menu_move) {
+                onTransferDocuments(docs, CopyService.TRANSFER_MODE_MOVE);
                 mode.finish();
                 return true;
 
@@ -655,7 +664,7 @@
         }
     }
 
-    private void onCopyDocuments(List<DocumentInfo> docs) {
+    private void onTransferDocuments(List<DocumentInfo> docs, int mode) {
         getDisplayState(this).selectedDocumentsForCopy = docs;
 
         // Pop up a dialog to pick a destination.  This is inadequate but works for now.
@@ -673,6 +682,7 @@
             }
         }
         intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy);
+        intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode);
         startActivityForResult(intent, REQUEST_COPY_DESTINATION);
     }
 
@@ -1220,7 +1230,7 @@
             tmpStack = curStack;
         }
 
-        CopyService.start(getActivity(), srcDocs, tmpStack);
+        CopyService.start(getActivity(), srcDocs, tmpStack, CopyService.TRANSFER_MODE_COPY);
     }
 
     private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {
diff --git a/src/com/android/documentsui/DocumentsActivity.java b/src/com/android/documentsui/DocumentsActivity.java
index e58c637..b00f89c 100644
--- a/src/com/android/documentsui/DocumentsActivity.java
+++ b/src/com/android/documentsui/DocumentsActivity.java
@@ -242,6 +242,8 @@
         if (state.action == ACTION_OPEN_COPY_DESTINATION) {
             state.directoryCopy = intent.getBooleanExtra(
                     BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, false);
+            state.transferMode = intent.getIntExtra(CopyService.EXTRA_TRANSFER_MODE,
+                    CopyService.TRANSFER_MODE_NONE);
         }
 
         return state;
@@ -703,6 +705,7 @@
                     | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
             // TODO: Move passing the stack to the separate ACTION_COPY action once it's implemented.
             intent.putExtra(CopyService.EXTRA_STACK, (Parcelable)mState.stack);
+            intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mState.transferMode);
         } else {
             intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                     | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
diff --git a/src/com/android/documentsui/FailureDialogFragment.java b/src/com/android/documentsui/FailureDialogFragment.java
index 00b0f78..8a480fa 100644
--- a/src/com/android/documentsui/FailureDialogFragment.java
+++ b/src/com/android/documentsui/FailureDialogFragment.java
@@ -16,23 +16,18 @@
 
 package com.android.documentsui;
 
-import android.app.Activity;
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.DialogFragment;
-import android.app.Fragment;
 import android.app.FragmentManager;
 import android.app.FragmentTransaction;
 import android.content.DialogInterface;
-import android.net.Uri;
 import android.os.Bundle;
 import android.text.Html;
 
-import com.android.documentsui.CopyService;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
 
-import java.io.FileNotFoundException;
 import java.util.ArrayList;
 
 /**
@@ -43,10 +38,11 @@
     private static final String TAG = "FailureDialogFragment";
 
     private int mFailure;
+    private int mTransferMode;
     private ArrayList<DocumentInfo> mFailedSrcList;
 
     public static void show(FragmentManager fm, int failure,
-            ArrayList<DocumentInfo> failedSrcList, DocumentStack dstStack) {
+            ArrayList<DocumentInfo> failedSrcList, DocumentStack dstStack, int transferMode) {
         // TODO: Add support for other failures than copy.
         if (failure != CopyService.FAILURE_COPY) {
             return;
@@ -54,6 +50,7 @@
 
         final Bundle args = new Bundle();
         args.putInt(CopyService.EXTRA_FAILURE, failure);
+        args.putInt(CopyService.EXTRA_TRANSFER_MODE, transferMode);
         args.putParcelableArrayList(CopyService.EXTRA_SRC_LIST, failedSrcList);
 
         final FragmentTransaction ft = fm.beginTransaction();
@@ -66,11 +63,12 @@
 
     @Override
     public void onClick(DialogInterface dialog, int whichButton) {
-      if (whichButton == DialogInterface.BUTTON_POSITIVE) {
-          CopyService.start(getActivity(), mFailedSrcList,
-                  (DocumentStack) getActivity().getIntent().getParcelableExtra(
-                          CopyService.EXTRA_STACK));
-      }
+        if (whichButton == DialogInterface.BUTTON_POSITIVE) {
+            CopyService.start(getActivity(), mFailedSrcList,
+                    (DocumentStack) getActivity().getIntent().getParcelableExtra(
+                            CopyService.EXTRA_STACK),
+                            mTransferMode);
+        }
     }
 
     @Override
@@ -78,6 +76,7 @@
         super.onCreate(inState);
 
         mFailure = getArguments().getInt(CopyService.EXTRA_FAILURE);
+        mTransferMode = getArguments().getInt(CopyService.EXTRA_TRANSFER_MODE);
         mFailedSrcList = getArguments().getParcelableArrayList(CopyService.EXTRA_SRC_LIST);
 
         final StringBuilder list = new StringBuilder("<p>");
@@ -89,9 +88,9 @@
                 list.toString());
 
         return new AlertDialog.Builder(getActivity())
-            .setMessage(Html.fromHtml(message))
-            .setPositiveButton(R.string.retry, this)
-            .setNegativeButton(android.R.string.cancel, this)
-            .create();
+                .setMessage(Html.fromHtml(message))
+                .setPositiveButton(R.string.retry, this)
+                .setNegativeButton(android.R.string.cancel, this)
+                .create();
     }
 }
diff --git a/src/com/android/documentsui/StandaloneActivity.java b/src/com/android/documentsui/StandaloneActivity.java
index 1f62973..8b8a217 100644
--- a/src/com/android/documentsui/StandaloneActivity.java
+++ b/src/com/android/documentsui/StandaloneActivity.java
@@ -19,6 +19,7 @@
 import static com.android.documentsui.DirectoryFragment.ANIM_DOWN;
 import static com.android.documentsui.DirectoryFragment.ANIM_NONE;
 import static com.android.documentsui.DirectoryFragment.ANIM_UP;
+
 import android.app.Activity;
 import android.app.FragmentManager;
 import android.content.ActivityNotFoundException;
@@ -27,7 +28,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.graphics.Point;
 import android.net.Uri;
 import android.os.Bundle;
 import android.provider.DocumentsContract;
@@ -36,13 +36,11 @@
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.WindowManager;
 import android.widget.BaseAdapter;
 import android.widget.Spinner;
 import android.widget.Toast;
 import android.widget.Toolbar;
 
-import com.android.documentsui.FailureDialogFragment;
 import com.android.documentsui.RecentsProvider.ResumeColumns;
 import com.android.documentsui.model.DocumentInfo;
 import com.android.documentsui.model.DocumentStack;
@@ -83,8 +81,8 @@
         mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory);
 
         mState = (icicle != null)
-            ? icicle.<State>getParcelable(EXTRA_STATE)
-            : buildDefaultState();
+                ? icicle.<State> getParcelable(EXTRA_STATE)
+                : buildDefaultState();
 
         mToolbar = (Toolbar) findViewById(R.id.toolbar);
         mToolbar.setTitleTextAppearance(context,
@@ -111,10 +109,13 @@
             final Intent intent = getIntent();
             final DocumentStack dstStack = intent.getParcelableExtra(CopyService.EXTRA_STACK);
             final int failure = intent.getIntExtra(CopyService.EXTRA_FAILURE, 0);
+            final int transferMode = intent.getIntExtra(CopyService.EXTRA_TRANSFER_MODE,
+                    CopyService.TRANSFER_MODE_NONE);
             if (failure != 0) {
                 final ArrayList<DocumentInfo> failedSrcList =
                         intent.getParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST);
-                FailureDialogFragment.show(getFragmentManager(), failure, failedSrcList, dstStack);
+                FailureDialogFragment.show(getFragmentManager(), failure, failedSrcList, dstStack,
+                        transferMode);
             }
         } else {
             onCurrentDirectoryChanged(ANIM_NONE);
@@ -276,6 +277,7 @@
         }
     }
 
+    @Override
     public void onDocumentsPicked(List<DocumentInfo> docs) {
         // TODO
     }
diff --git a/tests/src/com/android/documentsui/CopyTest.java b/tests/src/com/android/documentsui/CopyTest.java
index b1c84dd..568e9e4 100644
--- a/tests/src/com/android/documentsui/CopyTest.java
+++ b/tests/src/com/android/documentsui/CopyTest.java
@@ -83,7 +83,7 @@
             // Signal that the test is now waiting for files.
             mReadySignal.countDown();
             if (!mNotificationSignal.await(timeOut, TimeUnit.MILLISECONDS)) {
-                throw new TimeoutException("Timed out waiting for files to be copied.");
+                throw new TimeoutException("Timed out waiting for file operations to complete.");
             }
         }
 
@@ -159,7 +159,7 @@
 
         assertDstFileCountEquals(0);
 
-        copyToDestination(Lists.newArrayList(testFile));
+        startService(createCopyIntent(Lists.newArrayList(testFile)));
 
         // 2 operations: file creation, then writing data.
         mResolver.waitForChanges(2);
@@ -169,6 +169,28 @@
         assertCopied(srcPath);
     }
 
+    public void testMoveFile() throws Exception {
+        String srcPath = "/test0.txt";
+        String testContent = "The five boxing wizards jump quickly";
+        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain", testContent.getBytes());
+
+        assertDstFileCountEquals(0);
+
+        Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
+        moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
+        startService(moveIntent);
+
+        // 3 operations: file creation, writing data, deleting original.
+        mResolver.waitForChanges(3);
+
+        // Verify that one file was moved; check file contents.
+        assertDstFileCountEquals(1);
+        assertDoesNotExist(SRC, srcPath);
+
+        byte[] dstContent = readFile(DST, srcPath);
+        MoreAsserts.assertEquals("Moved file contents differ", testContent.getBytes(), dstContent);
+    }
+
     /**
      * Test copying multiple files.
      */
@@ -191,7 +213,7 @@
         assertDstFileCountEquals(0);
 
         // Copy all the test files.
-        copyToDestination(testFiles);
+        startService(createCopyIntent(testFiles));
 
         // 3 file creations, 3 file writes.
         mResolver.waitForChanges(6);
@@ -209,40 +231,190 @@
 
         assertDstFileCountEquals(0);
 
-        copyToDestination(Lists.newArrayList(testDir));
+        startService(createCopyIntent(Lists.newArrayList(testDir)));
 
         // Just 1 operation: Directory creation.
         mResolver.waitForChanges(1);
 
         assertDstFileCountEquals(1);
 
+        // Verify that the dst exists and is a directory.
         File dst = mStorage.getFile(DST, srcPath);
         assertTrue(dst.isDirectory());
     }
 
-    public void testReadErrors() throws Exception {
+    public void testMoveEmptyDir() throws Exception {
+        String srcPath = "/emptyDir";
+        Uri testDir = mStorage.createFile(SRC, srcPath, DocumentsContract.Document.MIME_TYPE_DIR,
+                null);
+
+        assertDstFileCountEquals(0);
+
+        Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
+        moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
+        startService(moveIntent);
+
+        // 2 operations: Directory creation, and removal of the original.
+        mResolver.waitForChanges(2);
+
+        assertDstFileCountEquals(1);
+
+        // Verify that the dst exists and is a directory.
+        File dst = mStorage.getFile(DST, srcPath);
+        assertTrue(dst.isDirectory());
+
+        // Verify that the src was cleaned up.
+        assertDoesNotExist(SRC, srcPath);
+    }
+
+    public void testMovePopulatedDir() throws Exception {
+        String testContent[] = {
+                "The five boxing wizards jump quickly",
+                "The quick brown fox jumps over the lazy dog",
+                "Jackdaws love my big sphinx of quartz"
+        };
+        String srcDir = "/testdir";
+        String srcFiles[] = {
+                srcDir + "/test0.txt",
+                srcDir + "/test1.txt",
+                srcDir + "/test2.txt"
+        };
+        // Create test dir; put some files in it.
+        Uri testDir = mStorage.createFile(SRC, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
+                null);
+        mStorage.createFile(SRC, srcFiles[0], "text/plain", testContent[0].getBytes());
+        mStorage.createFile(SRC, srcFiles[1], "text/plain", testContent[1].getBytes());
+        mStorage.createFile(SRC, srcFiles[2], "text/plain", testContent[2].getBytes());
+
+        Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
+        moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
+        startService(moveIntent);
+
+        // dir creation, then creation and writing of 3 files, then removal of src dir and 3 src
+        // files.
+        mResolver.waitForChanges(11);
+
+        // Check the content of the moved files.
+        File dst = mStorage.getFile(DST, srcDir);
+        assertTrue(dst.isDirectory());
+        for (int i = 0; i < testContent.length; ++i) {
+            byte[] dstContent = readFile(DST, srcFiles[i]);
+            MoreAsserts.assertEquals("Copied file contents differ", testContent[i].getBytes(),
+                    dstContent);
+        }
+
+        // Check that the src files were removed.
+        assertDoesNotExist(SRC, srcDir);
+        for (String srcFile : srcFiles) {
+            assertDoesNotExist(SRC, srcFile);
+        }
+    }
+
+    public void testCopyFileWithReadErrors() throws Exception {
         String srcPath = "/test0.txt";
         Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
                 "The five boxing wizards jump quickly".getBytes());
 
         assertDstFileCountEquals(0);
 
-        mStorage.simulateReadErrors(true);
+        mStorage.simulateReadErrorsForFile(testFile);
 
-        copyToDestination(Lists.newArrayList(testFile));
+        startService(createCopyIntent(Lists.newArrayList(testFile)));
 
         // 3 operations: file creation, writing, then deletion (due to failed copy).
         mResolver.waitForChanges(3);
 
+        // Verify that the failed copy was cleaned up.
         assertDstFileCountEquals(0);
     }
 
+    public void testMoveFileWithReadErrors() throws Exception {
+        String srcPath = "/test0.txt";
+        Uri testFile = mStorage.createFile(SRC, srcPath, "text/plain",
+                "The five boxing wizards jump quickly".getBytes());
+
+        assertDstFileCountEquals(0);
+
+        mStorage.simulateReadErrorsForFile(testFile);
+
+        Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile));
+        moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
+        startService(moveIntent);
+
+        try {
+            // There should be 3 operations: file creation, writing, then deletion (due to failed
+            // copy). Wait for 4, in case the CopyService also attempts to do extra stuff (like
+            // delete the src file). This should time out.
+            mResolver.waitForChanges(4);
+        } catch (TimeoutException e) {
+            // Success path
+            return;
+        } finally {
+            // Verify that the failed copy was cleaned up, and the src file wasn't removed.
+            assertDstFileCountEquals(0);
+            assertExists(SRC, srcPath);
+        }
+        // The asserts above didn't fail, but the CopyService did something unexpected.
+        fail("Extra file operations were detected");
+    }
+
+    public void testMoveDirectoryWithReadErrors() throws Exception {
+        String testContent[] = {
+                "The five boxing wizards jump quickly",
+                "The quick brown fox jumps over the lazy dog",
+                "Jackdaws love my big sphinx of quartz"
+        };
+        String srcDir = "/testdir";
+        String srcFiles[] = {
+                srcDir + "/test0.txt",
+                srcDir + "/test1.txt",
+                srcDir + "/test2.txt"
+        };
+        // Create test dir; put some files in it.
+        Uri testDir = mStorage.createFile(SRC, srcDir, DocumentsContract.Document.MIME_TYPE_DIR,
+                null);
+        mStorage.createFile(SRC, srcFiles[0], "text/plain", testContent[0].getBytes());
+        Uri errFile = mStorage
+                .createFile(SRC, srcFiles[1], "text/plain", testContent[1].getBytes());
+        mStorage.createFile(SRC, srcFiles[2], "text/plain", testContent[2].getBytes());
+
+        mStorage.simulateReadErrorsForFile(errFile);
+
+        Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir));
+        moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE);
+        startService(moveIntent);
+
+        // - dst dir creation,
+        // - creation and writing of 2 files, removal of 2 src files
+        // - creation and writing of 1 file, then removal of that file (due to error)
+        mResolver.waitForChanges(10);
+
+        // Check that both the src and dst dirs exist. The src dir shouldn't have been removed,
+        // because it should contain the one errFile.
+        assertTrue(mStorage.getFile(SRC, srcDir).isDirectory());
+        assertTrue(mStorage.getFile(DST, srcDir).isDirectory());
+
+        // Check the content of the moved files.
+        MoreAsserts.assertEquals("Copied file contents differ", testContent[0].getBytes(),
+                readFile(DST, srcFiles[0]));
+        MoreAsserts.assertEquals("Copied file contents differ", testContent[2].getBytes(),
+                readFile(DST, srcFiles[2]));
+
+        // Check that the src files were removed.
+        assertDoesNotExist(SRC, srcFiles[0]);
+        assertDoesNotExist(SRC, srcFiles[2]);
+
+        // Check that the error file was not copied over.
+        assertDoesNotExist(DST, srcFiles[1]);
+        assertExists(SRC, srcFiles[1]);
+    }
+
     /**
      * Copies the given files to a pre-determined destination.
      *
      * @throws FileNotFoundException
      */
-    private void copyToDestination(List<Uri> srcs) throws FileNotFoundException {
+    private Intent createCopyIntent(List<Uri> srcs) throws FileNotFoundException {
         final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList();
         for (Uri src : srcs) {
             srcDocs.add(DocumentInfo.fromUri(mResolver, src));
@@ -255,7 +427,8 @@
         copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs);
         copyIntent.putExtra(CopyService.EXTRA_STACK, (Parcelable) stack);
 
-        startService(copyIntent);
+        // startService(copyIntent);
+        return copyIntent;
     }
 
     /**
@@ -275,24 +448,34 @@
         assertEquals("Incorrect file count after copy", expected, count);
     }
 
-    private void assertCopied(String path) throws Exception {
-        File srcFile = mStorage.getFile(SRC, path);
-        File dstFile = mStorage.getFile(DST, path);
-        assertNotNull(dstFile);
+    private void assertExists(String rootId, String path) throws Exception {
+        assertNotNull("An expected file was not found: " + path + " on root " + rootId,
+                mStorage.getFile(rootId, path));
+    }
 
-        FileInputStream src = null;
-        FileInputStream dst = null;
+    private void assertDoesNotExist(String rootId, String path) throws Exception {
+        assertNull("Unexpected file found: " + path + " on root " + rootId,
+                mStorage.getFile(rootId, path));
+    }
+
+    private byte[] readFile(String rootId, String path) throws Exception {
+        File file = mStorage.getFile(rootId, path);
+        byte[] buf = null;
+        assertNotNull(file);
+
+        FileInputStream in = null;
         try {
-            src = new FileInputStream(srcFile);
-            dst = new FileInputStream(dstFile);
-            byte[] srcbuf = Streams.readFully(src);
-            byte[] dstbuf = Streams.readFully(dst);
-
-            MoreAsserts.assertEquals(srcbuf, dstbuf);
+            in = new FileInputStream(file);
+            buf = Streams.readFully(in);
         } finally {
-            IoUtils.closeQuietly(src);
-            IoUtils.closeQuietly(dst);
+            IoUtils.closeQuietly(in);
         }
+        return buf;
+    }
+
+    private void assertCopied(String path) throws Exception {
+        MoreAsserts.assertEquals("Copied file contents differ", readFile(SRC, path),
+                readFile(DST, path));
     }
 
     /**
diff --git a/tests/src/com/android/documentsui/StubProvider.java b/tests/src/com/android/documentsui/StubProvider.java
index 8cef433..c2f1762 100644
--- a/tests/src/com/android/documentsui/StubProvider.java
+++ b/tests/src/com/android/documentsui/StubProvider.java
@@ -72,7 +72,7 @@
     private String mAuthority;
     private SharedPreferences mPrefs;
     private Map<String, RootInfo> mRoots;
-    private boolean mSimulateReadErrors;
+    private String mSimulateReadErrors;
 
     @Override
     public void attachInfo(Context context, ProviderInfo info) {
@@ -176,6 +176,7 @@
         }
 
         final StubDocument document = new StubDocument(file, mimeType, parentDocument);
+        Log.d(TAG, "Created document " + document.documentId);
         notifyParentChanged(document.parentId);
         getContext().getContentResolver().notifyChange(
                 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
@@ -193,7 +194,9 @@
             throw new FileNotFoundException();
         synchronized (mWriteLock) {
             document.rootInfo.size -= fileSize;
+            mStorage.remove(documentId);
         }
+        Log.d(TAG, "Document deleted: " + documentId);
         notifyParentChanged(document.parentId);
         getContext().getContentResolver().notifyChange(
                 DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
@@ -239,7 +242,7 @@
         if ("r".equals(mode)) {
             ParcelFileDescriptor pfd = ParcelFileDescriptor.open(document.file,
                     ParcelFileDescriptor.MODE_READ_ONLY);
-            if (mSimulateReadErrors) {
+            if (docId.equals(mSimulateReadErrors)) {
                 pfd = new ParcelFileDescriptor(pfd) {
                     @Override
                     public void checkError() throws IOException {
@@ -257,8 +260,8 @@
     }
 
     @VisibleForTesting
-    public void simulateReadErrors(boolean b) {
-        mSimulateReadErrors = b;
+    public void simulateReadErrorsForFile(Uri uri) {
+        mSimulateReadErrors = DocumentsContract.getDocumentId(uri);
     }
 
     @Override
@@ -284,6 +287,7 @@
                 InputStream inputStream = null;
                 OutputStream outputStream = null;
                 try {
+                    Log.d(TAG, "Opening write stream on file " + document.documentId);
                     inputStream = new ParcelFileDescriptor.AutoCloseInputStream(readPipe);
                     outputStream = new FileOutputStream(document.file);
                     byte[] buffer = new byte[32 * 1024];
@@ -312,6 +316,7 @@
                 } finally {
                     IoUtils.closeQuietly(inputStream);
                     IoUtils.closeQuietly(outputStream);
+                    Log.d(TAG, "Closing write stream on file " + document.documentId);
                     notifyParentChanged(document.parentId);
                     getContext().getContentResolver().notifyChange(
                             DocumentsContract.buildDocumentUri(mAuthority, document.documentId),
@@ -408,6 +413,7 @@
     @VisibleForTesting
     public Uri createFile(String rootId, String path, String mimeType, byte[] content)
             throws FileNotFoundException, IOException {
+        Log.d(TAG, "Creating file " + rootId + ":" + path);
         StubDocument root = mRoots.get(rootId).rootDocument;
         if (root == null) {
             throw new FileNotFoundException("No roots with the ID " + rootId + " were found");
@@ -417,6 +423,9 @@
         if (parent == null) {
             parent = mStorage.get(createFile(rootId, file.getParentFile().getPath(),
                     DocumentsContract.Document.MIME_TYPE_DIR, null));
+            Log.d(TAG, "Created parent " + parent.documentId);
+        } else {
+            Log.d(TAG, "Found parent " + parent.documentId);
         }
 
         if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) {