Merge "Fix recursive copy bug Test: Added test to FileCopyUiTest"
diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java
index 196e970..63f591b 100644
--- a/src/com/android/documentsui/services/CopyJob.java
+++ b/src/com/android/documentsui/services/CopyJob.java
@@ -19,12 +19,15 @@
import static android.content.ContentResolver.wrap;
import static android.provider.DocumentsContract.buildChildDocumentsUri;
import static android.provider.DocumentsContract.buildDocumentUri;
+import static android.provider.DocumentsContract.findDocumentPath;
import static android.provider.DocumentsContract.getDocumentId;
import static android.provider.DocumentsContract.isChildDocument;
import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED;
import static com.android.documentsui.base.DocumentInfo.getCursorLong;
import static com.android.documentsui.base.DocumentInfo.getCursorString;
+import static com.android.documentsui.base.Providers.AUTHORITY_DOWNLOADS;
+import static com.android.documentsui.base.Providers.AUTHORITY_STORAGE;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.services.FileOperationService.EXTRA_DIALOG_TYPE;
import static com.android.documentsui.services.FileOperationService.EXTRA_FAILED_DOCS;
@@ -37,6 +40,7 @@
import android.app.Notification.Builder;
import android.app.PendingIntent;
import android.content.ContentProviderClient;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
@@ -55,10 +59,12 @@
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Path;
import android.system.ErrnoException;
import android.system.Int64Ref;
import android.system.Os;
import android.system.OsConstants;
+import android.system.StructStat;
import android.util.ArrayMap;
import android.util.Log;
import android.webkit.MimeTypeMap;
@@ -226,7 +232,9 @@
try {
// Copying recursively to itself or one of descendants is not allowed.
- if (mDstInfo.equals(srcInfo) || isDescendentOf(srcInfo, mDstInfo)) {
+ if (mDstInfo.equals(srcInfo)
+ || isDescendantOf(srcInfo, mDstInfo)
+ || isRecursiveCopy(srcInfo, mDstInfo)) {
Log.e(TAG, "Skipping recursive copy of " + srcInfo.derivedUri);
onFileFailed(srcInfo);
} else {
@@ -808,7 +816,7 @@
* Returns true if {@code doc} is a descendant of {@code parentDoc}.
* @throws ResourceException
*/
- boolean isDescendentOf(DocumentInfo doc, DocumentInfo parent)
+ boolean isDescendantOf(DocumentInfo doc, DocumentInfo parent)
throws ResourceException {
if (parent.isDirectory() && doc.authority.equals(parent.authority)) {
try {
@@ -822,6 +830,72 @@
return false;
}
+
+ private boolean isRecursiveCopy(DocumentInfo source, DocumentInfo target) {
+ if (!source.isDirectory() || !target.isDirectory()) {
+ return false;
+ }
+
+ // Recursive copy within the same authority is prevented by a check to isDescendantOf.
+ if (source.authority.equals(target.authority)) {
+ return false;
+ }
+
+ if (!isFileSystemProvider(source) || !isFileSystemProvider(target)) {
+ return false;
+ }
+
+ Uri sourceUri = source.derivedUri;
+ Uri targetUri = target.derivedUri;
+
+ try {
+ final Path targetPath = findDocumentPath(wrap(getClient(target)), targetUri);
+ if (targetPath == null) {
+ return false;
+ }
+
+ ContentResolver cr = wrap(getClient(source));
+ try (ParcelFileDescriptor sourceFd = cr.openFile(sourceUri, "r", null)) {
+ StructStat sourceStat = Os.fstat(sourceFd.getFileDescriptor());
+ final long sourceDev = sourceStat.st_dev;
+ final long sourceIno = sourceStat.st_ino;
+ // Walk down the target hierarchy. If we ever match the source, we know we are a
+ // descendant of them and should abort the copy.
+ for (String targetNodeDocId : targetPath.getPath()) {
+ Uri targetNodeUri = buildDocumentUri(target.authority, targetNodeDocId);
+ cr = wrap(getClient(target));
+
+ try (ParcelFileDescriptor targetFd = cr.openFile(targetNodeUri, "r", null)) {
+ StructStat targetNodeStat = Os.fstat(targetFd.getFileDescriptor());
+ final long targetNodeDev = targetNodeStat.st_dev;
+ final long targetNodeIno = targetNodeStat.st_ino;
+
+ // Devices differ, just return early.
+ if (sourceDev != targetNodeDev) {
+ return false;
+ }
+
+ if (sourceIno == targetNodeIno) {
+ Log.w(TAG, String.format(
+ "Preventing copy from %s to %s", sourceUri, targetUri));
+ return true;
+ }
+
+ }
+ }
+ }
+ } catch (Throwable t) {
+ Log.w(TAG, String.format("Failed to determine if isRecursiveCopy" +
+ " for source %s and target %s", sourceUri, targetUri), t);
+ }
+ return false;
+ }
+
+ private static boolean isFileSystemProvider(DocumentInfo info) {
+ return AUTHORITY_STORAGE.equals(info.authority)
+ || AUTHORITY_DOWNLOADS.equals(info.authority);
+ }
+
@Override
public String toString() {
return new StringBuilder()
diff --git a/tests/functional/com/android/documentsui/FileCopyUiTest.java b/tests/functional/com/android/documentsui/FileCopyUiTest.java
index 3d18b8f..2113ef7 100644
--- a/tests/functional/com/android/documentsui/FileCopyUiTest.java
+++ b/tests/functional/com/android/documentsui/FileCopyUiTest.java
@@ -21,6 +21,7 @@
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -44,8 +45,10 @@
import com.android.documentsui.services.TestNotificationService;
import java.util.HashMap;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
@@ -67,6 +70,8 @@
private final Map<String, Long> mTargetFileList = new HashMap<String, Long>();
+ private final List<RootAndFolderPair> mFoldersToCleanup = new ArrayList<>();
+
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -109,6 +114,8 @@
public void setUp() throws Exception {
super.setUp();
+ mFoldersToCleanup.clear();
+
// Create ContentProviderClient and DocumentsProviderHelper for using SD Card.
ContentProviderClient storageClient =
mResolver.acquireUnstableContentProviderClient(AUTHORITY_STORAGE);
@@ -164,6 +171,10 @@
deleteDocuments(Build.MODEL);
deleteDocuments(mSdCardLabel);
+ for (RootAndFolderPair rootAndFolder : mFoldersToCleanup) {
+ deleteDocuments(rootAndFolder.root, rootAndFolder.folder);
+ }
+
if (mIsVirtualSdCard) {
device.executeShellCommand("sm set-virtual-disk false");
}
@@ -214,25 +225,29 @@
return true;
}
- private boolean deleteDocuments(String label) throws Exception {
+ private boolean deleteDocuments(String label, String targetFolder) throws Exception {
if (TextUtils.isEmpty(label)) {
return false;
}
bots.roots.openRoot(label);
- if (!bots.directory.hasDocuments(TARGET_FOLDER)) {
+ if (!bots.directory.hasDocuments(targetFolder)) {
return true;
}
- bots.directory.selectDocument(TARGET_FOLDER, 1);
+ bots.directory.selectDocument(targetFolder, 1);
device.waitForIdle();
bots.main.clickToolbarItem(R.id.action_menu_delete);
bots.main.clickDialogOkButton();
device.waitForIdle();
- bots.directory.findDocument(TARGET_FOLDER).waitUntilGone(WAIT_TIME_SECONDS);
- return !bots.directory.hasDocuments(TARGET_FOLDER);
+ bots.directory.findDocument(targetFolder).waitUntilGone(WAIT_TIME_SECONDS);
+ return !bots.directory.hasDocuments(targetFolder);
+ }
+
+ private boolean deleteDocuments(String label) throws Exception {
+ return deleteDocuments(label, TARGET_FOLDER);
}
private void loadImages(Uri root, DocumentsProviderHelper helper) throws Exception {
@@ -426,4 +441,72 @@
assertFalse(bots.directory.findDocument(fileName1).isEnabled());
}
+
+ @HugeLongTest
+ public void testRecursiveCopyDocuments_InternalStorageToDownloadsProvider() throws Exception {
+ // Create Download folder if it doesn't exist.
+ DocumentInfo info = mStorageDocsHelper.findFile(mPrimaryRoot.documentId, "Download");
+
+ if (info == null) {
+ ContentResolver cr = context.getContentResolver();
+ Uri uri = mStorageDocsHelper.createFolder(mPrimaryRoot.documentId, "Download");
+ info = DocumentInfo.fromUri(cr, uri);
+ }
+
+ assertTrue(info != null && info.isDirectory());
+
+ // Setup folder /storage/emulated/0/Download/UUID
+ String randomFolder = UUID.randomUUID().toString();
+ assertNull(mStorageDocsHelper.findFile(info.documentId, randomFolder));
+
+ Uri subFolderUri = mStorageDocsHelper.createFolder(info.documentId, randomFolder);
+ assertNotNull(subFolderUri);
+ mFoldersToCleanup.add(new RootAndFolderPair("Downloads", randomFolder));
+
+ // Load images into /storage/emulated/0/Download/UUID
+ loadImages(subFolderUri, mStorageDocsHelper);
+
+ mCountDownLatch = new CountDownLatch(1);
+
+ // Open Internal Storage Root.
+ bots.roots.openRoot(Build.MODEL);
+ device.waitForIdle();
+
+ // Select Download folder.
+ bots.directory.selectDocument("Download");
+ device.waitForIdle();
+
+ // Click copy button.
+ bots.main.clickToolbarOverflowItem(context.getResources().getString(R.string.menu_copy));
+ device.waitForIdle();
+
+ // Downloads folder is automatically opened, so just open the folder defined
+ // by the UUID.
+ bots.directory.openDocument(randomFolder);
+ device.waitForIdle();
+
+ // Initiate the copy operation.
+ bots.main.clickDialogOkButton();
+ device.waitForIdle();
+
+ try {
+ mCountDownLatch.await(WAIT_TIME_SECONDS, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ fail("Cannot wait because of error." + e.toString());
+ }
+
+ assertFalse(mOperationExecuted);
+ }
+
+ /** Holds a pair of a root and folder. */
+ private static final class RootAndFolderPair {
+
+ private final String root;
+ private final String folder;
+
+ RootAndFolderPair(String root, String folder) {
+ this.root = root;
+ this.folder = folder;
+ }
+ }
}