Make srcParent for move/delete nullable.
Add 3 unit tests to cover these cases.
Test: It stopped crashing. New tests pass.
Bug: 33540755
Change-Id: I0c80ff1b0aa25cc218b4a0538a11189d36d6826b
diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java
index d15f407..0efb19c 100644
--- a/src/com/android/documentsui/dirlist/DirectoryFragment.java
+++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java
@@ -736,10 +736,10 @@
throw new RuntimeException("Failed to create uri supplier.", e);
}
- Uri srcParent = mState.stack.peek().derivedUri;
+ final DocumentInfo parent = mState.stack.peek();
mLocalState.mPendingOperation = new FileOperation.Builder()
.withOpType(mode)
- .withSrcParent(srcParent)
+ .withSrcParent(parent == null ? null : parent.derivedUri)
.withSrcs(srcs)
.build();
diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java
index e365c4d..e0583a2 100644
--- a/src/com/android/documentsui/files/ActionHandler.java
+++ b/src/com/android/documentsui/files/ActionHandler.java
@@ -244,8 +244,7 @@
return;
}
- final DocumentInfo srcParent = mState.stack.peek();
- assert(srcParent != null);
+ final @Nullable DocumentInfo srcParent = mState.stack.peek();
// Model must be accessed in UI thread, since underlying cursor is not threadsafe.
List<DocumentInfo> docs = mScope.model.getDocuments(selection);
@@ -272,7 +271,7 @@
.withOpType(FileOperationService.OPERATION_DELETE)
.withDestination(mState.stack)
.withSrcs(srcs)
- .withSrcParent(srcParent.derivedUri)
+ .withSrcParent(srcParent == null ? null : srcParent.derivedUri)
.build();
FileOperations.start(mActivity, operation, mDialogs::showFileOperationStatus);
diff --git a/src/com/android/documentsui/services/DeleteJob.java b/src/com/android/documentsui/services/DeleteJob.java
index e77beae..5d32b70 100644
--- a/src/com/android/documentsui/services/DeleteJob.java
+++ b/src/com/android/documentsui/services/DeleteJob.java
@@ -32,6 +32,7 @@
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.DocumentStack;
+import javax.annotation.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@@ -40,9 +41,9 @@
private static final String TAG = "DeleteJob";
- private volatile int mDocsProcessed = 0;
+ private final Uri mSrcParent;
- Uri mSrcParent;
+ private volatile int mDocsProcessed = 0;
/**
* Moves files to a destination identified by {@code destination}.
* Performs most work by delegating to CopyJob, then deleting
@@ -50,8 +51,8 @@
*
* @see @link {@link Job} constructor for most param descriptions.
*/
- DeleteJob(Context service, Listener listener, String id, Uri srcParent, DocumentStack stack,
- UrisSupplier srcs) {
+ DeleteJob(Context service, Listener listener, String id, DocumentStack stack,
+ UrisSupplier srcs, @Nullable Uri srcParent) {
super(service, listener, id, OPERATION_DELETE, stack, srcs);
mSrcParent = srcParent;
}
@@ -100,7 +101,10 @@
final Iterable<Uri> uris = this.srcs.getUris(appContext);
final ContentResolver resolver = appContext.getContentResolver();
- final DocumentInfo srcParent = DocumentInfo.fromUri(resolver, mSrcParent);
+ final DocumentInfo srcParent =
+ mSrcParent != null
+ ? DocumentInfo.fromUri(resolver, mSrcParent)
+ : null;
for (Uri uri : uris) {
DocumentInfo doc = DocumentInfo.fromUri(resolver, uri);
srcs.add(doc);
diff --git a/src/com/android/documentsui/services/FileOperation.java b/src/com/android/documentsui/services/FileOperation.java
index 5e4c21e..b2e40ea 100644
--- a/src/com/android/documentsui/services/FileOperation.java
+++ b/src/com/android/documentsui/services/FileOperation.java
@@ -31,6 +31,8 @@
import com.android.documentsui.clipping.UrisSupplier;
import com.android.documentsui.services.FileOperationService.OpType;
+import javax.annotation.Nullable;
+
/**
* FileOperation describes a file operation, such as move/copy/delete etc.
*/
@@ -136,13 +138,12 @@
}
public static class MoveDeleteOperation extends FileOperation {
- private final Uri mSrcParent;
+ private final @Nullable Uri mSrcParent;
- private MoveDeleteOperation(
- @OpType int opType, UrisSupplier srcs, Uri srcParent, DocumentStack destination) {
+ private MoveDeleteOperation(@OpType int opType, UrisSupplier srcs,
+ DocumentStack destination, @Nullable Uri srcParent) {
super(opType, srcs, destination);
- assert(srcParent != null);
mSrcParent = srcParent;
}
@@ -151,10 +152,10 @@
switch(getOpType()) {
case OPERATION_MOVE:
return new MoveJob(
- service, listener, id, mSrcParent, getDestination(), getSrc());
+ service, listener, id, getDestination(), getSrc(), mSrcParent);
case OPERATION_DELETE:
return new DeleteJob(
- service, listener, id, mSrcParent, getDestination(), getSrc());
+ service, listener, id, getDestination(), getSrc(), mSrcParent);
default:
throw new UnsupportedOperationException("Unsupported op type: " + getOpType());
}
@@ -210,7 +211,7 @@
return this;
}
- public Builder withSrcParent(Uri srcParent) {
+ public Builder withSrcParent(@Nullable Uri srcParent) {
mSrcParent = srcParent;
return this;
}
@@ -231,7 +232,7 @@
return new CopyOperation(mSrcs, mDestination);
case OPERATION_MOVE:
case OPERATION_DELETE:
- return new MoveDeleteOperation(mOpType, mSrcs, mSrcParent, mDestination);
+ return new MoveDeleteOperation(mOpType, mSrcs, mDestination, mSrcParent);
default:
throw new UnsupportedOperationException("Unsupported op type: " + mOpType);
}
diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java
index 22f45ff..3cec4a4 100644
--- a/src/com/android/documentsui/services/Job.java
+++ b/src/com/android/documentsui/services/Job.java
@@ -50,6 +50,7 @@
import com.android.documentsui.base.Shared;
import com.android.documentsui.services.FileOperationService.OpType;
+import javax.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
@@ -225,9 +226,10 @@
return false;
}
- final void deleteDocument(DocumentInfo doc, DocumentInfo parent) throws ResourceException {
+ final void deleteDocument(DocumentInfo doc, @Nullable DocumentInfo parent)
+ throws ResourceException {
try {
- if (doc.isRemoveSupported()) {
+ if (parent != null && doc.isRemoveSupported()) {
DocumentsContract.removeDocument(getClient(doc), doc.derivedUri, parent.derivedUri);
} else if (doc.isDeleteSupported()) {
DocumentsContract.deleteDocument(getClient(doc), doc.derivedUri);
diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java
index 7e66cb0..a315579 100644
--- a/src/com/android/documentsui/services/MoveJob.java
+++ b/src/com/android/documentsui/services/MoveJob.java
@@ -34,6 +34,7 @@
import com.android.documentsui.base.DocumentStack;
import com.android.documentsui.clipping.UrisSupplier;
+import javax.annotation.Nullable;
import java.io.FileNotFoundException;
// TODO: Stop extending CopyJob.
@@ -41,8 +42,10 @@
private static final String TAG = "MoveJob";
- Uri mSrcParentUri;
- DocumentInfo mSrcParent;
+ private final @Nullable Uri mSrcParentUri;
+
+ // mSrcParent may be populated during setup.
+ private @Nullable DocumentInfo mSrcParent;
/**
* Moves files to a destination identified by {@code destination}.
@@ -52,7 +55,7 @@
* @see @link {@link Job} constructor for most param descriptions.
*/
MoveJob(Context service, Listener listener,
- String id, Uri srcParent, DocumentStack destination, UrisSupplier srcs) {
+ String id, DocumentStack destination, UrisSupplier srcs, @Nullable Uri srcParent) {
super(service, listener, id, OPERATION_MOVE, destination, srcs);
mSrcParentUri = srcParent;
}
@@ -84,13 +87,15 @@
@Override
public boolean setUp() {
- final ContentResolver resolver = appContext.getContentResolver();
- try {
- mSrcParent = DocumentInfo.fromUri(resolver, mSrcParentUri);
- } catch(FileNotFoundException e) {
- Log.e(TAG, "Failed to create srcParent.", e);
- failedFileCount += srcs.getItemCount();
- return false;
+ if (mSrcParentUri != null) {
+ final ContentResolver resolver = appContext.getContentResolver();
+ try {
+ mSrcParent = DocumentInfo.fromUri(resolver, mSrcParentUri);
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to create srcParent.", e);
+ failedFileCount += srcs.getItemCount();
+ return false;
+ }
}
return super.setUp();
@@ -134,7 +139,7 @@
// When moving within the same provider, try to use optimized moving.
// If not supported, then fallback to byte-by-byte copy/move.
- if (src.authority.equals(dest.authority)) {
+ if (src.authority.equals(dest.authority) && (srcParent != null || mSrcParent != null)) {
if ((src.flags & Document.FLAG_SUPPORTS_MOVE) != 0) {
try {
if (DocumentsContract.moveDocument(getClient(src), src.derivedUri,
diff --git a/tests/common/com/android/documentsui/testing/TestRootsAccess.java b/tests/common/com/android/documentsui/testing/TestRootsAccess.java
index 7a97ffb..45a30c7 100644
--- a/tests/common/com/android/documentsui/testing/TestRootsAccess.java
+++ b/tests/common/com/android/documentsui/testing/TestRootsAccess.java
@@ -15,6 +15,7 @@
*/
package com.android.documentsui.testing;
+import android.provider.DocumentsContract.Root;
import com.android.documentsui.base.Providers;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.State;
@@ -34,6 +35,7 @@
public static final RootInfo HOME;
public static final RootInfo HAMMY;
public static final RootInfo PICKLES;
+ public static final RootInfo RECENTS;
static {
DOWNLOADS = new RootInfo();
@@ -51,6 +53,13 @@
PICKLES = new RootInfo();
PICKLES.authority = "yummies";
PICKLES.rootId = "pickles";
+
+ RECENTS = new RootInfo() {{
+ // Special root for recents
+ derivedType = RootInfo.TYPE_RECENTS;
+ flags = Root.FLAG_LOCAL_ONLY | Root.FLAG_SUPPORTS_IS_CHILD;
+ availableBytes = -1;
+ }};
}
public final Map<String, Collection<RootInfo>> roots = new HashMap<>();
diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
index 013a391..4470b99 100644
--- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
+++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java
@@ -157,6 +157,17 @@
mActionModeAddons.finishOnConfirmed.assertRejected();
}
+ // Recents root means when deleting the srcParent will be null.
+ @Test
+ public void testDeleteSelectedDocuments_RecentsRoot() {
+ mEnv.state.stack.changeRoot(TestRootsAccess.RECENTS);
+
+ mHandler.deleteSelectedDocuments();
+ mDialogs.assertNoFileFailures();
+ mActivity.startService.assertCalled();
+ mActionModeAddons.finishOnConfirmed.assertCalled();
+ }
+
@Test
public void testShareSelectedDocuments_ShowsChooser() {
mActivity.resources.strings.put(R.string.share_via, "Sharezilla!");
diff --git a/tests/unit/com/android/documentsui/services/AbstractCopyJobTest.java b/tests/unit/com/android/documentsui/services/AbstractCopyJobTest.java
index 6b52fdd..83b2109 100644
--- a/tests/unit/com/android/documentsui/services/AbstractCopyJobTest.java
+++ b/tests/unit/com/android/documentsui/services/AbstractCopyJobTest.java
@@ -167,6 +167,10 @@
*/
final T createJob(List<Uri> srcs) throws Exception {
Uri srcParent = DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId);
+ return createJob(srcs, srcParent);
+ }
+
+ final T createJob(List<Uri> srcs, Uri srcParent) throws Exception {
Uri destination = DocumentsContract.buildDocumentUri(AUTHORITY, mDestRoot.documentId);
return createJob(mOpType, srcs, srcParent, destination);
}
diff --git a/tests/unit/com/android/documentsui/services/DeleteJobTest.java b/tests/unit/com/android/documentsui/services/DeleteJobTest.java
index 9dbe7ce..f97a360 100644
--- a/tests/unit/com/android/documentsui/services/DeleteJobTest.java
+++ b/tests/unit/com/android/documentsui/services/DeleteJobTest.java
@@ -43,6 +43,19 @@
mDocs.assertChildCount(mSrcRoot, 0);
}
+ public void testDeleteFiles_NoSrcParent() throws Exception {
+ Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
+ mDocs.writeDocument(testFile1, HAM_BYTES);
+
+ Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
+ mDocs.writeDocument(testFile2, FRUITY_BYTES);
+
+ createJob(newArrayList(testFile1, testFile2), null).run();
+ mJobListener.waitForFinished();
+
+ mDocs.assertChildCount(mSrcRoot, 0);
+ }
+
/**
* Creates a job with a stack consisting to the default src directory.
*/
diff --git a/tests/unit/com/android/documentsui/services/MoveJobTest.java b/tests/unit/com/android/documentsui/services/MoveJobTest.java
index 56d96cc..6bdd9ed 100644
--- a/tests/unit/com/android/documentsui/services/MoveJobTest.java
+++ b/tests/unit/com/android/documentsui/services/MoveJobTest.java
@@ -37,6 +37,23 @@
mDocs.assertChildCount(mSrcRoot, 0);
}
+ public void testMoveFiles_NoSrcParent() throws Exception {
+ Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt");
+ mDocs.writeDocument(testFile1, HAM_BYTES);
+
+ Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt");
+ mDocs.writeDocument(testFile2, FRUITY_BYTES);
+
+ createJob(newArrayList(testFile1, testFile2), null).run();
+ mJobListener.waitForFinished();
+
+ mDocs.assertChildCount(mDestRoot, 2);
+ mDocs.assertHasFile(mDestRoot, "test1.txt");
+ mDocs.assertHasFile(mDestRoot, "test2.txt");
+ mDocs.assertFileContents(mDestRoot.documentId, "test1.txt", HAM_BYTES);
+ mDocs.assertFileContents(mDestRoot.documentId, "test2.txt", FRUITY_BYTES);
+ }
+
public void testMoveVirtualTypedFile() throws Exception {
mDocs.createFolder(mSrcRoot, "hello");
Uri testFile = mDocs.createVirtualFile(