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)) {