Merge "Add a move feature to DocumentsUI."

This commit is contained in:
Ben Kwa
2015-05-15 16:10:28 +00:00
committed by Android (Google) Code Review
10 changed files with 321 additions and 72 deletions

View File

@@ -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>

View File

@@ -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] -->

View File

@@ -269,14 +269,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();

View File

@@ -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<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 @@ public class CopyService extends IntentService {
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 @@ 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;
}
}
}
}

View File

@@ -369,7 +369,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
@@ -502,6 +503,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);
@@ -511,7 +513,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;
}
@@ -536,7 +538,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;
@@ -665,7 +672,7 @@ public class DirectoryFragment extends Fragment {
}
}
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.
@@ -683,6 +690,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);
}
@@ -1241,7 +1249,7 @@ public class DirectoryFragment extends Fragment {
tmpStack = curStack;
}
CopyService.start(getActivity(), srcDocs, tmpStack);
CopyService.start(getActivity(), srcDocs, tmpStack, CopyService.TRANSFER_MODE_COPY);
}
private List<DocumentInfo> getDocumentsFromClipData(ClipData clipData) {

View File

@@ -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;
@@ -704,6 +706,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

View File

@@ -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<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 @@ 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("<p>");
@@ -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();
}
}

View File

@@ -83,8 +83,8 @@ public class StandaloneActivity extends BaseActivity {
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 +111,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<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);
@@ -281,6 +284,7 @@ public class StandaloneActivity extends BaseActivity {
}
}
@Override
public void onDocumentsPicked(List<DocumentInfo> docs) {
// TODO
}

View File

@@ -83,7 +83,7 @@ public class CopyTest extends ServiceTestCase<CopyService> {
// 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<CopyService> {
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<CopyService> {
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<CopyService> {
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<CopyService> {
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 @@ public class CopyTest extends ServiceTestCase<CopyService> {
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<CopyService> {
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));
}
/**

View File

@@ -72,7 +72,7 @@ public class StubProvider extends DocumentsProvider {
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 @@ 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)) {