diff --git a/CHANGELOG.md b/CHANGELOG.md index cfaa85784..8e7a31ef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- moved file losing its extension and no longer being detected as media in some cases - opening home when launching app as media picker - removing groups with obsolete albums - loading group custom covers diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt index 58c07fbf4..9b23fbc9d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt @@ -311,7 +311,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { embeddedByteStream: InputStream, embeddedByteLength: Long, ) { - val extension = extensionFor(mimeType) + val extension = extensionFor(mimeType, defaultExtension = null) val targetFile = StorageUtils.createTempFile(context, extension).apply { transferFrom(embeddedByteStream, embeddedByteLength) } @@ -319,7 +319,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler { val authority = "${context.applicationContext.packageName}.file_provider" val uri = if (displayName != null) { // add extension to ease type identification when sharing this content - val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) { + val displayNameWithExtension = if (displayName.endsWith(extension, ignoreCase = true)) { displayName } else { "$displayName$extension" 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 9d36f1812..2804bda5f 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 @@ -142,16 +142,18 @@ abstract class ImageProvider { val oldFile = File(sourcePath) if (oldFile.nameWithoutExtension != desiredNameWithoutExtension) { + val defaultExtension = oldFile.extension oldFile.parent?.let { dir -> val resolution = resolveTargetFileNameWithoutExtension( contextWrapper = activity, dir = dir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = mimeType, + defaultExtension = defaultExtension, conflictStrategy = NameConflictStrategy.RENAME, ) resolution.nameWithoutExtension?.let { targetNameWithoutExtension -> - val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}" + val targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}" val newFile = File(dir, targetFileName) if (oldFile != newFile) { newFields = renameSingle( @@ -277,11 +279,17 @@ abstract class ImageProvider { val page = if (sourceMimeType == MimeTypes.TIFF) pageId + 1 else pageId desiredNameWithoutExtension += "_${page.toString().padStart(3, '0')}" } + + // there is no benefit providing input extension + // for known output MIME type + val defaultExtension = null + val resolution = resolveTargetFileNameWithoutExtension( contextWrapper = activity, dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = exportMimeType, + defaultExtension = defaultExtension, conflictStrategy = nameConflictStrategy, ) val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap @@ -358,6 +366,7 @@ abstract class ImageProvider { targetDir = targetDir, targetDirDocFile = targetDirDocFile, targetNameWithoutExtension = targetNameWithoutExtension, + defaultExtension = defaultExtension, write = write, ) @@ -465,6 +474,7 @@ abstract class ImageProvider { dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = captureMimeType, + defaultExtension = null, conflictStrategy = nameConflictStrategy, ) } catch (e: Exception) { @@ -571,13 +581,14 @@ abstract class ImageProvider { dir: String, desiredNameWithoutExtension: String, mimeType: String, + defaultExtension: String?, conflictStrategy: NameConflictStrategy, ): NameConflictResolution { val sanitizedNameWithoutExtension = sanitizeDesiredFileName(desiredNameWithoutExtension) var resolvedName: String? = sanitizedNameWithoutExtension var replacementFile: File? = null - val extension = extensionFor(mimeType) + val extension = extensionFor(mimeType, defaultExtension) val targetFile = File(dir, "$sanitizedNameWithoutExtension$extension") when (conflictStrategy) { NameConflictStrategy.RENAME -> { 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 a7dbecf71..7aca99e80 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 @@ -557,6 +557,7 @@ class MediaStoreImageProvider : ImageProvider() { toBin: Boolean, ): FieldMap { val sourcePath = sourceFile?.path + val sourceExtension = sourceFile?.extension val sourceDir = sourceFile?.parent?.let { ensureTrailingSeparator(it) } if (sourceDir == targetDir && !(copy && nameConflictStrategy == NameConflictStrategy.RENAME)) { // nothing to do unless it's a renamed copy @@ -569,6 +570,7 @@ class MediaStoreImageProvider : ImageProvider() { dir = targetDir, desiredNameWithoutExtension = desiredNameWithoutExtension, mimeType = mimeType, + defaultExtension = sourceExtension, conflictStrategy = nameConflictStrategy, ) val targetNameWithoutExtension = resolution.nameWithoutExtension ?: return skippedFieldMap @@ -580,6 +582,7 @@ class MediaStoreImageProvider : ImageProvider() { targetDir = targetDir, targetDirDocFile = targetDirDocFile, targetNameWithoutExtension = targetNameWithoutExtension, + defaultExtension = sourceExtension, ) { output: OutputStream -> try { sourceDocFile.copyTo(output) @@ -615,12 +618,13 @@ class MediaStoreImageProvider : ImageProvider() { targetDir: String, targetDirDocFile: DocumentFileCompat?, targetNameWithoutExtension: String, + defaultExtension: String?, write: (OutputStream) -> Unit, ): String { if (StorageUtils.isInVault(activity, targetDir)) { return insertByFile( targetDir = targetDir, - targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", + targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}", write = write, ) } @@ -630,7 +634,7 @@ class MediaStoreImageProvider : ImageProvider() { return insertByMediaStore( activity = activity, targetDir = targetDir, - targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType)}", + targetFileName = "$targetNameWithoutExtension${extensionFor(mimeType, defaultExtension)}", write = write, ) } @@ -642,6 +646,7 @@ class MediaStoreImageProvider : ImageProvider() { targetDir = targetDir, targetDirDocFile = targetDirDocFile, targetNameWithoutExtension = targetNameWithoutExtension, + defaultExtension = defaultExtension, write = write, ) } @@ -700,6 +705,7 @@ class MediaStoreImageProvider : ImageProvider() { targetDir: String, targetDirDocFile: DocumentFileCompat?, targetNameWithoutExtension: String, + defaultExtension: String?, write: (OutputStream) -> Unit, ): String { targetDirDocFile ?: throw Exception("failed to get tree doc for directory at path=$targetDir") @@ -708,9 +714,22 @@ class MediaStoreImageProvider : ImageProvider() { // 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 targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) - val targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) - // TODO TLAD [missing extension] check whether targetDocFile.name has a valid extension + var targetTreeFile = targetDirDocFile.createFile(mimeType, targetNameWithoutExtension) + var targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) + + // providing a display name and a MIME type does not guarantee + // that the created document will be backed by a file with a valid media extension, + // but having an extension is essential for media detection by Android, + // so we retry with a display name that includes the extension + if ((targetDocFile.extension == null || targetDocFile.extension.isEmpty() || targetDocFile.extension == "bin") && defaultExtension != null) { + if (targetDocFile.exists()) { + targetDocFile.delete() + } + + val extension = if (defaultExtension.startsWith(".")) defaultExtension else ".$defaultExtension" + targetTreeFile = targetDirDocFile.createFile(mimeType, "$targetNameWithoutExtension$extension") + targetDocFile = DocumentFileCompat.fromSingleUri(activity, targetTreeFile.uri) + } try { targetDocFile.openOutputStream().use(write) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 8d9d53f1a..1e6ae58cd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -163,13 +163,24 @@ object MimeTypes { // among other refs: // - https://android.googlesource.com/platform/external/mime-support/+/refs/heads/master/mime.types - fun extensionFor(mimeType: String): String? = when (mimeType) { + fun extensionFor(mimeType: String, defaultExtension: String?): String = when (mimeType) { AVI, AVI_VND -> ".avi" + DNG, DNG_ADOBE -> ".dng" HEIC, HEIF -> ".heif" MP2T, MP2TS -> ".m2ts" PSD_VND, PSD_X -> ".psd" - // TODO TLAD [missing extension] check whether to define more manual mapping and raise exception on miss - else -> MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType)?.let { ".$it" } + else -> { + val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: defaultExtension + if (ext != null) { + // fallback to provided extension when available, + // typically the original file extension when moving/renaming + if (ext.startsWith(".")) ext else ".$ext" + } else { + // fallback to generic extensions, + // as incorrect file extensions are better than none for media detection + if (isVideo(mimeType)) ".mp4" else ".jpg" + } + } } val TIFF_EXTENSION_PATTERN = Regex(".*\\.tiff?", RegexOption.IGNORE_CASE)