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.metadata.PixyMetaHelper
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils 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.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.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId 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) { private fun getContextDirs(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
val dirs = hashMapOf( val dirs = hashMapOf(
"cacheDir" to context.cacheDir, "cacheDir" to context.cacheDir,
@ -206,7 +181,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
val metadataMap = HashMap<String, String?>() val metadataMap = HashMap<String, String?>()
if (isSupportedByExifInterface(mimeType, strict = false)) { if (canReadWithExifInterface(mimeType, strict = false)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
val exif = ExifInterface(input) val exif = ExifInterface(input)
@ -258,7 +233,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
} }
val metadataMap = HashMap<String, String>() val metadataMap = HashMap<String, String>()
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)
@ -290,6 +265,28 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
result.success(metadataMap) 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) { private fun getTiffStructure(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
if (uri == null) { 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
import deckers.thibault.aves.utils.MimeTypes.extensionFor import deckers.thibault.aves.utils.MimeTypes.extensionFor
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@ -62,7 +62,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
} }
val thumbnails = ArrayList<ByteArray>() val thumbnails = ArrayList<ByteArray>()
if (isSupportedByExifInterface(mimeType)) { if (canReadWithExifInterface(mimeType)) {
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
@ -150,7 +150,7 @@ class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
return return
} }
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)

View file

@ -202,17 +202,17 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
} }
val op = if (clockwise) ExifOrientationOp.ROTATE_CW else ExifOrientationOp.ROTATE_CCW 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) { 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") val entryMap = call.argument<FieldMap>("entry")
if (entryMap == null) { if (entryMap == null) {
result.error("changeOrientation-args", "failed because of missing arguments", null) result.error("editOrientation-args", "failed because of missing arguments", null)
return return
} }
@ -220,19 +220,19 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val path = entryMap["path"] as String? val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String? val mimeType = entryMap["mimeType"] as String?
if (uri == null || path == null || mimeType == null) { 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 return
} }
val provider = getProvider(uri) val provider = getProvider(uri)
if (provider == null) { 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 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 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
import deckers.thibault.aves.utils.MimeTypes.isHeic import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
@ -97,7 +97,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var foundExif = false var foundExif = false
var foundXmp = false var foundXmp = false
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)
@ -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 // fallback to read EXIF via ExifInterface
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> 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 flags = (metadataMap[KEY_FLAGS] ?: 0) as Int
var foundExif = false var foundExif = false
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)
@ -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 // fallback to read EXIF via ExifInterface
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@ -584,7 +584,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
} }
var foundExif = false var foundExif = false
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)
@ -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 // fallback to read EXIF via ExifInterface
try { try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input -> Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
@ -654,7 +654,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
return return
} }
if (isSupportedByMetadataExtractor(mimeType)) { if (canReadWithMetadataExtractor(mimeType)) {
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)

View file

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

View file

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

View file

@ -1,13 +1,10 @@
package deckers.thibault.aves.model.provider package deckers.thibault.aves.model.provider
import android.app.Activity import android.app.Activity
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore
import android.util.Log import android.util.Log
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import com.bumptech.glide.Glide 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.ExifOrientationOp
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.* 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.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.MimeTypes.isVideo
import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
import deckers.thibault.aves.utils.UriUtils.tryParseId import java.io.ByteArrayInputStream
import java.io.* import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.* import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
abstract class ImageProvider { abstract class ImageProvider {
open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) { open fun fetchSingle(context: Context, uri: Uri, sourceMimeType: String?, callback: ImageOpCallback) {
@ -54,6 +50,18 @@ abstract class ImageProvider {
callback.onFailure(UnsupportedOperationException()) 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( suspend fun exportMultiple(
context: Context, context: Context,
imageExportMimeType: String, imageExportMimeType: String,
@ -324,7 +332,7 @@ abstract class ImageProvider {
return return
} }
MediaScannerConnection.scanFile(context, arrayOf(oldPath), arrayOf(mimeType), null) scanObsoletePath(context, oldPath, mimeType)
try { try {
callback.onSuccess(scanNewPath(context, newFile.path, mimeType)) callback.onSuccess(scanNewPath(context, newFile.path, mimeType))
} catch (e: Exception) { } 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( private fun editExif(
context: Context, context: Context,
path: String, 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) { fun editOrientation(context: Context, path: String, uri: Uri, mimeType: String, op: ExifOrientationOp, 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) {
val newFields = HashMap<String, Any?>() val newFields = HashMap<String, Any?>()
val success = editExif(context, path, uri, mimeType, callback) { exif -> 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 { interface ImageOpCallback {
fun onSuccess(fields: FieldMap) fun onSuccess(fields: FieldMap)
fun onFailure(throwable: Throwable) fun onFailure(throwable: Throwable)

View file

@ -5,6 +5,7 @@ import android.app.Activity
import android.app.RecoverableSecurityException import android.app.RecoverableSecurityException
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
@ -27,6 +28,9 @@ import java.io.File
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class MediaStoreImageProvider : ImageProvider() { class MediaStoreImageProvider : ImageProvider() {
fun fetchAll(context: Context, knownEntries: Map<Int, Int?>, handleNewEntry: NewEntryHandler) { 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 { companion object {
private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>() private val LOG_TAG = LogUtils.createTag<MediaStoreImageProvider>()

View file

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