vaults: fixed moved out item getting duplicated, fixed renaming item
This commit is contained in:
parent
a8159f9525
commit
df53b91bdf
4 changed files with 111 additions and 81 deletions
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue