diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index ff76d43b0..b7decb934 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -29,7 +29,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { - "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) } + "getEntry" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEntry) } "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) } "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) } "rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) } @@ -40,7 +40,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { } } - private suspend fun getEntry(call: MethodCall, result: MethodChannel.Result) { + private fun getEntry(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") // MIME type is optional val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 98f80393f..68811e69b 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -127,16 +127,7 @@ object Metadata { Log.d(LOG_TAG, "use a preview for uri=$uri mimeType=$mimeType size=$sizeBytes") var previewFile = previewFiles[uri] if (previewFile == null) { - previewFile = File.createTempFile("aves", null, context.cacheDir).apply { - deleteOnExit() - outputStream().use { output -> - StorageUtils.openInputStream(context, uri)?.use { input -> - val b = ByteArray(previewSize) - input.read(b, 0, previewSize) - output.write(b) - } - } - } + previewFile = createPreviewFile(context, uri) previewFiles[uri] = previewFile } Uri.fromFile(previewFile) @@ -147,6 +138,19 @@ object Metadata { } } + fun createPreviewFile(context: Context, uri: Uri): File { + return File.createTempFile("aves", null, context.cacheDir).apply { + deleteOnExit() + outputStream().use { output -> + StorageUtils.openInputStream(context, uri)?.use { input -> + val b = ByteArray(previewSize) + input.read(b, 0, previewSize) + output.write(b) + } + } + } + } + fun openSafeInputStream(context: Context, uri: Uri, mimeType: String, sizeBytes: Long?): InputStream? { val safeUri = getSafeUri(context, uri, mimeType, sizeBytes) return StorageUtils.openInputStream(context, safeUri) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 0f85cc768..03ac091b8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -4,10 +4,44 @@ import android.content.Context import android.net.Uri import android.provider.MediaStore import android.provider.OpenableColumns +import android.util.Log +import com.drew.imaging.ImageMetadataReader +import com.drew.metadata.file.FileTypeDirectory +import deckers.thibault.aves.metadata.Metadata +import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.model.SourceEntry +import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes +import deckers.thibault.aves.utils.StorageUtils internal class ContentImageProvider : ImageProvider() { - override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { + // source MIME type may be incorrect, so we get a second opinion if possible + var extractorMimeType: String? = null + try { + val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri)) + StorageUtils.openInputStream(context, safeUri)?.use { input -> + val metadata = ImageMetadataReader.readMetadata(input) + for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { + // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives) + // cf https://github.com/drewnoakes/metadata-extractor/issues/296 + dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { + if (it != MimeTypes.TIFF) { + extractorMimeType = it + if (extractorMimeType != sourceMimeType) { + Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri") + } + } + } + } + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) + } catch (e: NoClassDefFoundError) { + Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e) + } + + val mimeType = extractorMimeType ?: sourceMimeType if (mimeType == null) { callback.onFailure(Exception("MIME type is null for uri=$uri")) return @@ -39,6 +73,8 @@ internal class ContentImageProvider : ImageProvider() { } companion object { + private val LOG_TAG = LogUtils.createTag() + @Suppress("DEPRECATION") const val PATH = MediaStore.MediaColumns.DATA