diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index f1b8675b6..a3c0ca863 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -15,7 +15,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: beta
- flutter-version: '2.1.0-12.2.pre'
+ flutter-version: '2.2.0-10.1.pre'
- name: Clone the repository.
uses: actions/checkout@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7f17426e2..cc38288d9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: beta
- flutter-version: '2.1.0-12.2.pre'
+ flutter-version: '2.2.0-10.1.pre'
# Workaround for this Android Gradle Plugin issue (supposedly fixed in AGP 4.1):
# https://issuetracker.google.com/issues/144111441
@@ -50,8 +50,8 @@ jobs:
echo "${{ secrets.KEY_JKS }}" > release.keystore.asc
gpg -d --passphrase "${{ secrets.KEY_JKS_PASSPHRASE }}" --batch release.keystore.asc > $AVES_STORE_FILE
rm release.keystore.asc
- flutter build apk --bundle-sksl-path shaders_2.1.0-12.2.pre.sksl.json
- flutter build appbundle --bundle-sksl-path shaders_2.1.0-12.2.pre.sksl.json
+ flutter build apk --bundle-sksl-path shaders_2.2.0-10.1.pre.sksl.json
+ flutter build appbundle --bundle-sksl-path shaders_2.2.0-10.1.pre.sksl.json
rm $AVES_STORE_FILE
env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93f8d47c9..f8dceea69 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.4.1] - 2021-04-29
+### Added
+- Motion photo support
+- Viewer: play videos in multi-track HEIC
+- Handle share intent
+
+### Changed
+- Upgraded Flutter to beta v2.2.0-10.1.pre
+
+### Fixed
+- fixed crash when cataloguing large MP4/PSD
+- prevent videos playing in the background when quickly switching entries
+
## [v1.4.0] - 2021-04-16
### Added
- Viewer: support for videos with EAC3/FLAC/OPUS audio
diff --git a/android/app/libs/fijkplayer-full-release.aar b/android/app/libs/fijkplayer-full-release.aar
index 72547c9dc..d325ae6f0 100644
Binary files a/android/app/libs/fijkplayer-full-release.aar and b/android/app/libs/fijkplayer-full-release.aar differ
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index bde50c227..1aa2beace 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -62,6 +62,14 @@
+
+
+
+
+
+
+
+
@@ -69,6 +77,7 @@
+
@@ -88,6 +97,7 @@
+
@@ -96,6 +106,7 @@
+
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
index a29d60cf7..e41bb1d0c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -4,6 +4,7 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
+import android.os.Parcelable
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.content.pm.ShortcutInfoCompat
@@ -12,6 +13,7 @@ import androidx.core.graphics.drawable.IconCompat
import app.loup.streams_channel.StreamsChannel
import deckers.thibault.aves.channel.calls.*
import deckers.thibault.aves.channel.streams.*
+import deckers.thibault.aves.model.provider.MediaStoreImageProvider
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.PermissionManager
import io.flutter.embedding.android.FlutterActivity
@@ -33,6 +35,7 @@ class MainActivity : FlutterActivity() {
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
MethodChannel(messenger, AppShortcutHandler.CHANNEL).setMethodCallHandler(AppShortcutHandler(this))
MethodChannel(messenger, DebugHandler.CHANNEL).setMethodCallHandler(DebugHandler(this))
+ MethodChannel(messenger, EmbeddedDataHandler.CHANNEL).setMethodCallHandler(EmbeddedDataHandler(this))
MethodChannel(messenger, ImageFileHandler.CHANNEL).setMethodCallHandler(ImageFileHandler(this))
MethodChannel(messenger, GeocodingHandler.CHANNEL).setMethodCallHandler(GeocodingHandler(this))
MethodChannel(messenger, MediaStoreHandler.CHANNEL).setMethodCallHandler(MediaStoreHandler(this))
@@ -83,21 +86,29 @@ class MainActivity : FlutterActivity() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- if (requestCode == PermissionManager.VOLUME_ACCESS_REQUEST_CODE) {
- val treeUri = data?.data
- if (resultCode != RESULT_OK || treeUri == null) {
- PermissionManager.onPermissionResult(requestCode, null)
- return
+ when (requestCode) {
+ VOLUME_ACCESS_REQUEST -> {
+ val treeUri = data?.data
+ if (resultCode != RESULT_OK || treeUri == null) {
+ PermissionManager.onPermissionResult(requestCode, null)
+ return
+ }
+
+ // save access permissions across reboots
+ val takeFlags = (data.flags
+ and (Intent.FLAG_GRANT_READ_URI_PERMISSION
+ or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
+ contentResolver.takePersistableUriPermission(treeUri, takeFlags)
+
+ // resume pending action
+ PermissionManager.onPermissionResult(requestCode, treeUri)
+ }
+ DELETE_PERMISSION_REQUEST -> {
+ // delete permission may be requested on Android 10+ only
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
+ }
}
-
- // save access permissions across reboots
- val takeFlags = (data.flags
- and (Intent.FLAG_GRANT_READ_URI_PERMISSION
- or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
- contentResolver.takePersistableUriPermission(treeUri, takeFlags)
-
- // resume pending action
- PermissionManager.onPermissionResult(requestCode, treeUri)
}
}
@@ -111,8 +122,8 @@ class MainActivity : FlutterActivity() {
)
}
}
- Intent.ACTION_VIEW, "com.android.camera.action.REVIEW" -> {
- intent.data?.let { uri ->
+ Intent.ACTION_VIEW, Intent.ACTION_SEND, "com.android.camera.action.REVIEW" -> {
+ (intent.data ?: (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri))?.let { uri ->
return hashMapOf(
"action" to "view",
"uri" to uri.toString(),
@@ -171,7 +182,9 @@ class MainActivity : FlutterActivity() {
}
companion object {
- private val LOG_TAG = LogUtils.createTag(MainActivity::class.java)
+ private val LOG_TAG = LogUtils.createTag()
const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
+ const val VOLUME_ACCESS_REQUEST = 1
+ const val DELETE_PERMISSION_REQUEST = 2
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
index 9df5b08c6..f3c8e551c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt
@@ -12,6 +12,7 @@ import com.bumptech.glide.Glide
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.request.RequestOptions
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
@@ -30,7 +31,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
- "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) }
+ "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getAppIcon) }
"edit" -> {
val title = call.argument("title")
val uri = call.argument("uri")?.let { Uri.parse(it) }
@@ -109,7 +110,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
result.success(ArrayList(packages.values))
}
- private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
+ private suspend fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
val packageName = call.argument("packageName")
val sizeDip = call.argument("sizeDip")
if (packageName == null || sizeDip == null) {
@@ -254,8 +255,8 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
return when (uri.scheme?.toLowerCase(Locale.ROOT)) {
ContentResolver.SCHEME_FILE -> {
uri.path?.let { path ->
- val applicationId = context.applicationContext.packageName
- FileProvider.getUriForFile(context, "$applicationId.fileprovider", File(path))
+ val authority = "${context.applicationContext.packageName}.fileprovider"
+ FileProvider.getUriForFile(context, authority, File(path))
}
}
else -> uri
@@ -263,7 +264,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
companion object {
- private val LOG_TAG = LogUtils.createTag(AppAdapterHandler::class.java)
+ private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/app"
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
index 70e7a46f5..be5cbdb78 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/DebugHandler.kt
@@ -305,7 +305,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
)
companion object {
- private val LOG_TAG = LogUtils.createTag(DebugHandler::class.java)
+ private val LOG_TAG = LogUtils.createTag()
const val CHANNEL = "deckers.thibault/aves/debug"
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
new file mode 100644
index 000000000..534f5c520
--- /dev/null
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/EmbeddedDataHandler.kt
@@ -0,0 +1,237 @@
+package deckers.thibault.aves.channel.calls
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import androidx.core.content.FileProvider
+import androidx.exifinterface.media.ExifInterface
+import com.adobe.internal.xmp.XMPException
+import com.adobe.internal.xmp.XMPUtils
+import com.bumptech.glide.load.resource.bitmap.TransformationUtils
+import com.drew.imaging.ImageMetadataReader
+import com.drew.metadata.file.FileTypeDirectory
+import com.drew.metadata.xmp.XmpDirectory
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
+import deckers.thibault.aves.metadata.Metadata
+import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString
+import deckers.thibault.aves.metadata.MultiPage
+import deckers.thibault.aves.metadata.XMP
+import deckers.thibault.aves.model.FieldMap
+import deckers.thibault.aves.model.provider.ContentImageProvider
+import deckers.thibault.aves.model.provider.ImageProvider
+import deckers.thibault.aves.utils.BitmapUtils
+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.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.isVideo
+import deckers.thibault.aves.utils.StorageUtils
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import java.io.File
+import java.io.InputStream
+import java.util.*
+
+class EmbeddedDataHandler(private val context: Context) : MethodCallHandler {
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getExifThumbnails) }
+ "extractMotionPhotoVideo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractMotionPhotoVideo) }
+ "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
+ "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
+ else -> result.notImplemented()
+ }
+ }
+
+ private suspend fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
+ val mimeType = call.argument("mimeType")
+ val uri = call.argument("uri")?.let { Uri.parse(it) }
+ val sizeBytes = call.argument("sizeBytes")?.toLong()
+ if (mimeType == null || uri == null) {
+ result.error("getExifThumbnails-args", "failed because of missing arguments", null)
+ return
+ }
+
+ val thumbnails = ArrayList()
+ if (isSupportedByExifInterface(mimeType)) {
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ val exif = ExifInterface(input)
+ val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+ exif.thumbnailBitmap?.let { bitmap ->
+ TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let {
+ it.getBytes(canHaveAlpha = false, recycle = false)?.let { bytes -> thumbnails.add(bytes) }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ // ExifInterface initialization can fail with a RuntimeException
+ // caused by an internal MediaMetadataRetriever failure
+ }
+ }
+ result.success(thumbnails)
+ }
+
+ private fun extractMotionPhotoVideo(call: MethodCall, result: MethodChannel.Result) {
+ val mimeType = call.argument("mimeType")
+ val uri = call.argument("uri")?.let { Uri.parse(it) }
+ val sizeBytes = call.argument("sizeBytes")?.toLong()
+ val displayName = call.argument("displayName")
+ if (mimeType == null || uri == null || sizeBytes == null) {
+ result.error("extractMotionPhotoVideo-args", "failed because of missing arguments", null)
+ return
+ }
+
+ MultiPage.getMotionPhotoOffset(context, uri, mimeType, sizeBytes)?.let { videoSizeBytes ->
+ val videoStartOffset = sizeBytes - videoSizeBytes
+ StorageUtils.openInputStream(context, uri)?.let { input ->
+ input.skip(videoStartOffset)
+ copyEmbeddedBytes(result, MimeTypes.MP4, displayName, input)
+ }
+ return
+ }
+
+ result.error("extractMotionPhotoVideo-empty", "failed to extract video from motion photo at uri=$uri", null)
+ }
+
+ private fun extractVideoEmbeddedPicture(call: MethodCall, result: MethodChannel.Result) {
+ val uri = call.argument("uri")?.let { Uri.parse(it) }
+ val displayName = call.argument("displayName")
+ if (uri == null) {
+ result.error("extractVideoEmbeddedPicture-args", "failed because of missing arguments", null)
+ return
+ }
+
+ val retriever = StorageUtils.openMetadataRetriever(context, uri)
+ if (retriever != null) {
+ try {
+ retriever.embeddedPicture?.let { bytes ->
+ var embedMimeType: String? = null
+ bytes.inputStream().use { input ->
+ val metadata = ImageMetadataReader.readMetadata(input)
+ metadata.getFirstDirectoryOfType(FileTypeDirectory::class.java)?.let { dir ->
+ dir.getSafeString(FileTypeDirectory.TAG_DETECTED_FILE_MIME_TYPE) { embedMimeType = it }
+ }
+ }
+ embedMimeType?.let { mime ->
+ copyEmbeddedBytes(result, mime, displayName, bytes.inputStream())
+ return
+ }
+ }
+ } catch (e: Exception) {
+ result.error("extractVideoEmbeddedPicture-fetch", "failed to fetch picture for uri=$uri", e.message)
+ } finally {
+ // cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
+ retriever.release()
+ }
+ }
+ result.error("extractVideoEmbeddedPicture-empty", "failed to extract picture for uri=$uri", null)
+ }
+
+ private fun extractXmpDataProp(call: MethodCall, result: MethodChannel.Result) {
+ val mimeType = call.argument("mimeType")
+ val uri = call.argument("uri")?.let { Uri.parse(it) }
+ val sizeBytes = call.argument("sizeBytes")?.toLong()
+ val displayName = call.argument("displayName")
+ val dataPropPath = call.argument("propPath")
+ val embedMimeType = call.argument("propMimeType")
+ if (mimeType == null || uri == null || dataPropPath == null || embedMimeType == null) {
+ result.error("extractXmpDataProp-args", "failed because of missing arguments", null)
+ return
+ }
+
+ if (isSupportedByMetadataExtractor(mimeType)) {
+ try {
+ Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
+ val metadata = ImageMetadataReader.readMetadata(input)
+ // data can be large and stored in "Extended XMP",
+ // which is returned as a second XMP directory
+ val xmpDirs = metadata.getDirectoriesOfType(XmpDirectory::class.java)
+ try {
+ val pathParts = dataPropPath.split('/')
+
+ val embedBytes: ByteArray = if (pathParts.size == 1) {
+ val propName = pathParts[0]
+ val propNs = XMP.namespaceForPropPath(propName)
+ xmpDirs.map { it.xmpMeta.getPropertyBase64(propNs, propName) }.first { it != null }
+ } else {
+ val structName = pathParts[0]
+ val structNs = XMP.namespaceForPropPath(structName)
+ val fieldName = pathParts[1]
+ val fieldNs = XMP.namespaceForPropPath(fieldName)
+ xmpDirs.map { it.xmpMeta.getStructField(structNs, structName, fieldNs, fieldName) }.first { it != null }.let {
+ XMPUtils.decodeBase64(it.value)
+ }
+ }
+
+ copyEmbeddedBytes(result, embedMimeType, displayName, embedBytes.inputStream())
+ return
+ } catch (e: XMPException) {
+ result.error("extractXmpDataProp-xmp", "failed to read XMP directory for uri=$uri prop=$dataPropPath", e.message)
+ return
+ }
+ }
+ } catch (e: Exception) {
+ Log.w(LOG_TAG, "failed to extract file from XMP", e)
+ } catch (e: NoClassDefFoundError) {
+ Log.w(LOG_TAG, "failed to extract file from XMP", e)
+ }
+ }
+ result.error("extractXmpDataProp-empty", "failed to extract file from XMP uri=$uri prop=$dataPropPath", null)
+ }
+
+ private fun copyEmbeddedBytes(result: MethodChannel.Result, mimeType: String, displayName: String?, embeddedByteStream: InputStream) {
+ val extension = extensionFor(mimeType)
+ val file = File.createTempFile("aves", extension, context.cacheDir).apply {
+ deleteOnExit()
+ outputStream().use { output ->
+ embeddedByteStream.use { input ->
+ input.copyTo(output)
+ }
+ }
+ }
+ val authority = "${context.applicationContext.packageName}.fileprovider"
+ val uri = if (displayName != null) {
+ // add extension to ease type identification when sharing this content
+ val displayNameWithExtension = if (extension == null || displayName.endsWith(extension, ignoreCase = true)) {
+ displayName
+ } else {
+ "$displayName$extension"
+ }
+ FileProvider.getUriForFile(context, authority, file, displayNameWithExtension)
+ } else {
+ FileProvider.getUriForFile(context, authority, file)
+ }
+ val resultFields: FieldMap = hashMapOf(
+ "uri" to uri.toString(),
+ "mimeType" to mimeType,
+ )
+ if (isImage(mimeType) || isVideo(mimeType)) {
+ GlobalScope.launch(Dispatchers.IO) {
+ ContentImageProvider().fetchSingle(context, uri, mimeType, object : ImageProvider.ImageOpCallback {
+ override fun onSuccess(fields: FieldMap) {
+ resultFields.putAll(fields)
+ result.success(resultFields)
+ }
+
+ override fun onFailure(throwable: Throwable) = result.error("copyEmbeddedBytes-failure", "failed to get entry for uri=$uri mime=$mimeType", throwable.message)
+ })
+ }
+ } else {
+ result.success(resultFields)
+ }
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag()
+ const val CHANNEL = "deckers.thibault/aves/embedded"
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
index 67f6afec8..ff76d43b0 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt
@@ -3,7 +3,6 @@ package deckers.thibault.aves.channel.calls
import android.app.Activity
import android.graphics.Rect
import android.net.Uri
-import android.util.Size
import com.bumptech.glide.Glide
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
@@ -31,8 +30,8 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) }
- "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getThumbnail) }
- "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getRegion) }
+ "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) }
+ "getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) }
"rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) }
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
@@ -61,7 +60,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
})
}
- private fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
+ private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument("uri")
val mimeType = call.argument("mimeType")
val dateModifiedSecs = call.argument("dateModifiedSecs")?.toLong()
@@ -93,7 +92,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
).fetch()
}
- private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
+ private suspend fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val mimeType = call.argument("mimeType")
val pageId = call.argument("pageId")
@@ -185,7 +184,8 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val uri = (entryMap["uri"] as String?)?.let { Uri.parse(it) }
val path = entryMap["path"] as String?
val mimeType = entryMap["mimeType"] as String?
- if (uri == null || path == null || mimeType == null) {
+ val sizeBytes = (entryMap["sizeBytes"] as Number?)?.toLong()
+ if (uri == null || path == null || mimeType == null || sizeBytes == null) {
result.error("changeOrientation-args", "failed because entry fields are missing", null)
return
}
@@ -196,7 +196,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
return
}
- provider.changeOrientation(activity, path, uri, mimeType, op, object : ImageOpCallback {
+ provider.changeOrientation(activity, path, uri, mimeType, sizeBytes, 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)
})
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
index 0aa6ce5e1..573777615 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt
@@ -4,17 +4,13 @@ import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.database.Cursor
-import android.media.MediaExtractor
-import android.media.MediaFormat
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException
-import com.adobe.internal.xmp.XMPUtils
import com.adobe.internal.xmp.properties.XMPPropertyInfo
-import com.bumptech.glide.load.resource.bitmap.TransformationUtils
import com.drew.imaging.ImageMetadataReader
import com.drew.lang.Rational
import com.drew.metadata.Tag
@@ -51,12 +47,9 @@ import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeInt
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.getSafeString
+import deckers.thibault.aves.metadata.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
-import deckers.thibault.aves.model.provider.FileImageProvider
-import deckers.thibault.aves.model.provider.ImageProvider
-import deckers.thibault.aves.utils.BitmapUtils
-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
@@ -73,8 +66,6 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
-import org.beyka.tiffbitmapfactory.TiffBitmapFactory
-import java.io.File
import java.text.ParseException
import java.util.*
import kotlin.math.roundToLong
@@ -88,9 +79,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
"getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) }
"getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) }
- "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
- "extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
- "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
else -> result.notImplemented()
}
}
@@ -315,10 +303,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
// File type
for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) {
- // * `metadata-extractor` sometimes detect the the wrong mime type (e.g. `pef` file as `tiff`)
- // * the content resolver / media store sometimes report the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`)
- // * `context.getContentResolver().getType()` sometimes return incorrect value
- // * `MediaMetadataRetriever.setDataSource()` sometimes fail with `status = 0x80000000`
+ // * `metadata-extractor` sometimes detects the wrong mime type (e.g. `pef` file as `tiff`)
+ // * the content resolver / media store sometimes reports the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`)
+ // * `context.getContentResolver().getType()` sometimes returns an incorrect value
+ // * `MediaMetadataRetriever.setDataSource()` sometimes fails with `status = 0x80000000`
// * file extension is unreliable
// In the end, `metadata-extractor` is the most reliable, except for `tiff` (false positives, false negatives),
// in which case we trust the file extension
@@ -382,6 +370,11 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
if (xmpMeta.isPanorama()) {
flags = flags or MASK_IS_360
}
+
+ // identification of motion photo
+ if (xmpMeta.isMotionPhoto()) {
+ flags = flags or MASK_IS_MULTIPAGE
+ }
} catch (e: XMPException) {
Log.w(LOG_TAG, "failed to read XMP directory for uri=$uri", e)
}
@@ -471,7 +464,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
- if (mimeType == MimeTypes.TIFF && isMultiPageTiff(uri)) flags = flags or MASK_IS_MULTIPAGE
+ if (mimeType == MimeTypes.TIFF && MultiPage.isMultiPageTiff(context, uri)) flags = flags or MASK_IS_MULTIPAGE
metadataMap[KEY_FLAGS] = flags
}
@@ -591,67 +584,23 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
private fun getMultiPageInfo(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument("mimeType")
val uri = call.argument("uri")?.let { Uri.parse(it) }
- if (mimeType == null || uri == null) {
+ val sizeBytes = call.argument("sizeBytes")?.toLong()
+ if (mimeType == null || uri == null || sizeBytes == null) {
result.error("getMultiPageInfo-args", "failed because of missing arguments", null)
return
}
- val pages = ArrayList