#107 renaming: check and delete Media Store obsolete entry
This commit is contained in:
parent
d87cf4395f
commit
a3bd158ca6
10 changed files with 213 additions and 104 deletions
|
@ -35,7 +35,6 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
|
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getThumbnail) }
|
||||||
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
|
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::getRegion) }
|
||||||
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
|
"captureFrame" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::captureFrame) }
|
||||||
"rename" -> GlobalScope.launch(Dispatchers.IO) { safeSuspend(call, result, ::rename) }
|
|
||||||
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }
|
"clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
|
@ -164,34 +163,6 @@ class MediaFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun rename(call: MethodCall, result: MethodChannel.Result) {
|
|
||||||
val entryMap = call.argument<FieldMap>("entry")
|
|
||||||
val newName = call.argument<String>("newName")
|
|
||||||
if (entryMap == null || newName == null) {
|
|
||||||
result.error("rename-args", "failed because of missing arguments", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
|
|
||||||
val path = entryMap["path"] as String?
|
|
||||||
val mimeType = entryMap["mimeType"] as String?
|
|
||||||
if (uri == null || path == null || mimeType == null) {
|
|
||||||
result.error("rename-args", "failed because entry fields are missing", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val provider = getProvider(uri)
|
|
||||||
if (provider == null) {
|
|
||||||
result.error("rename-provider", "failed to find provider for uri=$uri", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
provider.rename(activity, path, uri, mimeType, newName, object : ImageOpCallback {
|
|
||||||
override fun onSuccess(fields: FieldMap) = result.success(fields)
|
|
||||||
override fun onFailure(throwable: Throwable) = result.error("rename-failure", "failed to rename", throwable.message)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearSizedThumbnailDiskCache(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
|
private fun clearSizedThumbnailDiskCache(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
|
||||||
Glide.get(activity).clearDiskCache()
|
Glide.get(activity).clearDiskCache()
|
||||||
result.success(null)
|
result.success(null)
|
||||||
|
|
|
@ -709,7 +709,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
|
||||||
try {
|
try {
|
||||||
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
|
||||||
val metadata = ImageMetadataReader.readMetadata(input)
|
val metadata = ImageMetadataReader.readMetadata(input)
|
||||||
val fields = hashMapOf<String, Any?>(
|
val fields: FieldMap = hashMapOf(
|
||||||
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
|
"projectionType" to XMP.GPANO_PROJECTION_TYPE_DEFAULT,
|
||||||
)
|
)
|
||||||
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
for (dir in metadata.getDirectoriesOfType(XmpDirectory::class.java)) {
|
||||||
|
|
|
@ -45,6 +45,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
||||||
"delete" -> GlobalScope.launch(Dispatchers.IO) { delete() }
|
"delete" -> GlobalScope.launch(Dispatchers.IO) { delete() }
|
||||||
"export" -> GlobalScope.launch(Dispatchers.IO) { export() }
|
"export" -> GlobalScope.launch(Dispatchers.IO) { export() }
|
||||||
"move" -> GlobalScope.launch(Dispatchers.IO) { move() }
|
"move" -> GlobalScope.launch(Dispatchers.IO) { move() }
|
||||||
|
"rename" -> GlobalScope.launch(Dispatchers.IO) { rename() }
|
||||||
else -> endOfStream()
|
else -> endOfStream()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +101,7 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
||||||
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
|
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
|
||||||
val path = entryMap["path"] as String?
|
val path = entryMap["path"] as String?
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
val result = hashMapOf<String, Any?>(
|
val result: FieldMap = hashMapOf(
|
||||||
"uri" to uri.toString(),
|
"uri" to uri.toString(),
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
|
@ -178,6 +179,34 @@ class ImageOpStreamHandler(private val activity: Activity, private val arguments
|
||||||
endOfStream()
|
endOfStream()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun rename() {
|
||||||
|
if (arguments !is Map<*, *> || entryMapList.isEmpty()) {
|
||||||
|
endOfStream()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val newName = arguments["newName"] as String?
|
||||||
|
if (newName == null) {
|
||||||
|
error("rename-args", "failed because of missing arguments", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// assume same provider for all entries
|
||||||
|
val firstEntry = entryMapList.first()
|
||||||
|
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) }
|
||||||
|
if (provider == null) {
|
||||||
|
error("rename-provider", "failed to find provider for entry=$firstEntry", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val entries = entryMapList.map(::AvesEntry)
|
||||||
|
provider.renameMultiple(activity, newName, entries, object : ImageOpCallback {
|
||||||
|
override fun onSuccess(fields: FieldMap) = success(fields)
|
||||||
|
override fun onFailure(throwable: Throwable) = error("rename-failure", "failed to rename", throwable.message)
|
||||||
|
})
|
||||||
|
endOfStream()
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
|
private val LOG_TAG = LogUtils.createTag<ImageOpStreamHandler>()
|
||||||
const val CHANNEL = "deckers.thibault/aves/media_op_stream"
|
const val CHANNEL = "deckers.thibault/aves/media_op_stream"
|
||||||
|
|
|
@ -46,7 +46,7 @@ object MultiPage {
|
||||||
val format = extractor.getTrackFormat(i)
|
val format = extractor.getTrackFormat(i)
|
||||||
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
||||||
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
|
val trackMime = if (mime == MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC) MimeTypes.HEIC else mime
|
||||||
val track = hashMapOf<String, Any?>(
|
val track: FieldMap = hashMapOf(
|
||||||
KEY_PAGE to i,
|
KEY_PAGE to i,
|
||||||
KEY_MIME_TYPE to trackMime,
|
KEY_MIME_TYPE to trackMime,
|
||||||
)
|
)
|
||||||
|
@ -106,7 +106,7 @@ object MultiPage {
|
||||||
val format = extractor.getTrackFormat(i)
|
val format = extractor.getTrackFormat(i)
|
||||||
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
format.getString(MediaFormat.KEY_MIME)?.let { mime ->
|
||||||
if (MimeTypes.isVideo(mime)) {
|
if (MimeTypes.isVideo(mime)) {
|
||||||
val track = hashMapOf<String, Any?>(
|
val track: FieldMap = hashMapOf(
|
||||||
KEY_PAGE to trackCount++,
|
KEY_PAGE to trackCount++,
|
||||||
KEY_MIME_TYPE to MimeTypes.MP4,
|
KEY_MIME_TYPE to MimeTypes.MP4,
|
||||||
KEY_IS_DEFAULT to false,
|
KEY_IS_DEFAULT to false,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.drew.imaging.ImageMetadataReader
|
||||||
import com.drew.metadata.file.FileTypeDirectory
|
import com.drew.metadata.file.FileTypeDirectory
|
||||||
import deckers.thibault.aves.metadata.Metadata
|
import deckers.thibault.aves.metadata.Metadata
|
||||||
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
|
||||||
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.model.SourceEntry
|
import deckers.thibault.aves.model.SourceEntry
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
import deckers.thibault.aves.utils.MimeTypes
|
import deckers.thibault.aves.utils.MimeTypes
|
||||||
|
@ -47,16 +48,16 @@ internal class ContentImageProvider : ImageProvider() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val map = hashMapOf<String, Any?>(
|
val fields: FieldMap = hashMapOf(
|
||||||
"uri" to uri.toString(),
|
"uri" to uri.toString(),
|
||||||
"sourceMimeType" to mimeType,
|
"sourceMimeType" to mimeType,
|
||||||
)
|
)
|
||||||
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()) {
|
||||||
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) map["title"] = cursor.getString(it) }
|
cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME).let { if (it != -1) fields["title"] = cursor.getString(it) }
|
||||||
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) map["sizeBytes"] = cursor.getLong(it) }
|
cursor.getColumnIndex(OpenableColumns.SIZE).let { if (it != -1) fields["sizeBytes"] = cursor.getLong(it) }
|
||||||
cursor.getColumnIndex(PATH).let { if (it != -1) map["path"] = cursor.getString(it) }
|
cursor.getColumnIndex(PATH).let { if (it != -1) fields["path"] = cursor.getString(it) }
|
||||||
cursor.close()
|
cursor.close()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
@ -64,7 +65,7 @@ internal class ContentImageProvider : ImageProvider() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val entry = SourceEntry(map).fillPreCatalogMetadata(context)
|
val entry = SourceEntry(fields).fillPreCatalogMetadata(context)
|
||||||
if (entry.isSized || entry.isSvg || entry.isVideo) {
|
if (entry.isSized || entry.isSvg || entry.isVideo) {
|
||||||
callback.onSuccess(entry.toMap())
|
callback.onSuccess(entry.toMap())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,7 +32,6 @@ import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
|
||||||
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
|
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.HashMap
|
import kotlin.collections.HashMap
|
||||||
|
@ -50,6 +49,10 @@ 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(activity: Activity, newFileName: String, entries: List<AvesEntry>, callback: ImageOpCallback) {
|
||||||
|
callback.onFailure(UnsupportedOperationException("`renameMultiple` is not supported by this image provider"))
|
||||||
|
}
|
||||||
|
|
||||||
open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
|
open fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
|
||||||
throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider")
|
throw UnsupportedOperationException("`scanPostMetadataEdit` is not supported by this image provider")
|
||||||
}
|
}
|
||||||
|
@ -81,7 +84,7 @@ abstract class ImageProvider {
|
||||||
val sourcePath = entry.path
|
val sourcePath = entry.path
|
||||||
val pageId = entry.pageId
|
val pageId = entry.pageId
|
||||||
|
|
||||||
val result = hashMapOf<String, Any?>(
|
val result: FieldMap = hashMapOf(
|
||||||
"uri" to sourceUri.toString(),
|
"uri" to sourceUri.toString(),
|
||||||
"pageId" to pageId,
|
"pageId" to pageId,
|
||||||
"success" to false,
|
"success" to false,
|
||||||
|
@ -371,36 +374,6 @@ abstract class ImageProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) {
|
|
||||||
val oldFile = File(oldPath)
|
|
||||||
val newFile = File(oldFile.parent, newFilename)
|
|
||||||
if (oldFile == newFile) {
|
|
||||||
Log.w(LOG_TAG, "new name and old name are the same, path=$oldPath")
|
|
||||||
callback.onSuccess(HashMap())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val df = getDocumentFile(context, oldPath, oldMediaUri)
|
|
||||||
try {
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
val renamed = df != null && df.renameTo(newFilename)
|
|
||||||
if (!renamed) {
|
|
||||||
callback.onFailure(Exception("failed to rename entry at path=$oldPath"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
callback.onFailure(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
scanObsoletePath(context, oldPath, mimeType)
|
|
||||||
try {
|
|
||||||
callback.onSuccess(MediaStoreImageProvider().scanNewPath(context, newFile.path, mimeType))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
callback.onFailure(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun editExif(
|
private fun editExif(
|
||||||
context: Context,
|
context: Context,
|
||||||
path: String,
|
path: String,
|
||||||
|
|
|
@ -223,6 +223,23 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun hasEntry(context: Context, contentUri: Uri): Boolean {
|
||||||
|
var found = false
|
||||||
|
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||||
|
try {
|
||||||
|
val cursor = context.contentResolver.query(contentUri, projection, null, null, null)
|
||||||
|
if (cursor != null) {
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(LOG_TAG, "failed to get entry at contentUri=$contentUri", e)
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
private fun needSize(mimeType: String) = MimeTypes.SVG != mimeType
|
||||||
|
|
||||||
// `uri` is a media URI, not a document URI
|
// `uri` is a media URI, not a document URI
|
||||||
|
@ -286,7 +303,7 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
val sourcePath = entry.path
|
val sourcePath = entry.path
|
||||||
val mimeType = entry.mimeType
|
val mimeType = entry.mimeType
|
||||||
|
|
||||||
val result = hashMapOf<String, Any?>(
|
val result: FieldMap = hashMapOf(
|
||||||
"uri" to sourceUri.toString(),
|
"uri" to sourceUri.toString(),
|
||||||
"success" to false,
|
"success" to false,
|
||||||
)
|
)
|
||||||
|
@ -391,6 +408,90 @@ class MediaStoreImageProvider : ImageProvider() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun renameMultiple(
|
||||||
|
activity: Activity,
|
||||||
|
newFileName: String,
|
||||||
|
entries: List<AvesEntry>,
|
||||||
|
callback: ImageOpCallback,
|
||||||
|
) {
|
||||||
|
for (entry in entries) {
|
||||||
|
val sourceUri = entry.uri
|
||||||
|
val sourcePath = entry.path
|
||||||
|
val mimeType = entry.mimeType
|
||||||
|
|
||||||
|
val result: FieldMap = hashMapOf(
|
||||||
|
"uri" to sourceUri.toString(),
|
||||||
|
"success" to false,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (sourcePath != null) {
|
||||||
|
try {
|
||||||
|
val newFields = renameSingle(
|
||||||
|
activity = activity,
|
||||||
|
oldPath = sourcePath,
|
||||||
|
oldMediaUri = sourceUri,
|
||||||
|
newFileName = newFileName,
|
||||||
|
mimeType = mimeType,
|
||||||
|
)
|
||||||
|
result["newFields"] = newFields
|
||||||
|
result["success"] = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to rename to newFileName=$newFileName entry with sourcePath=$sourcePath", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback.onSuccess(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun renameSingle(
|
||||||
|
activity: Activity,
|
||||||
|
oldPath: String,
|
||||||
|
oldMediaUri: Uri,
|
||||||
|
newFileName: String,
|
||||||
|
mimeType: String,
|
||||||
|
): FieldMap {
|
||||||
|
val oldFile = File(oldPath)
|
||||||
|
val newFile = File(oldFile.parent, newFileName)
|
||||||
|
if (oldFile == newFile) {
|
||||||
|
// nothing to do
|
||||||
|
return skippedFieldMap
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
val renamed = getDocumentFile(activity, oldPath, oldMediaUri)?.renameTo(newFileName) ?: false
|
||||||
|
if (!renamed) {
|
||||||
|
throw Exception("failed to rename entry at path=$oldPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
// renaming may be successful and the file at the old path no longer exists
|
||||||
|
// but, in some situations, scanning the old path does not clear the Media Store entry
|
||||||
|
// e.g. for media owned by another package in the Download folder on API 29
|
||||||
|
|
||||||
|
// for higher chance of accurate obsolete item check, keep this order:
|
||||||
|
// 1) scan obsolete item,
|
||||||
|
// 2) scan current item,
|
||||||
|
// 3) check obsolete item in Media Store
|
||||||
|
|
||||||
|
scanObsoletePath(activity, oldPath, mimeType)
|
||||||
|
val newFields = scanNewPath(activity, newFile.path, mimeType)
|
||||||
|
|
||||||
|
var deletedSource = !hasEntry(activity, oldMediaUri)
|
||||||
|
if (!deletedSource) {
|
||||||
|
Log.w(LOG_TAG, "renaming item at uri=$oldMediaUri to newFileName=$newFileName did not clear the MediaStore entry for obsolete path=$oldPath")
|
||||||
|
|
||||||
|
// delete obsolete entry
|
||||||
|
try {
|
||||||
|
delete(activity, oldMediaUri, oldPath)
|
||||||
|
deletedSource = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(LOG_TAG, "failed to delete entry with path=$oldPath", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newFields["deletedSource"] = deletedSource
|
||||||
|
|
||||||
|
return newFields
|
||||||
|
}
|
||||||
|
|
||||||
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
|
override fun scanPostMetadataEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
|
||||||
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, _ ->
|
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, _ ->
|
||||||
val projection = arrayOf(
|
val projection = arrayOf(
|
||||||
|
|
|
@ -162,13 +162,34 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM
|
||||||
|
|
||||||
Future<bool> renameEntry(AvesEntry entry, String newName, {required bool persist}) async {
|
Future<bool> renameEntry(AvesEntry entry, String newName, {required bool persist}) async {
|
||||||
if (newName == entry.filenameWithoutExtension) return true;
|
if (newName == entry.filenameWithoutExtension) return true;
|
||||||
final newFields = await mediaFileService.rename(entry, '$newName${entry.extension}');
|
|
||||||
if (newFields.isEmpty) return false;
|
|
||||||
|
|
||||||
await _moveEntry(entry, newFields, persist: persist);
|
pauseMonitoring();
|
||||||
entry.metadataChangeNotifier.notifyListeners();
|
final completer = Completer<bool>();
|
||||||
eventBus.fire(EntryMovedEvent({entry}));
|
final processed = <MoveOpEvent>{};
|
||||||
return true;
|
mediaFileService.rename({entry}, newName: '$newName${entry.extension}').listen(
|
||||||
|
processed.add,
|
||||||
|
onError: (error) => reportService.recordError('renameEntry failed with error=$error', null),
|
||||||
|
onDone: () async {
|
||||||
|
final successOps = processed.where((e) => e.success).toSet();
|
||||||
|
if (successOps.isEmpty) {
|
||||||
|
completer.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final newFields = successOps.first.newFields;
|
||||||
|
if (newFields.isEmpty) {
|
||||||
|
completer.complete(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _moveEntry(entry, newFields, persist: persist);
|
||||||
|
entry.metadataChangeNotifier.notifyListeners();
|
||||||
|
eventBus.fire(EntryMovedEvent({entry}));
|
||||||
|
completer.complete(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final success = await completer.future;
|
||||||
|
resumeMonitoring();
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> renameAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> todoEntries, Set<MoveOpEvent> movedOps) async {
|
Future<void> renameAlbum(String sourceAlbum, String destinationAlbum, Set<AvesEntry> todoEntries, Set<MoveOpEvent> movedOps) async {
|
||||||
|
|
|
@ -84,6 +84,11 @@ abstract class MediaFileService {
|
||||||
required NameConflictStrategy nameConflictStrategy,
|
required NameConflictStrategy nameConflictStrategy,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Stream<MoveOpEvent> rename(
|
||||||
|
Iterable<AvesEntry> entries, {
|
||||||
|
required String newName,
|
||||||
|
});
|
||||||
|
|
||||||
Future<Map<String, dynamic>> captureFrame(
|
Future<Map<String, dynamic>> captureFrame(
|
||||||
AvesEntry entry, {
|
AvesEntry entry, {
|
||||||
required String desiredName,
|
required String desiredName,
|
||||||
|
@ -92,8 +97,6 @@ abstract class MediaFileService {
|
||||||
required String destinationAlbum,
|
required String destinationAlbum,
|
||||||
required NameConflictStrategy nameConflictStrategy,
|
required NameConflictStrategy nameConflictStrategy,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlatformMediaFileService implements MediaFileService {
|
class PlatformMediaFileService implements MediaFileService {
|
||||||
|
@ -346,6 +349,23 @@ class PlatformMediaFileService implements MediaFileService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<MoveOpEvent> rename(
|
||||||
|
Iterable<AvesEntry> entries, {
|
||||||
|
required String newName,
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
return _opStreamChannel.receiveBroadcastStream(<String, dynamic>{
|
||||||
|
'op': 'rename',
|
||||||
|
'entries': entries.map(_toPlatformEntryMap).toList(),
|
||||||
|
'newName': newName,
|
||||||
|
}).map((event) => MoveOpEvent.fromMap(event));
|
||||||
|
} on PlatformException catch (e, stack) {
|
||||||
|
reportService.recordError(e, stack);
|
||||||
|
return Stream.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> captureFrame(
|
Future<Map<String, dynamic>> captureFrame(
|
||||||
AvesEntry entry, {
|
AvesEntry entry, {
|
||||||
|
@ -370,19 +390,4 @@ class PlatformMediaFileService implements MediaFileService {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName) async {
|
|
||||||
try {
|
|
||||||
// returns map with: 'contentId' 'path' 'title' 'uri' (all optional)
|
|
||||||
final result = await platform.invokeMethod('rename', <String, dynamic>{
|
|
||||||
'entry': _toPlatformEntryMap(entry),
|
|
||||||
'newName': newName,
|
|
||||||
});
|
|
||||||
if (result != null) return (result as Map).cast<String, dynamic>();
|
|
||||||
} on PlatformException catch (e, stack) {
|
|
||||||
await reportService.recordError(e, stack);
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/services/common/image_op_events.dart';
|
||||||
import 'package:aves/services/media/media_file_service.dart';
|
import 'package:aves/services/media/media_file_service.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'media_store_service.dart';
|
import 'media_store_service.dart';
|
||||||
|
|
||||||
class FakeMediaFileService extends Fake implements MediaFileService {
|
class FakeMediaFileService extends Fake implements MediaFileService {
|
||||||
@override
|
@override
|
||||||
Future<Map<String, dynamic>> rename(AvesEntry entry, String newName) {
|
Stream<MoveOpEvent> rename(
|
||||||
|
Iterable<AvesEntry> entries, {
|
||||||
|
required String newName,
|
||||||
|
}) {
|
||||||
final contentId = FakeMediaStoreService.nextContentId;
|
final contentId = FakeMediaStoreService.nextContentId;
|
||||||
return SynchronousFuture({
|
final entry = entries.first;
|
||||||
'uri': 'content://media/external/images/media/$contentId',
|
return Stream.value(MoveOpEvent(
|
||||||
'contentId': contentId,
|
success: true,
|
||||||
'path': '${entry.directory}/$newName',
|
uri: entry.uri,
|
||||||
'displayName': newName,
|
newFields: {
|
||||||
'title': newName.substring(0, newName.length - entry.extension!.length),
|
'uri': 'content://media/external/images/media/$contentId',
|
||||||
'dateModifiedSecs': FakeMediaStoreService.dateSecs,
|
'contentId': contentId,
|
||||||
});
|
'path': '${entry.directory}/$newName',
|
||||||
|
'displayName': newName,
|
||||||
|
'title': newName.substring(0, newName.length - entry.extension!.length),
|
||||||
|
'dateModifiedSecs': FakeMediaStoreService.dateSecs,
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue