From df53b91bdf437256b5dbc532b1b2ca907dc74bc7 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 20 Feb 2023 23:26:21 +0100 Subject: [PATCH] vaults: fixed moved out item getting duplicated, fixed renaming item --- .../channel/streams/ImageOpStreamHandler.kt | 23 +++--- .../aves/model/provider/FileImageProvider.kt | 21 +++++ .../aves/model/provider/ImageProvider.kt | 66 ++++++++++++++- .../model/provider/MediaStoreImageProvider.kt | 82 +++---------------- 4 files changed, 111 insertions(+), 81 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index de0d7d78a..dce85d755 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import java.util.* class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler { private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -219,18 +220,20 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments entriesToNewName[AvesEntry(rawEntry)] = newName } - // assume same provider for all entries - val firstEntry = entriesToNewName.keys.first() - val provider = getProvider(firstEntry.uri) - if (provider == null) { - error("rename-provider", "failed to find provider for entry=$firstEntry", null) - return + val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) } + for ((provider, entryList) in byProvider) { + if (provider == null) { + error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null) + return + } + + val entryMap = mapOf(*entryList.map { Pair(it.key, it.value) }.toTypedArray()) + provider.renameMultiple(activity, entryMap, ::isCancelledOp, object : ImageOpCallback { + override fun onSuccess(fields: FieldMap) = success(fields) + override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) + }) } - provider.renameMultiple(activity, entriesToNewName, ::isCancelledOp, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) = success(fields) - override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message) - }) endOfStream() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index 6a1b0725e..7ef565d8b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves.model.provider +import android.app.Activity import android.content.Context import android.content.ContextWrapper import android.net.Uri @@ -53,6 +54,26 @@ internal class FileImageProvider : ImageProvider() { throw Exception("failed to delete entry with uri=$uri path=$path") } + override suspend fun renameSingle( + activity: Activity, + mimeType: String, + oldMediaUri: Uri, + oldPath: String, + newFile: File, + ): FieldMap { + Log.d(LOG_TAG, "rename file at path=$oldPath") + val renamed = File(oldPath).renameTo(newFile) + if (!renamed) { + throw Exception("failed to rename file at path=$oldPath") + } + + return hashMapOf( + "uri" to Uri.fromFile(newFile).toString(), + "path" to newFile.path, + "dateModifiedSecs" to newFile.lastModified() / 1000, + ) + } + override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) { try { val file = File(path) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index 4fb59c16c..8e973af43 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -68,13 +68,75 @@ abstract class ImageProvider { callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider")) } - open suspend fun renameMultiple( + suspend fun renameMultiple( activity: Activity, entriesToNewName: Map, isCancelledOp: CancelCheck, callback: ImageOpCallback, ) { - callback.onFailure(UnsupportedOperationException("`renameMultiple` is not supported by this image provider")) + for (kv in entriesToNewName) { + val entry = kv.key + val desiredName = kv.value + + val sourceUri = entry.uri + val sourcePath = entry.path + val mimeType = entry.mimeType + + val result: FieldMap = hashMapOf( + "uri" to sourceUri.toString(), + "success" to false, + ) + + // prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store + if (sourcePath != null && !desiredName.startsWith('.')) { + try { + var newFields: FieldMap = skippedFieldMap + if (!isCancelledOp()) { + val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") + + val oldFile = File(sourcePath) + if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { + oldFile.parent?.let { dir -> + resolveTargetFileNameWithoutExtension( + contextWrapper = activity, + dir = dir, + desiredNameWithoutExtension = desiredNameWithoutExtension, + mimeType = mimeType, + conflictStrategy = NameConflictStrategy.RENAME, + )?.let { targetNameWithoutExtension -> + val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" + val newFile = File(dir, targetFileName) + if (oldFile != newFile) { + newFields = renameSingle( + activity = activity, + mimeType = mimeType, + oldMediaUri = sourceUri, + oldPath = sourcePath, + newFile = newFile, + ) + } + } + } + } + } + result["newFields"] = newFields + result["success"] = true + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to rename to newFileName=$desiredName entry with sourcePath=$sourcePath", e) + } + } + callback.onSuccess(result) + } + } + + open suspend fun renameSingle( + activity: Activity, + mimeType: String, + oldMediaUri: Uri, + oldPath: String, + newFile: File, + ): FieldMap { + throw UnsupportedOperationException("`renameSingle` is not supported by this image provider") } open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) { 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 5e568be50..c57872841 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 @@ -552,10 +552,10 @@ class MediaStoreImageProvider : ImageProvider() { ) } else if (toVault) { hashMapOf( + "origin" to SourceEntry.ORIGIN_VAULT, "uri" to File(targetPath).toUri().toString(), "contentId" to null, "path" to targetPath, - "origin" to SourceEntry.ORIGIN_VAULT, ) } else { scanNewPath(activity, targetPath, mimeType) @@ -626,74 +626,16 @@ class MediaStoreImageProvider : ImageProvider() { return targetDir + fileName } - override suspend fun renameMultiple( - activity: Activity, - entriesToNewName: Map, - isCancelledOp: CancelCheck, - callback: ImageOpCallback, - ) { - for (kv in entriesToNewName) { - val entry = kv.key - val desiredName = kv.value - - val sourceUri = entry.uri - val sourcePath = entry.path - val mimeType = entry.mimeType - - val result: FieldMap = hashMapOf( - "uri" to sourceUri.toString(), - "success" to false, - ) - - // prevent naming with a `.` prefix as it would hide the file and remove it from the Media Store - if (sourcePath != null && !desiredName.startsWith('.')) { - try { - val newFields = if (isCancelledOp()) skippedFieldMap else renameSingle( - activity = activity, - mimeType = mimeType, - oldMediaUri = sourceUri, - oldPath = sourcePath, - desiredName = desiredName, - ) - result["newFields"] = newFields - result["success"] = true - } catch (e: Exception) { - Log.w(LOG_TAG, "failed to rename to newFileName=$desiredName entry with sourcePath=$sourcePath", e) - } - } - callback.onSuccess(result) - } - } - - private suspend fun renameSingle( + override suspend fun renameSingle( activity: Activity, mimeType: String, oldMediaUri: Uri, oldPath: String, - desiredName: String, - ): FieldMap { - val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") - - val oldFile = File(oldPath) - if (oldFile.nameWithoutExtension == desiredNameWithoutExtension) return skippedFieldMap - - val dir = oldFile.parent ?: return skippedFieldMap - val targetNameWithoutExtension = resolveTargetFileNameWithoutExtension( - contextWrapper = activity, - dir = dir, - desiredNameWithoutExtension = desiredNameWithoutExtension, - mimeType = mimeType, - conflictStrategy = NameConflictStrategy.RENAME, - ) ?: return skippedFieldMap - val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" - - val newFile = File(dir, targetFileName) - return when { - oldFile == newFile -> skippedFieldMap - StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile) - isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile) - else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile) - } + newFile: File, + ): FieldMap = when { + StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile) + isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile) + else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile) } private suspend fun renameSingleByMediaStore( @@ -851,10 +793,12 @@ class MediaStoreImageProvider : ImageProvider() { try { val cursor = context.contentResolver.query(uri, projection, null, null, null) if (cursor != null && cursor.moveToFirst()) { - val newFields = HashMap() - newFields["uri"] = uri.toString() - newFields["contentId"] = uri.tryParseId() - newFields["path"] = path + val newFields = hashMapOf( + "origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT, + "uri" to uri.toString(), + "contentId" to uri.tryParseId(), + "path" to path, + ) cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) } cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } cursor.close()