Loading src/com/android/documentsui/services/CopyJob.java +76 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -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 { Loading @@ -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() Loading tests/functional/com/android/documentsui/FileCopyUiTest.java +88 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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); Loading Loading @@ -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"); } Loading Loading @@ -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 { Loading Loading @@ -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; } } } Loading
src/com/android/documentsui/services/CopyJob.java +76 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; Loading Loading @@ -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 { Loading Loading @@ -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 { Loading @@ -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() Loading
tests/functional/com/android/documentsui/FileCopyUiTest.java +88 −5 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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) { Loading Loading @@ -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); Loading Loading @@ -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"); } Loading Loading @@ -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 { Loading Loading @@ -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; } } }