Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4795963e authored by Stephen Hughes's avatar Stephen Hughes Committed by Android (Google) Code Review
Browse files

Merge "Fix recursive copy bug Test: Added test to FileCopyUiTest"

parents ff4915e3 496b6ad8
Loading
Loading
Loading
Loading
+76 −2
Original line number Diff line number Diff line
@@ -19,12 +19,15 @@ package com.android.documentsui.services;
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;
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.SystemClock;
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 @@ class CopyJob extends ResolvedResourcesJob {

            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 @@ class CopyJob extends ResolvedResourcesJob {
     * 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 @@ class CopyJob extends ResolvedResourcesJob {
        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()
+88 −5
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import static com.android.documentsui.base.Providers.ROOT_ID_DEVICE;

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.filters.HugeLongTest;
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 @@ public class FileCopyUiTest extends ActivityTest<FilesActivity> {

    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 class FileCopyUiTest extends ActivityTest<FilesActivity> {
    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 @@ public class FileCopyUiTest extends ActivityTest<FilesActivity> {
        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 @@ public class FileCopyUiTest extends ActivityTest<FilesActivity> {
        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 @@ public class FileCopyUiTest extends ActivityTest<FilesActivity> {

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