mimetype/provider cleanup
This commit is contained in:
parent
c6a022ec4b
commit
b59450343f
9 changed files with 185 additions and 170 deletions
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue