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;
+        }
+    }
 }