From d6b233ac2c4ae40a0d15a8484730b273efb64658 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 22 Oct 2020 18:25:17 +0900 Subject: [PATCH] handle moving entries to source directory --- .../model/provider/MediaStoreImageProvider.kt | 97 +++++++++++-------- lib/model/source/collection_source.dart | 14 +-- .../selection_action_delegate.dart | 2 +- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 8425d9073..e58733b11 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -16,6 +16,7 @@ import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent +import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission import java.io.File @@ -265,51 +266,61 @@ class MediaStoreImageProvider : ImageProvider() { val future = SettableFuture.create() try { - val sourceFileName = File(sourcePath).name - val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") - - // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` - // but in order to open an output stream to it, we need to use a `SingleDocumentFile` - // through a document URI, not a tree URI - // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first - val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension) - val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) - - // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry - // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument" - // when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri` - val source = DocumentFileCompat.fromSingleUri(context, sourceUri) - source.copyTo(destinationDocFile) - - // the source file name and the created document file name can be different when: - // - a file with the same name already exists, so the name gets a suffix like ` (1)` - // - the original extension does not match the extension added by the underlying provider - val fileName = destinationDocFile.name - val destinationFullPath = destinationDir + fileName - - var deletedSource = false - if (!copy) { - // delete original entry - try { - delete(context, sourceUri, sourcePath).get() - deletedSource = true - } catch (e: ExecutionException) { - Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) - } catch (e: InterruptedException) { - Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) + val sourceFile = File(sourcePath) + val sourceDir = sourceFile.parent?.let { ensureTrailingSeparator(it) } + if (sourceDir == destinationDir) { + if (copy) { + future.setException(Exception("file at path=$sourcePath is already in destination directory")) + } else { + future.set(HashMap()) } + } else { + val sourceFileName = sourceFile.name + val desiredNameWithoutExtension = sourceFileName.replaceFirst("[.][^.]+$".toRegex(), "") + + // the file created from a `TreeDocumentFile` is also a `TreeDocumentFile` + // but in order to open an output stream to it, we need to use a `SingleDocumentFile` + // through a document URI, not a tree URI + // note that `DocumentFile.getParentFile()` returns null if we did not pick a tree first + val destinationTreeFile = destinationDirDocFile.createFile(mimeType, desiredNameWithoutExtension) + val destinationDocFile = DocumentFileCompat.fromSingleUri(context, destinationTreeFile.uri) + + // `DocumentsContract.moveDocument()` needs `sourceParentDocumentUri`, which could be different for each entry + // `DocumentsContract.copyDocument()` yields "Unsupported call: android:copyDocument" + // when used with entry URI as `sourceDocumentUri`, and destinationDirDocFile URI as `targetParentDocumentUri` + val source = DocumentFileCompat.fromSingleUri(context, sourceUri) + source.copyTo(destinationDocFile) + + // the source file name and the created document file name can be different when: + // - a file with the same name already exists, so the name gets a suffix like ` (1)` + // - the original extension does not match the extension added by the underlying provider + val fileName = destinationDocFile.name + val destinationFullPath = destinationDir + fileName + + var deletedSource = false + if (!copy) { + // delete original entry + try { + delete(context, sourceUri, sourcePath).get() + deletedSource = true + } catch (e: ExecutionException) { + Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) + } catch (e: InterruptedException) { + Log.w(LOG_TAG, "failed to delete entry with path=$sourcePath", e) + } + } + + scanNewPath(context, destinationFullPath, mimeType, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) { + fields["deletedSource"] = deletedSource + future.set(fields) + } + + override fun onFailure(throwable: Throwable) { + future.setException(throwable) + } + }) } - - scanNewPath(context, destinationFullPath, mimeType, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) { - fields["deletedSource"] = deletedSource - future.set(fields) - } - - override fun onFailure(throwable: Throwable) { - future.setException(throwable) - } - }) } catch (e: Exception) { Log.e(LOG_TAG, "failed to ${(if (copy) "copy" else "move")} entry", e) future.setException(e) diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index d3c80fe7a..7c99427bd 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -136,13 +136,15 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); } else { await Future.forEach(movedOps, (movedOp) async { - final sourceUri = movedOp.uri; final newFields = movedOp.newFields; - final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - if (entry != null) { - fromAlbums.add(entry.directory); - movedEntries.add(entry); - await moveEntry(entry, newFields); + if (newFields.isNotEmpty) { + final sourceUri = movedOp.uri; + final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + if (entry != null) { + fromAlbums.add(entry.directory); + movedEntries.add(entry); + await moveEntry(entry, newFields); + } } }); } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 26ef150a1..837776778 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -126,7 +126,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { final selectionCount = selection.length; if (movedCount < selectionCount) { final count = selectionCount - movedCount; - showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}'); + showFeedback(context, 'Failed to ${copy ? 'copy' : 'move'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); } else { final count = movedCount; showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}');