diff options
| -rw-r--r-- | core/java/android/provider/DocumentsContract.java | 28 | ||||
| -rw-r--r-- | core/java/android/provider/DocumentsProvider.java | 170 | ||||
| -rw-r--r-- | packages/DocumentsUI/src/com/android/documentsui/CopyService.java | 81 | ||||
| -rw-r--r-- | packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java | 2 | ||||
| -rw-r--r-- | packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java | 26 | ||||
| -rw-r--r-- | packages/DocumentsUI/tests/src/com/android/documentsui/CopyServiceTest.java (renamed from packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java) | 132 | ||||
| -rw-r--r-- | packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java | 10 | ||||
| -rw-r--r-- | packages/DocumentsUI/tests/src/com/android/documentsui/model/DocumentInfoTest.java | 56 |
8 files changed, 373 insertions, 132 deletions
diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 8ae899f4d453..d53bb0d2aa18 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -67,7 +67,7 @@ import java.util.List; * @see DocumentsProvider */ public final class DocumentsContract { - private static final String TAG = "Documents"; + private static final String TAG = "DocumentsContract"; // content://com.example/root/ // content://com.example/root/sdcard/ @@ -591,6 +591,12 @@ public final class DocumentsContract { */ public static final String EXTRA_ERROR = "error"; + /** + * Optional result (I'm thinking boolean) answer to a question. + * {@hide} + */ + public static final String EXTRA_RESULT = "result"; + /** {@hide} */ public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; /** {@hide} */ @@ -601,6 +607,8 @@ public final class DocumentsContract { public static final String METHOD_COPY_DOCUMENT = "android:copyDocument"; /** {@hide} */ public static final String METHOD_MOVE_DOCUMENT = "android:moveDocument"; + /** {@hide} */ + public static final String METHOD_IS_CHILD_DOCUMENT = "android:isChildDocument"; /** {@hide} */ public static final String EXTRA_URI = "uri"; @@ -1025,6 +1033,24 @@ public final class DocumentsContract { return out.getParcelable(DocumentsContract.EXTRA_URI); } + /** {@hide} */ + public static boolean isChildDocument(ContentProviderClient client, Uri parentDocumentUri, + Uri childDocumentUri) throws RemoteException { + + final Bundle in = new Bundle(); + in.putParcelable(DocumentsContract.EXTRA_URI, parentDocumentUri); + in.putParcelable(DocumentsContract.EXTRA_TARGET_URI, childDocumentUri); + + final Bundle out = client.call(METHOD_IS_CHILD_DOCUMENT, null, in); + if (out == null) { + throw new RemoteException("Failed to get a reponse from isChildDocument query."); + } + if (!out.containsKey(DocumentsContract.EXTRA_RESULT)) { + throw new RemoteException("Response did not include result field.."); + } + return out.getBoolean(DocumentsContract.EXTRA_RESULT); + } + /** * Change the display name of an existing document. * <p> diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index f01073bbd43d..e25ba35c8bb6 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -16,11 +16,12 @@ package android.provider; +import static android.provider.DocumentsContract.METHOD_COPY_DOCUMENT; import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; -import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; -import static android.provider.DocumentsContract.METHOD_COPY_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_IS_CHILD_DOCUMENT; import static android.provider.DocumentsContract.METHOD_MOVE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; import static android.provider.DocumentsContract.buildDocumentUri; import static android.provider.DocumentsContract.buildDocumentUriMaybeUsingTree; import static android.provider.DocumentsContract.buildTreeDocumentUri; @@ -688,6 +689,16 @@ public abstract class DocumentsProvider extends ContentProvider { return super.call(method, arg, extras); } + try { + return callUnchecked(method, arg, extras); + } catch (FileNotFoundException e) { + throw new IllegalStateException("Failed call " + method, e); + } + } + + private Bundle callUnchecked(String method, String arg, Bundle extras) + throws FileNotFoundException { + final Context context = getContext(); final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); final String authority = documentUri.getAuthority(); @@ -697,109 +708,120 @@ public abstract class DocumentsProvider extends ContentProvider { throw new SecurityException( "Requested authority " + authority + " doesn't match provider " + mAuthority); } - enforceTree(documentUri); final Bundle out = new Bundle(); - try { - if (METHOD_CREATE_DOCUMENT.equals(method)) { - enforceWritePermissionInner(documentUri, getCallingPackage(), null); - final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); - final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); - final String newDocumentId = createDocument(documentId, mimeType, displayName); + // If the URI is a tree URI performs some validation. + enforceTree(documentUri); - // No need to issue new grants here, since caller either has - // manage permission or a prefix grant. We might generate a - // tree style URI if that's how they called us. - final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, - newDocumentId); - out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + if (METHOD_IS_CHILD_DOCUMENT.equals(method)) { + enforceReadPermissionInner(documentUri, getCallingPackage(), null); - } else if (METHOD_RENAME_DOCUMENT.equals(method)) { - enforceWritePermissionInner(documentUri, getCallingPackage(), null); + final Uri childUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); + final String childAuthority = childUri.getAuthority(); + final String childId = DocumentsContract.getDocumentId(childUri); - final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); - final String newDocumentId = renameDocument(documentId, displayName); + out.putBoolean( + DocumentsContract.EXTRA_RESULT, + mAuthority.equals(childAuthority) + && isChildDocument(documentId, childId)); - if (newDocumentId != null) { - final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, - newDocumentId); + } else if (METHOD_CREATE_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri, getCallingPackage(), null); - // If caller came in with a narrow grant, issue them a - // narrow grant for the newly renamed document. - if (!isTreeUri(newDocumentUri)) { - final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, - documentUri); - context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); - } + final String mimeType = extras.getString(Document.COLUMN_MIME_TYPE); + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + final String newDocumentId = createDocument(documentId, mimeType, displayName); + + // No need to issue new grants here, since caller either has + // manage permission or a prefix grant. We might generate a + // tree style URI if that's how they called us. + final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, + newDocumentId); + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + + } else if (METHOD_RENAME_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri, getCallingPackage(), null); - out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + final String newDocumentId = renameDocument(documentId, displayName); + + if (newDocumentId != null) { + final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, + newDocumentId); - // Original document no longer exists, clean up any grants - revokeDocumentPermission(documentId); + // If caller came in with a narrow grant, issue them a + // narrow grant for the newly renamed document. + if (!isTreeUri(newDocumentUri)) { + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, + documentUri); + context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } - } else if (METHOD_DELETE_DOCUMENT.equals(method)) { - enforceWritePermissionInner(documentUri, getCallingPackage(), null); - deleteDocument(documentId); + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); - // Document no longer exists, clean up any grants + // Original document no longer exists, clean up any grants revokeDocumentPermission(documentId); + } - } else if (METHOD_COPY_DOCUMENT.equals(method)) { - final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); - final String targetId = DocumentsContract.getDocumentId(targetUri); + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri, getCallingPackage(), null); + deleteDocument(documentId); - enforceReadPermissionInner(documentUri, getCallingPackage(), null); - enforceWritePermissionInner(targetUri, getCallingPackage(), null); + // Document no longer exists, clean up any grants + revokeDocumentPermission(documentId); - final String newDocumentId = copyDocument(documentId, targetId); + } else if (METHOD_COPY_DOCUMENT.equals(method)) { + final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); + final String targetId = DocumentsContract.getDocumentId(targetUri); - if (newDocumentId != null) { - final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, - newDocumentId); + enforceReadPermissionInner(documentUri, getCallingPackage(), null); + enforceWritePermissionInner(targetUri, getCallingPackage(), null); - if (!isTreeUri(newDocumentUri)) { - final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, - documentUri); - context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); - } + final String newDocumentId = copyDocument(documentId, targetId); + + if (newDocumentId != null) { + final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, + newDocumentId); - out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + if (!isTreeUri(newDocumentUri)) { + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, + documentUri); + context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } - } else if (METHOD_MOVE_DOCUMENT.equals(method)) { - final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); - final String targetId = DocumentsContract.getDocumentId(targetUri); + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + } - enforceReadPermissionInner(documentUri, getCallingPackage(), null); - enforceWritePermissionInner(targetUri, getCallingPackage(), null); + } else if (METHOD_MOVE_DOCUMENT.equals(method)) { + final Uri targetUri = extras.getParcelable(DocumentsContract.EXTRA_TARGET_URI); + final String targetId = DocumentsContract.getDocumentId(targetUri); - final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); - final String newDocumentId = moveDocument(documentId, targetId); + enforceReadPermissionInner(documentUri, getCallingPackage(), null); + enforceWritePermissionInner(targetUri, getCallingPackage(), null); - if (newDocumentId != null) { - final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, - newDocumentId); + final String newDocumentId = moveDocument(documentId, targetId); - if (!isTreeUri(newDocumentUri)) { - final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, - documentUri); - context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); - } + if (newDocumentId != null) { + final Uri newDocumentUri = buildDocumentUriMaybeUsingTree(documentUri, + newDocumentId); - out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + if (!isTreeUri(newDocumentUri)) { + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, + documentUri); + context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); } - // Original document no longer exists, clean up any grants - revokeDocumentPermission(documentId); - - } else { - throw new UnsupportedOperationException("Method not supported " + method); + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); } - } catch (FileNotFoundException e) { - throw new IllegalStateException("Failed call " + method, e); + + // Original document no longer exists, clean up any grants + revokeDocumentPermission(documentId); + + } else { + throw new UnsupportedOperationException("Method not supported " + method); } + return out; } diff --git a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java index 55e2f441a882..b99c8063843c 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/CopyService.java +++ b/packages/DocumentsUI/src/com/android/documentsui/CopyService.java @@ -39,13 +39,15 @@ import android.os.RemoteException; import android.os.SystemClock; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import android.support.design.widget.Snackbar; import android.text.format.DateUtils; import android.util.Log; -import android.widget.Toast; import com.android.documentsui.model.DocumentInfo; import com.android.documentsui.model.DocumentStack; +import com.android.documentsui.model.RootInfo; import libcore.io.IoUtils; @@ -72,6 +74,12 @@ public class CopyService extends IntentService { // TODO: Move it to a shared file when more operations are implemented. public static final int FAILURE_COPY = 1; + // Parameters of the copy job. Requests to an IntentService are serialized so this code only + // needs to deal with one job at a time. + // NOTE: This must be declared by concrete type as the concrete type + // is required by putParcelableArrayListExtra. + private final ArrayList<DocumentInfo> mFailedFiles = new ArrayList<>(); + private PowerManager mPowerManager; private NotificationManager mNotificationManager; @@ -80,9 +88,6 @@ public class CopyService extends IntentService { // Jobs are serialized but a job ID is used, to avoid mixing up cancellation requests. private String mJobId; private volatile boolean mIsCancelled; - // Parameters of the copy job. Requests to an IntentService are serialized so this code only - // needs to deal with one job at a time. - private final ArrayList<DocumentInfo> mFailedFiles; private long mBatchSize; private long mBytesCopied; private long mStartTime; @@ -97,10 +102,11 @@ public class CopyService extends IntentService { private ContentProviderClient mSrcClient; private ContentProviderClient mDstClient; + // For testing only. + @Nullable private TestOnlyListener mJobFinishedListener; + public CopyService() { super("CopyService"); - - mFailedFiles = new ArrayList<DocumentInfo>(); } /** @@ -115,7 +121,11 @@ public class CopyService extends IntentService { final Resources res = activity.getResources(); final Intent copyIntent = new Intent(activity, CopyService.class); copyIntent.putParcelableArrayListExtra( - EXTRA_SRC_LIST, new ArrayList<DocumentInfo>(srcDocs)); + EXTRA_SRC_LIST, + // Don't create a copy unless absolutely necessary :) + srcDocs instanceof ArrayList + ? (ArrayList<DocumentInfo>) srcDocs + : new ArrayList<DocumentInfo>(srcDocs)); copyIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) dstStack); copyIntent.putExtra(EXTRA_TRANSFER_MODE, mode); @@ -198,6 +208,11 @@ public class CopyService extends IntentService { .setAutoCancel(true); mNotificationManager.notify(mJobId, 0, errorBuilder.build()); } + + if (mJobFinishedListener != null) { + mJobFinishedListener.onFinished(mFailedFiles); + } + if (DEBUG) Log.d(TAG, "Done cleaning up"); } } @@ -269,6 +284,26 @@ public class CopyService extends IntentService { } /** + * Sets a callback to be run when the next run job is finished. + * This is test ONLY instrumentation. The alternative is for us to add + * broadcast intents SOLELY for the purpose of testing. + * @param listener + */ + @VisibleForTesting + void addFinishedListener(TestOnlyListener listener) { + this.mJobFinishedListener = listener; + + } + + /** + * Only used for testing. Is that obvious enough? + */ + @VisibleForTesting + interface TestOnlyListener { + void onFinished(List<DocumentInfo> failed); + } + + /** * Calculates the cumulative size of all the documents in the list. Directories are recursed * into and totaled up. * @@ -279,7 +314,7 @@ public class CopyService extends IntentService { private long calculateFileSizes(List<DocumentInfo> srcs) throws RemoteException { long result = 0; for (DocumentInfo src : srcs) { - if (Document.MIME_TYPE_DIR.equals(src.mimeType)) { + if (src.isDirectory()) { // Directories need to be recursed into. result += calculateFileSizesHelper(src.derivedUri); } else { @@ -412,8 +447,21 @@ public class CopyService extends IntentService { */ private void copy(DocumentInfo srcInfo, DocumentInfo dstDirInfo, int mode) throws RemoteException { - if (DEBUG) Log.d(TAG, "Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")" + - " to " + dstDirInfo.displayName + " (" + dstDirInfo.derivedUri + ")"); + + String opDesc = mode == TRANSFER_MODE_COPY ? "copy" : "move"; + + // Guard unsupported recursive operation. + if (dstDirInfo.equals(srcInfo) || isDescendentOf(srcInfo, dstDirInfo)) { + if (DEBUG) Log.d(TAG, + "Skipping recursive " + opDesc + " of directory " + dstDirInfo.derivedUri); + mFailedFiles.add(srcInfo); + return; + } + + if (DEBUG) Log.d(TAG, + "Performing " + opDesc + " of " + srcInfo.displayName + + " (" + srcInfo.derivedUri + ")" + " to " + dstDirInfo.displayName + + " (" + dstDirInfo.derivedUri + ")"); // When copying within the same provider, try to use optimized copying and moving. // If not supported, then fallback to byte-by-byte copy/move. @@ -450,7 +498,7 @@ public class CopyService extends IntentService { return; } - if (Document.MIME_TYPE_DIR.equals(srcInfo.mimeType)) { + if (srcInfo.isDirectory()) { copyDirectoryHelper(srcInfo.derivedUri, dstUri, mode); } else { copyFileHelper(srcInfo.derivedUri, dstUri, mode); @@ -458,6 +506,17 @@ public class CopyService extends IntentService { } /** + * Returns true if {@code doc} is a descendant of {@code parentDoc}. + */ + boolean isDescendentOf(DocumentInfo doc, DocumentInfo parentDoc) throws RemoteException { + if (parentDoc.isDirectory() && doc.authority.equals(parentDoc.authority)) { + return DocumentsContract.isChildDocument( + mDstClient, doc.derivedUri, parentDoc.derivedUri); + } + return false; + } + + /** * Handles recursion into a directory and copying its contents. Note that in linux terms, this * does the equivalent of "cp src/* dst", not "cp -r src dst". * diff --git a/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java index 120f6106a7a0..23074f072c99 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/FailureDialogFragment.java @@ -37,7 +37,6 @@ public class FailureDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { private static final String TAG = "FailureDialogFragment"; - private int mFailure; private int mTransferMode; private ArrayList<DocumentInfo> mFailedSrcList; @@ -75,7 +74,6 @@ public class FailureDialogFragment extends DialogFragment public Dialog onCreateDialog(Bundle inState) { super.onCreate(inState); - mFailure = getArguments().getInt(CopyService.EXTRA_FAILURE); mTransferMode = getArguments().getInt(CopyService.EXTRA_TRANSFER_MODE); mFailedSrcList = getArguments().getParcelableArrayList(CopyService.EXTRA_SRC_LIST); diff --git a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java index cc981e1eaaad..dfdc705a16c8 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java +++ b/packages/DocumentsUI/src/com/android/documentsui/model/DocumentInfo.java @@ -25,6 +25,7 @@ import android.os.Parcelable; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsProvider; +import android.support.annotation.VisibleForTesting; import android.text.TextUtils; import com.android.documentsui.DocumentsApplication; @@ -204,13 +205,18 @@ public class DocumentInfo implements Durable, Parcelable { } } - private void deriveFields() { + @VisibleForTesting + void deriveFields() { derivedUri = DocumentsContract.buildDocumentUri(authority, documentId); } @Override public String toString() { - return "Document{docId=" + documentId + ", name=" + displayName + "}"; + return "Document{" + + "docId=" + documentId + + ", name=" + displayName + + ", isDirectory=" + isDirectory() + + "}"; } public boolean isCreateSupported() { @@ -237,6 +243,22 @@ public class DocumentInfo implements Durable, Parcelable { return (flags & Document.FLAG_DIR_HIDE_GRID_TITLES) != 0; } + public int hashCode() { + return derivedUri.hashCode() + mimeType.hashCode(); + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof DocumentInfo)) { + return false; + } + + DocumentInfo that = (DocumentInfo) other; + // Uri + mime type should be totally unique. + return derivedUri.equals(that.derivedUri) && mimeType.equals(that.mimeType); + } + public static String getCursorString(Cursor cursor, String columnName) { final int index = cursor.getColumnIndex(columnName); return (index != -1) ? cursor.getString(index) : null; diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/CopyServiceTest.java index 369ab7dc59ea..079d59914de6 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/CopyTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/CopyServiceTest.java @@ -28,12 +28,10 @@ import android.net.Uri; import android.os.Parcelable; import android.os.RemoteException; import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; import android.test.MoreAsserts; import android.test.ServiceTestCase; import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.MediumTest; -import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import com.android.documentsui.model.DocumentInfo; @@ -48,6 +46,7 @@ import libcore.io.Streams; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -55,9 +54,9 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @MediumTest -public class CopyTest extends ServiceTestCase<CopyService> { +public class CopyServiceTest extends ServiceTestCase<CopyService> { - public CopyTest() { + public CopyServiceTest() { super(CopyService.class); } @@ -72,11 +71,13 @@ public class CopyTest extends ServiceTestCase<CopyService> { private DocumentsProviderHelper mDocHelper; private StubProvider mStorage; private Context mSystemContext; + private CopyJobListener mListener; @Override protected void setUp() throws Exception { super.setUp(); + mListener = new CopyJobListener(); setupTestContext(); mClient = mResolver.acquireContentProviderClient(AUTHORITY); @@ -84,6 +85,8 @@ public class CopyTest extends ServiceTestCase<CopyService> { mStorage.clearCacheAndBuildRoots(); mDocHelper = new DocumentsProviderHelper(AUTHORITY, mClient); + + assertDestFileCount(0); } @Override @@ -97,15 +100,13 @@ public class CopyTest extends ServiceTestCase<CopyService> { Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); - assertDstFileCountEquals(0); - startService(createCopyIntent(Lists.newArrayList(testFile))); // 2 operations: file creation, then writing data. mResolver.waitForChanges(2); // Verify that one file was copied; check file contents. - assertDstFileCountEquals(1); + assertDestFileCount(1); assertCopied(srcPath); } @@ -114,8 +115,6 @@ public class CopyTest extends ServiceTestCase<CopyService> { String testContent = "The five boxing wizards jump quickly"; Uri testFile = mStorage.createFile(SRC_ROOT, 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); @@ -124,7 +123,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { mResolver.waitForChanges(3); // Verify that one file was moved; check file contents. - assertDstFileCountEquals(1); + assertDestFileCount(1); assertDoesNotExist(SRC_ROOT, srcPath); byte[] dstContent = readFile(DST_ROOT, srcPath); @@ -147,15 +146,13 @@ public class CopyTest extends ServiceTestCase<CopyService> { mStorage.createFile(SRC_ROOT, srcPaths[1], "text/plain", testContent[1].getBytes()), mStorage.createFile(SRC_ROOT, srcPaths[2], "text/plain", testContent[2].getBytes())); - assertDstFileCountEquals(0); - // Copy all the test files. startService(createCopyIntent(testFiles)); // 3 file creations, 3 file writes. mResolver.waitForChanges(6); - assertDstFileCountEquals(3); + assertDestFileCount(3); for (String path : srcPaths) { assertCopied(path); } @@ -163,29 +160,54 @@ public class CopyTest extends ServiceTestCase<CopyService> { public void testCopyEmptyDir() throws Exception { String srcPath = "/emptyDir"; - Uri testDir = mStorage.createFile(SRC_ROOT, srcPath, DocumentsContract.Document.MIME_TYPE_DIR, - null); - - assertDstFileCountEquals(0); + Uri testDir = createTestDirectory(srcPath); startService(createCopyIntent(Lists.newArrayList(testDir))); // Just 1 operation: Directory creation. mResolver.waitForChanges(1); - assertDstFileCountEquals(1); + assertDestFileCount(1); // Verify that the dst exists and is a directory. File dst = mStorage.getFile(DST_ROOT, srcPath); assertTrue(dst.isDirectory()); } + public void testNoCopyDirToSelf() throws Exception { + Uri testDir = createTestDirectory("/someDir"); + + Intent intent = createCopyIntent(Lists.newArrayList(testDir), testDir); + startService(intent); + + getService().addFinishedListener(mListener); + + mListener.waitForFinished(); + mListener.assertFailedCount(1); + mListener.assertFileFailed("someDir"); + + assertDestFileCount(0); + } + + public void testNoCopyDirToDescendent() throws Exception { + Uri testDir = createTestDirectory("/someDir"); + Uri descDir = createTestDirectory("/someDir/theDescendent"); + + Intent intent = createCopyIntent(Lists.newArrayList(testDir), descDir); + startService(intent); + + getService().addFinishedListener(mListener); + + mListener.waitForFinished(); + mListener.assertFailedCount(1); + mListener.assertFileFailed("someDir"); + + assertDestFileCount(0); + } + public void testMoveEmptyDir() throws Exception { String srcPath = "/emptyDir"; - Uri testDir = mStorage.createFile(SRC_ROOT, srcPath, DocumentsContract.Document.MIME_TYPE_DIR, - null); - - assertDstFileCountEquals(0); + Uri testDir = createTestDirectory(srcPath); Intent moveIntent = createCopyIntent(Lists.newArrayList(testDir)); moveIntent.putExtra(CopyService.EXTRA_TRANSFER_MODE, CopyService.TRANSFER_MODE_MOVE); @@ -194,7 +216,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { // 2 operations: Directory creation, and removal of the original. mResolver.waitForChanges(2); - assertDstFileCountEquals(1); + assertDestFileCount(1); // Verify that the dst exists and is a directory. File dst = mStorage.getFile(DST_ROOT, srcPath); @@ -217,8 +239,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { srcDir + "/test2.txt" }; // Create test dir; put some files in it. - Uri testDir = mStorage.createFile(SRC_ROOT, srcDir, DocumentsContract.Document.MIME_TYPE_DIR, - null); + Uri testDir = createTestDirectory(srcDir); mStorage.createFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes()); mStorage.createFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes()); mStorage.createFile(SRC_ROOT, srcFiles[2], "text/plain", testContent[2].getBytes()); @@ -252,8 +273,6 @@ public class CopyTest extends ServiceTestCase<CopyService> { Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); - assertDstFileCountEquals(0); - mStorage.simulateReadErrorsForFile(testFile); startService(createCopyIntent(Lists.newArrayList(testFile))); @@ -262,7 +281,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { mResolver.waitForChanges(3); // Verify that the failed copy was cleaned up. - assertDstFileCountEquals(0); + assertDestFileCount(0); } public void testMoveFileWithReadErrors() throws Exception { @@ -270,8 +289,6 @@ public class CopyTest extends ServiceTestCase<CopyService> { Uri testFile = mStorage.createFile(SRC_ROOT, srcPath, "text/plain", "The five boxing wizards jump quickly".getBytes()); - assertDstFileCountEquals(0); - mStorage.simulateReadErrorsForFile(testFile); Intent moveIntent = createCopyIntent(Lists.newArrayList(testFile)); @@ -288,7 +305,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { return; } finally { // Verify that the failed copy was cleaned up, and the src file wasn't removed. - assertDstFileCountEquals(0); + assertDestFileCount(0); assertExists(SRC_ROOT, srcPath); } // The asserts above didn't fail, but the CopyService did something unexpected. @@ -308,8 +325,7 @@ public class CopyTest extends ServiceTestCase<CopyService> { srcDir + "/test2.txt" }; // Create test dir; put some files in it. - Uri testDir = mStorage.createFile(SRC_ROOT, srcDir, DocumentsContract.Document.MIME_TYPE_DIR, - null); + Uri testDir = createTestDirectory(srcDir); mStorage.createFile(SRC_ROOT, srcFiles[0], "text/plain", testContent[0].getBytes()); Uri errFile = mStorage .createFile(SRC_ROOT, srcFiles[1], "text/plain", testContent[1].getBytes()); @@ -346,33 +362,37 @@ public class CopyTest extends ServiceTestCase<CopyService> { assertExists(SRC_ROOT, srcFiles[1]); } - /** - * Copies the given files to a pre-determined destination. - * - * @throws FileNotFoundException - */ + private Uri createTestDirectory(String dir) throws IOException { + return mStorage.createFile( + SRC_ROOT, dir, DocumentsContract.Document.MIME_TYPE_DIR, null); + } + private Intent createCopyIntent(List<Uri> srcs) throws Exception { + RootInfo root = mDocHelper.getRoot(DST_ROOT); + final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, root.documentId); + + return createCopyIntent(srcs, dst); + } + + private Intent createCopyIntent(List<Uri> srcs, Uri dst) throws Exception { final ArrayList<DocumentInfo> srcDocs = Lists.newArrayList(); for (Uri src : srcs) { srcDocs.add(DocumentInfo.fromUri(mResolver, src)); } - RootInfo root = mDocHelper.getRoot(DST_ROOT); - final Uri dst = DocumentsContract.buildDocumentUri(AUTHORITY, root.documentId); DocumentStack stack = new DocumentStack(); stack.push(DocumentInfo.fromUri(mResolver, dst)); final Intent copyIntent = new Intent(mContext, CopyService.class); copyIntent.putParcelableArrayListExtra(CopyService.EXTRA_SRC_LIST, srcDocs); copyIntent.putExtra(Shared.EXTRA_STACK, (Parcelable) stack); - // startService(copyIntent); return copyIntent; } /** * Returns a count of the files in the given directory. */ - private void assertDstFileCountEquals(int expected) throws RemoteException { + private void assertDestFileCount(int expected) throws RemoteException { RootInfo dest = mDocHelper.getRoot(DST_ROOT); final Uri queryUri = DocumentsContract.buildChildDocumentsUri(AUTHORITY, dest.documentId); @@ -449,6 +469,34 @@ public class CopyTest extends ServiceTestCase<CopyService> { mResolver.addProvider(AUTHORITY, mStorage); } + private final class CopyJobListener implements CopyService.TestOnlyListener { + + final CountDownLatch latch = new CountDownLatch(1); + final List<DocumentInfo> failedDocs = new ArrayList<>(); + @Override + public void onFinished(List<DocumentInfo> failed) { + failedDocs.addAll(failed); + latch.countDown(); + } + + public void assertFileFailed(String expectedName) { + for (DocumentInfo failed : failedDocs) { + if (expectedName.equals(failed.displayName)) { + return; + } + } + fail("Couldn't find failed file: " + expectedName); + } + + public void waitForFinished() throws InterruptedException { + latch.await(500, TimeUnit.MILLISECONDS); + } + + public void assertFailedCount(int expected) { + assertEquals(expected, failedDocs.size()); + } + } + /** * A test resolver that enables this test suite to listen for notifications that mark when copy * operations are done. diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java index 2d42ddc4e6b5..d23cdeb18269 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/StubProvider.java @@ -531,6 +531,16 @@ public class StubProvider extends DocumentsProvider { this.rootInfo = rootInfo; mStorage.put(this.documentId, this); } + @Override + public String toString() { + return "StubDocument{" + + "path:" + file.getPath() + + ", mimeType:" + mimeType + + ", rootInfo:" + rootInfo + + ", documentId:" + documentId + + ", parentId:" + parentId + + "}"; + } } private static String getDocumentIdForFile(File file) { diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/model/DocumentInfoTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/model/DocumentInfoTest.java new file mode 100644 index 000000000000..a6aba7b06816 --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/model/DocumentInfoTest.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.model; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +@SmallTest +public class DocumentInfoTest extends AndroidTestCase { + + public void testEquals() throws Exception { + DocumentInfo doc = createDocInfo("authority.a", "doc.1", "text/plain"); + assertEquals(doc, doc); + } + + public void testNotEquals_differentAuthority() throws Exception { + DocumentInfo docA = createDocInfo("authority.a", "doc.1", "text/plain"); + DocumentInfo docB = createDocInfo("authority.b", "doc.1", "text/plain"); + assertFalse(docA.equals(docB)); + } + + public void testNotEquals_differentDocId() throws Exception { + DocumentInfo docA = createDocInfo("authority.a", "doc.1", "text/plain"); + DocumentInfo docB = createDocInfo("authority.a", "doc.2", "text/plain"); + assertFalse(docA.equals(docB)); + } + + public void testNotEquals_differentMimetype() throws Exception { + DocumentInfo docA = createDocInfo("authority.a", "doc.1", "text/plain"); + DocumentInfo docB = createDocInfo("authority.a", "doc.1", "image/png"); + assertFalse(docA.equals(docB)); + } + + private DocumentInfo createDocInfo(String authority, String docId, String mimeType) { + DocumentInfo doc = new DocumentInfo(); + doc.authority = authority; + doc.documentId = docId; + doc.mimeType = mimeType; + doc.deriveFields(); + return doc; + } +} |