From 0b4a3c47c657e3b713f0f9b0f5b6170ec8c48dfa Mon Sep 17 00:00:00 2001 From: Ben Kwa Date: Tue, 5 May 2015 11:50:11 -0700 Subject: [PATCH] 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 --- .../DocumentsUI/res/menu/mode_directory.xml | 4 + packages/DocumentsUI/res/values/strings.xml | 6 + .../com/android/documentsui/BaseActivity.java | 18 +- .../com/android/documentsui/CopyService.java | 59 +++-- .../documentsui/DirectoryFragment.java | 20 +- .../documentsui/DocumentsActivity.java | 3 + .../documentsui/FailureDialogFragment.java | 29 ++- .../documentsui/StandaloneActivity.java | 14 +- .../src/com/android/documentsui/CopyTest.java | 229 ++++++++++++++++-- .../com/android/documentsui/StubProvider.java | 17 +- 10 files changed, 324 insertions(+), 75 deletions(-) diff --git a/packages/DocumentsUI/res/menu/mode_directory.xml b/packages/DocumentsUI/res/menu/mode_directory.xml index 4b898234294a7..09d3a93ad0dbe 100644 --- a/packages/DocumentsUI/res/menu/mode_directory.xml +++ b/packages/DocumentsUI/res/menu/mode_directory.xml @@ -37,4 +37,8 @@ android:id="@+id/menu_copy" android:title="@string/menu_copy" android:showAsAction="never" /> + diff --git a/packages/DocumentsUI/res/values/strings.xml b/packages/DocumentsUI/res/values/strings.xml index 5281087770134..28e3b40fbe2da 100644 --- a/packages/DocumentsUI/res/values/strings.xml +++ b/packages/DocumentsUI/res/values/strings.xml @@ -50,6 +50,8 @@ Select All Copy to\u2026 + + Move to\u2026 Show internal storage @@ -124,6 +126,10 @@ Copying %1$d file. Copying %1$d files. + + Moving %1$d file. + Moving %1$d files. + Preparing for copy\u2026 diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java index cb21131e08611..b6b2ab8ef003a 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java @@ -267,14 +267,16 @@ abstract class BaseActivity extends Activity { /** 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/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java index 202402f007b44..1c97b85a23e89 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java +++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java @@ -61,6 +61,11 @@ public class CopyService extends IntentService { 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 @@ public class CopyService extends IntentService { * @param srcDocs A list of src files to copy. * @param dstStack The copy destination stack. */ - public static void start(Context context, List srcDocs, DocumentStack dstStack) { + public static void start(Context context, List 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(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 @@ public class CopyService extends IntentService { final ArrayList 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 @@ public class CopyService extends IntentService { 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 @@ public class CopyService extends IntentService { .setAutoCancel(true); mNotificationManager.notify(mJobId, 0, errorBuilder.build()); } - - // TODO: Display a toast if the copy was cancelled. } } @@ -377,7 +386,8 @@ public class CopyService extends IntentService { * @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 @@ public class CopyService extends IntentService { } 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 @@ public class CopyService extends IntentService { * @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 @@ public class CopyService extends IntentService { 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 @@ public class CopyService extends IntentService { * @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 @@ public class CopyService extends IntentService { 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 @@ public class CopyService extends IntentService { 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/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java index 7d737ca3b24ef..05dd16ce7bba9 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DirectoryFragment.java @@ -28,6 +28,7 @@ import static com.android.documentsui.DocumentsActivity.TAG; 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.Bundle; 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 @@ public class DirectoryFragment extends Fragment { } 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { 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 @@ public class DirectoryFragment extends Fragment { } } - private void onCopyDocuments(List docs) { + private void onTransferDocuments(List 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 @@ public class DirectoryFragment extends Fragment { } } intent.putExtra(BaseActivity.DocumentsIntent.EXTRA_DIRECTORY_COPY, directoryCopy); + intent.putExtra(CopyService.EXTRA_TRANSFER_MODE, mode); startActivityForResult(intent, REQUEST_COPY_DESTINATION); } @@ -1220,7 +1230,7 @@ public class DirectoryFragment extends Fragment { tmpStack = curStack; } - CopyService.start(getActivity(), srcDocs, tmpStack); + CopyService.start(getActivity(), srcDocs, tmpStack, CopyService.TRANSFER_MODE_COPY); } private List getDocumentsFromClipData(ClipData clipData) { diff --git a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java index e58c6373d98b8..b00f89c47858d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/DocumentsActivity.java @@ -242,6 +242,8 @@ public class DocumentsActivity extends BaseActivity { 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 @@ public class DocumentsActivity extends BaseActivity { | 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/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java index 00b0f78c0fab9..8a480faada8a6 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java +++ b/packages/DocumentsUI/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 @@ public class FailureDialogFragment extends DialogFragment private static final String TAG = "FailureDialogFragment"; private int mFailure; + private int mTransferMode; private ArrayList mFailedSrcList; public static void show(FragmentManager fm, int failure, - ArrayList failedSrcList, DocumentStack dstStack) { + ArrayList failedSrcList, DocumentStack dstStack, int transferMode) { // TODO: Add support for other failures than copy. if (failure != CopyService.FAILURE_COPY) { return; @@ -54,6 +50,7 @@ public class FailureDialogFragment extends DialogFragment 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 @@ public class FailureDialogFragment extends DialogFragment @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 @@ public class FailureDialogFragment extends DialogFragment 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("

"); @@ -89,9 +88,9 @@ public class FailureDialogFragment extends DialogFragment 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/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java index 1f629736a7635..8b8a217c12292 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/StandaloneActivity.java @@ -19,6 +19,7 @@ package com.android.documentsui; 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.ContentResolver; 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.util.Log; 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 @@ public class StandaloneActivity extends BaseActivity { mDirectoryContainer = (DirectoryContainerView) findViewById(R.id.container_directory); mState = (icicle != null) - ? icicle.getParcelable(EXTRA_STATE) - : buildDefaultState(); + ? icicle. getParcelable(EXTRA_STATE) + : buildDefaultState(); mToolbar = (Toolbar) findViewById(R.id.toolbar); mToolbar.setTitleTextAppearance(context, @@ -111,10 +109,13 @@ public class StandaloneActivity extends BaseActivity { 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 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 @@ public class StandaloneActivity extends BaseActivity { } } + @Override public void onDocumentsPicked(List docs) { // TODO } diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java index b1c84ddbd4a3c..568e9e4039604 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java @@ -83,7 +83,7 @@ public class CopyTest extends ServiceTestCase { // 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 @@ public class CopyTest extends ServiceTestCase { 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 @@ public class CopyTest extends ServiceTestCase { 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 @@ public class CopyTest extends ServiceTestCase { 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 @@ public class CopyTest extends ServiceTestCase { 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 srcs) throws FileNotFoundException { + private Intent createCopyIntent(List srcs) throws FileNotFoundException { final ArrayList srcDocs = Lists.newArrayList(); for (Uri src : srcs) { srcDocs.add(DocumentInfo.fromUri(mResolver, src)); @@ -255,7 +427,8 @@ public class CopyTest extends ServiceTestCase { copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs); copyIntent.putExtra(CopyService.EXTRA_STACK, (Parcelable) stack); - startService(copyIntent); + // startService(copyIntent); + return copyIntent; } /** @@ -275,24 +448,34 @@ public class CopyTest extends ServiceTestCase { 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/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java index 8cef433791b51..c2f176221f4b2 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java @@ -72,7 +72,7 @@ public class StubProvider extends DocumentsProvider { private String mAuthority; private SharedPreferences mPrefs; private Map mRoots; - private boolean mSimulateReadErrors; + private String mSimulateReadErrors; @Override public void attachInfo(Context context, ProviderInfo info) { @@ -176,6 +176,7 @@ public class StubProvider extends DocumentsProvider { } 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 @@ public class StubProvider extends DocumentsProvider { 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 @@ public class StubProvider extends DocumentsProvider { 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 @@ public class StubProvider extends DocumentsProvider { } @VisibleForTesting - public void simulateReadErrors(boolean b) { - mSimulateReadErrors = b; + public void simulateReadErrorsForFile(Uri uri) { + mSimulateReadErrors = DocumentsContract.getDocumentId(uri); } @Override @@ -284,6 +287,7 @@ public class StubProvider extends DocumentsProvider { 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 @@ public class StubProvider extends DocumentsProvider { } 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 @@ public class StubProvider extends DocumentsProvider { @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 @@ public class StubProvider extends DocumentsProvider { 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)) {