avoid double extraction for embedded media

This commit is contained in:
Thibault Deckers 2023-12-21 13:18:51 +01:00
parent 5c8cd81ff7
commit c95b381fad
9 changed files with 70 additions and 36 deletions

View file

@ -20,8 +20,8 @@ import deckers.thibault.aves.metadata.XMP.getSafeStructField
import deckers.thibault.aves.metadata.XMPPropName import deckers.thibault.aves.metadata.XMPPropName
import deckers.thibault.aves.metadata.metadataextractor.Helper import deckers.thibault.aves.metadata.metadataextractor.Helper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ContentImageProvider
import deckers.thibault.aves.model.provider.ImageProvider import deckers.thibault.aves.model.provider.ImageProvider
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.utils.BitmapUtils import deckers.thibault.aves.utils.BitmapUtils
import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.FileUtils.transferFrom import deckers.thibault.aves.utils.FileUtils.transferFrom
@ -341,8 +341,14 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
"mimeType" to mimeType, "mimeType" to mimeType,
) )
if (isImage(mimeType) || isVideo(mimeType)) { if (isImage(mimeType) || isVideo(mimeType)) {
val provider = getProvider(context, uri)
if (provider == null) {
result.error("copyEmbeddedBytes-provider", "failed to find provider for uri=$uri", null)
return
}
ioScope.launch { ioScope.launch {
ContentImageProvider().fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback { provider.fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback {
override fun onSuccess(fields: FieldMap) { override fun onSuccess(fields: FieldMap) {
resultFields.putAll(fields) resultFields.putAll(fields)
result.success(resultFields) result.success(resultFields)

View file

@ -55,7 +55,7 @@ class MediaEditHandler(private val contextWrapper: ContextWrapper) : MethodCallH
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("captureFrame-provider", "failed to find provider for uri=$uri", null) result.error("captureFrame-provider", "failed to find provider for uri=$uri", null)
return return

View file

@ -34,7 +34,7 @@ class MediaFetchObjectHandler(private val context: Context) : MethodCallHandler
return return
} }
val provider = getProvider(uri) val provider = getProvider(context, uri)
if (provider == null) { if (provider == null) {
result.error("getEntry-provider", "failed to find provider for uri=$uri", null) result.error("getEntry-provider", "failed to find provider for uri=$uri", null)
return return

View file

@ -62,7 +62,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editOrientation-provider", "failed to find provider for uri=$uri", null) result.error("editOrientation-provider", "failed to find provider for uri=$uri", null)
return return
@ -90,7 +90,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editDate-provider", "failed to find provider for uri=$uri", null) result.error("editDate-provider", "failed to find provider for uri=$uri", null)
return return
@ -117,7 +117,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("editMetadata-provider", "failed to find provider for uri=$uri", null) result.error("editMetadata-provider", "failed to find provider for uri=$uri", null)
return return
@ -142,7 +142,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("removeTrailerVideo-provider", "failed to find provider for uri=$uri", null) result.error("removeTrailerVideo-provider", "failed to find provider for uri=$uri", null)
return return
@ -168,7 +168,7 @@ class MetadataEditHandler(private val contextWrapper: ContextWrapper) : MethodCa
return return
} }
val provider = getProvider(uri) val provider = getProvider(contextWrapper, uri)
if (provider == null) { if (provider == null) {
result.error("removeTypes-provider", "failed to find provider for uri=$uri", null) result.error("removeTypes-provider", "failed to find provider for uri=$uri", null)
return return

View file

@ -107,7 +107,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
result["skipped"] = true result["skipped"] = true
} else { } else {
result["success"] = false result["success"] = false
getProvider(uri)?.let { provider -> getProvider(activity, uri)?.let { provider ->
try { try {
provider.delete(activity, uri, path, mimeType) provider.delete(activity, uri, path, mimeType)
result["success"] = true result["success"] = true
@ -142,7 +142,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
// assume same provider for all entries // assume same provider for all entries
val firstEntry = entryMapList.first() val firstEntry = entryMapList.first()
val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(it) } val provider = (firstEntry["uri"] as String?)?.let { Uri.parse(it) }?.let { getProvider(activity, it) }
if (provider == null) { if (provider == null) {
error("convert-provider", "failed to find provider for entry=$firstEntry", null) error("convert-provider", "failed to find provider for entry=$firstEntry", null)
return return
@ -231,7 +231,7 @@ class ImageOpStreamHandler(private val activity: FragmentActivity, private val a
entriesToNewName[AvesEntry(rawEntry)] = newName entriesToNewName[AvesEntry(rawEntry)] = newName
} }
val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(kv.key.uri) } val byProvider = entriesToNewName.entries.groupBy { kv -> getProvider(activity, kv.key.uri) }
for ((provider, entryList) in byProvider) { for ((provider, entryList) in byProvider) {
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=${entryList.firstOrNull()}", null)

View file

@ -10,7 +10,9 @@ import java.io.File
import java.io.InputStream import java.io.InputStream
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.regex.Pattern import java.util.regex.Pattern
object Metadata { object Metadata {

View file

@ -0,0 +1,18 @@
package deckers.thibault.aves.model.provider
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import java.util.Locale
class AvesEmbeddedMediaProvider : UnknownContentProvider() {
override val reliableProviderMimeType: Boolean
get() = true
companion object {
fun provides(context: Context, uri: Uri): Boolean {
if (uri.scheme?.lowercase(Locale.ROOT) != ContentResolver.SCHEME_CONTENT) return false
return uri.authority == "${context.applicationContext.packageName}.file_provider"
}
}
}

View file

@ -1,20 +1,24 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.net.Uri import android.net.Uri
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import java.util.* import java.util.Locale
object ImageProviderFactory { object ImageProviderFactory {
fun getProvider(uri: Uri): ImageProvider? { fun getProvider(context: Context, uri: Uri): ImageProvider? {
return when (uri.scheme?.lowercase(Locale.ROOT)) { return when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_CONTENT -> { ContentResolver.SCHEME_CONTENT -> {
if (StorageUtils.isMediaStoreContentUri(uri)) { if (StorageUtils.isMediaStoreContentUri(uri)) {
MediaStoreImageProvider() MediaStoreImageProvider()
} else if (AvesEmbeddedMediaProvider.provides(context, uri)) {
AvesEmbeddedMediaProvider()
} else { } else {
ContentImageProvider() UnknownContentProvider()
} }
} }
ContentResolver.SCHEME_FILE -> FileImageProvider() ContentResolver.SCHEME_FILE -> FileImageProvider()
else -> null else -> null
} }

View file

@ -13,31 +13,35 @@ import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
internal class ContentImageProvider : ImageProvider() { open class UnknownContentProvider : ImageProvider() {
open val reliableProviderMimeType: Boolean
get() = false
override fun fetchSingle(context: Context, uri: Uri, sourceMimeType: 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 mimeType = sourceMimeType
var extractorMimeType: String? = null if (sourceMimeType == null || !reliableProviderMimeType) {
try { // source MIME type may be incorrect, so we get a second opinion if possible
val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri)) try {
StorageUtils.openInputStream(context, safeUri)?.use { input -> val safeUri = Uri.fromFile(Metadata.createPreviewFile(context, uri))
// `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives) StorageUtils.openInputStream(context, safeUri)?.use { input ->
// cf https://github.com/drewnoakes/metadata-extractor/issues/296 // `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives)
Helper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let { // cf https://github.com/drewnoakes/metadata-extractor/issues/296
extractorMimeType = it Helper.readMimeType(input)?.takeIf { it != MimeTypes.TIFF }?.let {
if (extractorMimeType != sourceMimeType) { if (it != sourceMimeType) {
Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $extractorMimeType for uri=$uri") Log.d(LOG_TAG, "source MIME type is $sourceMimeType but extracted MIME type is $it for uri=$uri")
mimeType = it
}
} }
} }
} 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)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} }
} 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)
} catch (e: AssertionError) {
Log.w(LOG_TAG, "failed to get MIME type by metadata-extractor for uri=$uri", e)
} }
val mimeType = extractorMimeType ?: sourceMimeType
val fields: FieldMap = hashMapOf( val fields: FieldMap = hashMapOf(
"origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT, "origin" to SourceEntry.ORIGIN_UNKNOWN_CONTENT,
"uri" to uri.toString(), "uri" to uri.toString(),
@ -75,6 +79,6 @@ internal class ContentImageProvider : ImageProvider() {
} }
companion object { companion object {
private val LOG_TAG = LogUtils.createTag<ContentImageProvider>() private val LOG_TAG = LogUtils.createTag<UnknownContentProvider>()
} }
} }