vaults: fixed moved out item getting duplicated, fixed renaming item

This commit is contained in:
Thibault Deckers 2023-02-20 23:26:21 +01:00
parent a8159f9525
commit df53b91bdf
4 changed files with 111 additions and 81 deletions

View file

@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler { class ImageOpStreamHandler(private val activity: Activity, private val arguments: Any?) : EventChannel.StreamHandler {
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
@ -219,18 +220,20 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
entriesToNewName[AvesEntry(rawEntry)] = newName entriesToNewName[AvesEntry(rawEntry)] = newName
} }
// assume same provider for all entries val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) }
val firstEntry = entriesToNewName.keys.first() for ((provider, entryList) in byProvider) {
val provider = getProvider(firstEntry.uri) if (provider == null) {
if (provider == null) { error("rename-provider", "failed to find provider for entry=${entryList.firstOrNull()}", null)
error("rename-provider", "failed to find provider for entry=$firstEntry", null) return
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() endOfStream()
} }

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.net.Uri import android.net.Uri
@ -53,6 +54,26 @@ internal class FileImageProvider : ImageProvider() {
throw Exception("failed to delete entry with uri=$uri path=$path") 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) { override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {
try { try {
val file = File(path) val file = File(path)

View file

@ -68,13 +68,75 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider")) callback.onFailure(UnsupportedOperationException("`moveMultiple` is not supported by this image provider"))
} }
open suspend fun renameMultiple( suspend fun renameMultiple(
activity: Activity, activity: Activity,
entriesToNewName: Map<AvesEntry, String>, entriesToNewName: Map<AvesEntry, String>,
isCancelledOp: CancelCheck, isCancelledOp: CancelCheck,
callback: ImageOpCallback, 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) { open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: FieldMap, callback: ImageOpCallback) {

View file

@ -552,10 +552,10 @@ class MediaStoreImageProvider : ImageProvider() {
) )
} else if (toVault) { } else if (toVault) {
hashMapOf( hashMapOf(
"origin" to SourceEntry.ORIGIN_VAULT,
"uri" to File(targetPath).toUri().toString(), "uri" to File(targetPath).toUri().toString(),
"contentId" to null, "contentId" to null,
"path" to targetPath, "path" to targetPath,
"origin" to SourceEntry.ORIGIN_VAULT,
) )
} else { } else {
scanNewPath(activity, targetPath, mimeType) scanNewPath(activity, targetPath, mimeType)
@ -626,74 +626,16 @@ class MediaStoreImageProvider : ImageProvider() {
return targetDir + fileName return targetDir + fileName
} }
override suspend fun renameMultiple( override suspend fun renameSingle(
activity: Activity,
entriesToNewName: Map<AvesEntry, String>,
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(
activity: Activity, activity: Activity,
mimeType: String, mimeType: String,
oldMediaUri: Uri, oldMediaUri: Uri,
oldPath: String, oldPath: String,
desiredName: String, newFile: File,
): FieldMap { ): FieldMap = when {
val desiredNameWithoutExtension = desiredName.substringBeforeLast(".") StorageUtils.canEditByFile(activity, oldPath) -> renameSingleByFile(activity, mimeType, oldMediaUri, oldPath, newFile)
isMediaUriPermissionGranted(activity, oldMediaUri, mimeType) -> renameSingleByMediaStore(activity, mimeType, oldMediaUri, newFile)
val oldFile = File(oldPath) else -> renameSingleByTreeDoc(activity, mimeType, oldMediaUri, oldPath, newFile)
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)
}
} }
private suspend fun renameSingleByMediaStore( private suspend fun renameSingleByMediaStore(
@ -851,10 +793,12 @@ class MediaStoreImageProvider : ImageProvider() {
try { try {
val cursor = context.contentResolver.query(uri, projection, null, null, null) val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) { if (cursor != null && cursor.moveToFirst()) {
val newFields = HashMap<String, Any?>() val newFields = hashMapOf<String, Any?>(
newFields["uri"] = uri.toString() "origin" to SourceEntry.ORIGIN_MEDIA_STORE_CONTENT,
newFields["contentId"] = uri.tryParseId() "uri" to uri.toString(),
newFields["path"] = path "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_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.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.close() cursor.close()