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.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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue