mimetype/provider cleanup

This commit is contained in:
Thibault Deckers 2021-09-11 12:08:01 +09:00
parent c6a022ec4b
commit b59450343f
9 changed files with 185 additions and 170 deletions

View file

@ -20,10 +20,10 @@ import deckers.thibault.aves.metadata.Metadata
import deckers.thibault.aves.metadata.PixyMetaHelper
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.canReadWithPixyMeta
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isSupportedByPixyMeta
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId
@ -60,31 +60,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
}
private fun getPixyMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) {
result.error("getPixyMetadata-args", "failed because of missing arguments", null)
return
}
if (!isSupportedByPixyMeta(mimeType)) {
result.error("getPixyMetadata-unsupported", "PixyMeta does not support mimeType=$mimeType", null)
return
}
val metadataMap = HashMap<String, String>()
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
metadataMap.putAll(PixyMetaHelper.describe(input))
}
} catch (e: Exception) {
result.error("getPixyMetadata-exception", e.message, e.stackTraceToString())
return
}
result.success(metadataMap)
}
private fun getContextDirs(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
val dirs = hashMapOf(
"cacheDir" to context.cacheDir,
@ -206,7 +181,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
val metadataMap = HashMap<String, String?>()
if (isSupportedByExifInterface(mimeType, strict = false)) {
if (canReadWithExifInterface(mimeType, strict = false)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input)
@ -258,7 +233,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
}
val metadataMap = HashMap<String, String>()
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
@ -290,6 +265,28 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap)
}
private fun getPixyMetadata(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (mimeType == null || uri == null) {
result.error("getPixyMetadata-args", "failed because of missing arguments", null)
return
}
val metadataMap = HashMap<String, String>()
if (canReadWithPixyMeta(mimeType)) {
try {
StorageUtils.openInputStream(context, uri)?.use { input ->
metadataMap.putAll(PixyMetaHelper.describe(input))
}
} catch (e: Exception) {
result.error("getPixyMetadata-exception", e.message, e.stackTraceToString())
return
}
}
result.success(metadataMap)
}
private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) {

View file

@ -27,8 +27,8 @@ import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall
@ -62,7 +62,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
}
val thumbnails = ArrayList<ByteArray>()
if (isSupportedByExifInterface(mimeType)) {
if (canReadWithExifInterface(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@Suppress("BlockingMethodInNonBlockingContext")
@ -150,7 +150,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return
}
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)

View file

@ -202,17 +202,17 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
}
val op = if (clockwise) ExifOrientationOp.ROTATE_CW else ExifOrientationOp.ROTATE_CCW
changeOrientation(call, result, op)
editOrientation(call, result, op)
}
private fun flip(call: MethodCall, result: MethodChannel.Result) {
changeOrientation(call, result, ExifOrientationOp.FLIP)
editOrientation(call, result, ExifOrientationOp.FLIP)
}
private fun changeOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) {
private fun editOrientation(call: MethodCall, result: MethodChannel.Result, op: ExifOrientationOp) {
val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null) {
result.error("changeOrientation-args", "failed because of missing arguments", null)
result.error("editOrientation-args", "failed because of missing arguments", null)
return
}
@ -220,19 +220,19 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) {
result.error("changeOrientation-args", "failed because entry fields are missing", null)
result.error("editOrientation-args", "failed because entry fields are missing", null)
return
}
val provider = getProvider(uri)
if (provider == null) {
result.error("changeOrientation-provider", "failed to find provider for uri=$uri", null)
result.error("editOrientation-provider", "failed to find provider for uri=$uri", null)
return
}
provider.changeOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback {
provider.editOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
override fun onFailure(throwable: Throwable) = result.error("changeOrientation-failure", "failed to change orientation", throwable.message)
override fun onFailure(throwable: Throwable) = result.error("editOrientation-failure", "failed to change orientation", throwable.message)
})
}

View file

@ -54,8 +54,8 @@ import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
import deckers.thibault.aves.utils.StorageUtils
@ -97,7 +97,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var foundExif = false
var foundXmp = false
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
@ -225,7 +225,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType)) {
if (!foundExif && canReadWithExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@ -337,7 +337,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
var foundExif = false
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
@ -480,7 +480,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType)) {
if (!foundExif && canReadWithExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@ -584,7 +584,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
var foundExif = false
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)
@ -603,7 +603,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
if (!foundExif && isSupportedByExifInterface(mimeType)) {
if (!foundExif && canReadWithExifInterface(mimeType)) {
// fallback to read EXIF via ExifInterface
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@ -654,7 +654,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
return
}
if (isSupportedByMetadataExtractor(mimeType)) {
if (canReadWithMetadataExtractor(mimeType)) {
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val metadata = ImageMetadataReader.readMetadata(input)

View file

@ -17,7 +17,7 @@ import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter
import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.StorageUtils
@ -96,7 +96,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
if (isVideo(mimeType)) {
streamVideoByGlide(uri, mimeType)
} else if (!isSupportedByFlutter(mimeType, rotationDegrees, isFlipped)) {
} else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) {
// decode exotic format on platform side, then encode it in portable format for Flutter
streamImageByGlide(uri, pageId, mimeType, rotationDegrees, isFlipped)
} else {

View file

@ -150,7 +150,7 @@ class SourceEntry {
// finds: width, height, orientation, date, duration
private fun fillByMetadataExtractor(context: Context) {
// skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions
if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)
if (!MimeTypes.canReadWithMetadataExtractor(sourceMimeType)
|| MimeTypes.isRaw(sourceMimeType)
) return
@ -204,7 +204,7 @@ class SourceEntry {
// finds: width, height, orientation, date
private fun fillByExifInterface(context: Context) {
if (!MimeTypes.isSupportedByExifInterface(sourceMimeType)) return
if (!MimeTypes.canReadWithExifInterface(sourceMimeType)) return
try {
Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input ->

View file

@ -1,13 +1,10 @@
package deckers.thibault.aves.model.provider
import android.app.Activity
import android.content.ContentUris
import android.content.Context
import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide
@ -28,18 +25,17 @@ import deckers.thibault.aves.model.AvesEntry
import deckers.thibault.aves.model.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.*
import deckers.thibault.aves.utils.MimeTypes.canEditExif
import deckers.thibault.aves.utils.MimeTypes.canEditXmp
import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByPixyMeta
import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
import deckers.thibault.aves.utils.UriUtils.tryParseId
import java.io.*
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
@ -54,6 +50,18 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException())
}
open fun scanPostExifEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
throw UnsupportedOperationException()
}
open fun scanObsoletePath(context: Context, path: String, mimeType: String) {
throw UnsupportedOperationException()
}
open suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap {
throw UnsupportedOperationException()
}
suspend fun exportMultiple(
context: Context,
imageExportMimeType: String,
@ -324,7 +332,7 @@ abstract class ImageProvider {
return
}
MediaScannerConnection.scanFile(context, arrayOf(oldPath), arrayOf(mimeType), null)
scanObsoletePath(context, oldPath, mimeType)
try {
callback.onSuccess(scanNewPath(context, newFile.path, mimeType))
} catch (e: Exception) {
@ -332,23 +340,6 @@ abstract class ImageProvider {
}
}
// support for writing EXIF
// as of androidx.exifinterface:exifinterface:1.3.3
private fun canEditExif(mimeType: String): Boolean {
return when (mimeType) {
MimeTypes.DNG,
MimeTypes.JPEG,
MimeTypes.PNG,
MimeTypes.WEBP -> true
else -> false
}
}
// support for writing XMP
private fun canEditXmp(mimeType: String): Boolean {
return isSupportedByPixyMeta(mimeType)
}
private fun editExif(
context: Context,
path: String,
@ -524,28 +515,7 @@ abstract class ImageProvider {
}
}
private fun scanPostExifEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, _ ->
val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.SIZE,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(e)
return@scanFile
}
callback.onSuccess(newFields)
}
}
fun changeOrientation(context: Context, path: String, uri: Uri, mimeType: String, op: ExifOrientationOp, callback: ImageOpCallback) {
fun editOrientation(context: Context, path: String, uri: Uri, mimeType: String, op: ExifOrientationOp, callback: ImageOpCallback) {
val newFields = HashMap<String, Any?>()
val success = editExif(context, path, uri, mimeType, callback) { exif ->
@ -666,65 +636,6 @@ abstract class ImageProvider {
}
}
protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
suspendCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
fun scanUri(uri: Uri?): FieldMap? {
uri ?: return null
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.TITLE,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val newFields = HashMap<String, Any?>()
newFields["uri"] = uri.toString()
newFields["contentId"] = uri.tryParseId()
newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
cursor.close()
return newFields
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
}
return null
}
if (newUri == null) {
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
return@scanFile
}
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields)
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
}
}
}
interface ImageOpCallback {
fun onSuccess(fields: FieldMap)
fun onFailure(throwable: Throwable)

View file

@ -5,6 +5,7 @@ import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.ContentUris
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
@ -27,6 +28,9 @@ import java.io.File
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() {
fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) {
@ -374,6 +378,90 @@ class MediaStoreImageProvider : ImageProvider() {
}
}
override fun scanPostExifEdit(context: Context, path: String, uri: Uri, mimeType: String, newFields: HashMap<String, Any?>, callback: ImageOpCallback) {
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, _ ->
val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.SIZE,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.SIZE).let { if (it != -1) newFields["sizeBytes"] = cursor.getLong(it) }
cursor.close()
}
} catch (e: Exception) {
callback.onFailure(e)
return@scanFile
}
callback.onSuccess(newFields)
}
}
override fun scanObsoletePath(context: Context, path: String, mimeType: String) {
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType), null)
}
override suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
suspendCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
fun scanUri(uri: Uri?): FieldMap? {
uri ?: return null
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf(
MediaStore.MediaColumns.DATE_MODIFIED,
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.TITLE,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val newFields = HashMap<String, Any?>()
newFields["uri"] = uri.toString()
newFields["contentId"] = uri.tryParseId()
newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) }
cursor.close()
return newFields
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
}
return null
}
if (newUri == null) {
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
return@scanFile
}
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields)
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
}
}
}
companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>()

View file

@ -64,28 +64,47 @@ object MimeTypes {
else -> false
}
// as of Flutter v1.22.0, with additional custom handling for SVG
fun isSupportedByFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
JPEG, GIF, WEBP, BMP, WBMP, ICO, SVG -> true
// as of Flutter v1.22.0
fun canDecodeWithFlutter(mimeType: String, rotationDegrees: Int?, isFlipped: Boolean?) = when (mimeType) {
JPEG, GIF, WEBP, BMP, WBMP, ICO -> true
PNG -> rotationDegrees ?: 0 == 0 && !(isFlipped ?: false)
else -> false
}
// as of `metadata-extractor` v2.14.0
fun isSupportedByMetadataExtractor(mimeType: String) = when (mimeType) {
fun canReadWithMetadataExtractor(mimeType: String) = when (mimeType) {
DJVU, WBMP, MKV, MP2T, MP2TS, OGV, WEBM -> false
else -> true
}
// as of `ExifInterface` v1.3.1, `isSupportedMimeType` reports
// no support for TIFF images, but it can actually open them (maybe other formats too)
fun isSupportedByExifInterface(mimeType: String, strict: Boolean = true) = ExifInterface.isSupportedMimeType(mimeType) || !strict
fun canReadWithExifInterface(mimeType: String, strict: Boolean = true) = ExifInterface.isSupportedMimeType(mimeType) || !strict
fun isSupportedByPixyMeta(mimeType: String) = when (mimeType) {
// as of latest PixyMeta
fun canReadWithPixyMeta(mimeType: String) = when (mimeType) {
JPEG, TIFF, PNG, GIF, BMP -> true
else -> false
}
// as of androidx.exifinterface:exifinterface:1.3.3
fun canEditExif(mimeType: String) = when (mimeType) {
DNG,
JPEG,
PNG,
WEBP -> true
else -> false
}
// as of latest PixyMeta
fun canEditXmp(mimeType: String) = canReadWithPixyMeta(mimeType)
// as of latest PixyMeta
fun canRemoveMetadata(mimeType: String) = when (mimeType) {
JPEG, TIFF -> true
else -> false
}
// Glide automatically applies EXIF orientation when decoding images of known formats
// but we need to rotate the decoded bitmap for the other formats
// maybe related to ExifInterface version used by Glide: