diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml
index 3b4fc72b1..555bbde2e 100644
--- a/.github/workflows/check.yml
+++ b/.github/workflows/check.yml
@@ -15,7 +15,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: stable
- flutter-version: '1.22.5'
+ flutter-version: '1.22.6'
- name: Clone the repository.
uses: actions/checkout@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e4bd20b5c..d35450f7f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
- uses: subosito/flutter-action@v1
with:
channel: stable
- flutter-version: '1.22.5'
+ flutter-version: '1.22.6'
# 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_1.22.5.sksl.json
- flutter build appbundle --bundle-sksl-path shaders_1.22.5.sksl.json
+ flutter build apk --bundle-sksl-path shaders_1.22.6.sksl.json
+ flutter build appbundle --bundle-sksl-path shaders_1.22.6.sksl.json
rm $AVES_STORE_FILE
env:
AVES_STORE_FILE: ${{ github.workspace }}/key.jks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a089b539..44e021228 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
+## [v1.3.3] - 2021-01-31
+### Added
+- Viewer: support for multi-track HEIF
+- Viewer: export image (including multipage TIFF/HEIF and images embedded in XMP)
+- Info: show owner app (Android Q and up)
+- listen to Media Store changes
+
+### Changed
+- upgraded Flutter to stable v1.22.6
+- check connectivity before using features that need it
+
+### Fixed
+- checkerboard background performance
+- deleting files that no longer exist but are still registered in the Media Store
+- insets handling on Android 11
+
## [v1.3.2] - 2021-01-17
### Added
Collection: identify multipage TIFF & multitrack HEIC/HEIF
diff --git a/README.md b/README.md
index 52ed6d473..34fbae115 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
## Features
-- support raster images: JPEG, GIF, PNG, HEIC (from Android Pie), WEBP, TIFF, BMP, WBMP, ICO
+- support raster images: JPEG, GIF, PNG, HEIC/HEIF (including multi-track, from Android Pie), WEBP, TIFF (including multi-page), BMP, WBMP, ICO
- support animated images: GIF, WEBP
- support raw images: ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW
- support vector images: SVG
@@ -36,10 +36,10 @@ Aves is a gallery and metadata explorer app. It is built for Android, with Flutt
| Model | Name | Android Version | API |
| ----------- | -------------------------- | --------------- | ---:|
-| SM-G970N | Samsung Galaxy S10e | 10 (Android10) | 29 |
+| SM-G981N | Samsung Galaxy S20 5G | 11 | 30 |
+| SM-G970N | Samsung Galaxy S10e | 10 (Q) | 29 |
| SM-P580 | Samsung Galaxy Tab A 10.1 | 8.1.0 (Oreo) | 27 |
| SM-G930S | Samsung Galaxy S7 | 8.0.0 (Oreo) | 26 |
-| E5823 | Sony Xperia Z5 Compact | 7.1.1 (Nougat) | 25 |
## Project Setup
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 7fd1f982e..e8cea44fa 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -98,15 +98,15 @@ repositories {
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
- implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
+ implementation 'androidx.core:core-ktx:1.5.0-beta01' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts
implementation 'androidx.exifinterface:exifinterface:1.3.2'
implementation 'com.commonsware.cwac:document:0.4.1'
implementation 'com.drewnoakes:metadata-extractor:2.15.0'
implementation 'com.github.deckerst:Android-TiffBitmapFactory:f87db4305d' // forked, built by JitPack
- implementation 'com.github.bumptech.glide:glide:4.11.0'
+ implementation 'com.github.bumptech.glide:glide:4.12.0'
kapt 'androidx.annotation:annotation:1.1.0'
- kapt 'com.github.bumptech.glide:compiler:4.11.0'
+ kapt 'com.github.bumptech.glide:compiler:4.12.0'
compileOnly rootProject.findProject(':streams_channel')
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1faac6cc8..66f30eaf6 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -40,7 +40,6 @@
-
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 6ee05d8d1..a6742b22c 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt
@@ -16,24 +16,18 @@ import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.PermissionManager
import io.flutter.embedding.android.FlutterActivity
import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity() {
- companion object {
- private val LOG_TAG = LogUtils.createTag(MainActivity::class.java)
- const val INTENT_CHANNEL = "deckers.thibault/aves/intent"
- const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
- }
-
- private val intentStreamHandler = IntentStreamHandler()
+ private lateinit var contentStreamHandler: ContentChangeStreamHandler
+ private lateinit var intentStreamHandler: IntentStreamHandler
private lateinit var intentDataMap: MutableMap
override fun onCreate(savedInstanceState: Bundle?) {
Log.i(LOG_TAG, "onCreate intent=$intent")
super.onCreate(savedInstanceState)
- intentDataMap = extractIntentData(intent)
-
val messenger = flutterEngine!!.dartExecutor.binaryMessenger
MethodChannel(messenger, AppAdapterHandler.CHANNEL).setMethodCallHandler(AppAdapterHandler(this))
@@ -48,59 +42,34 @@ class MainActivity : FlutterActivity() {
StreamsChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandlerFactory { args -> MediaStoreStreamHandler(this, args) }
StreamsChannel(messenger, StorageAccessStreamHandler.CHANNEL).setStreamHandlerFactory { args -> StorageAccessStreamHandler(this, args) }
+ // Media Store change monitoring
+ contentStreamHandler = ContentChangeStreamHandler(this).apply {
+ EventChannel(messenger, ContentChangeStreamHandler.CHANNEL).setStreamHandler(this)
+ }
+
+ // intent handling
+ intentStreamHandler = IntentStreamHandler().apply {
+ EventChannel(messenger, IntentStreamHandler.CHANNEL).setStreamHandler(this)
+ }
+ intentDataMap = extractIntentData(intent)
MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"getIntentData" -> {
result.success(intentDataMap)
intentDataMap.clear()
}
- "pick" -> {
- val pickedUri = call.argument("uri")
- if (pickedUri != null) {
- val intent = Intent().apply {
- data = Uri.parse(pickedUri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- setResult(RESULT_OK, intent)
- } else {
- setResult(RESULT_CANCELED)
- }
- finish()
- }
-
+ "pick" -> pick(call)
}
}
- EventChannel(messenger, INTENT_CHANNEL).setStreamHandler(intentStreamHandler)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
setupShortcuts()
}
}
- @RequiresApi(Build.VERSION_CODES.N_MR1)
- private fun setupShortcuts() {
- // do not use 'route' as extra key, as the Flutter framework acts on it
-
- val search = ShortcutInfoCompat.Builder(this, "search")
- .setShortLabel(getString(R.string.search_shortcut_short_label))
- .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search))
- .setIntent(
- Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
- .putExtra("page", "/search")
- )
- .build()
-
- val videos = ShortcutInfoCompat.Builder(this, "videos")
- .setShortLabel(getString(R.string.videos_shortcut_short_label))
- .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie))
- .setIntent(
- Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
- .putExtra("page", "/collection")
- .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
- )
- .build()
-
- ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search))
+ override fun onDestroy() {
+ contentStreamHandler.dispose()
+ super.onDestroy()
}
override fun onNewIntent(intent: Intent) {
@@ -109,6 +78,25 @@ class MainActivity : FlutterActivity() {
intentStreamHandler.notifyNewIntent(extractIntentData(intent))
}
+ 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
+ }
+
+ // 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)
+ }
+ }
+
private fun extractIntentData(intent: Intent?): MutableMap {
when (intent?.action) {
Intent.ACTION_MAIN -> {
@@ -138,22 +126,48 @@ class MainActivity : FlutterActivity() {
return HashMap()
}
- 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
+ private fun pick(call: MethodCall) {
+ val pickedUri = call.argument("uri")
+ if (pickedUri != null) {
+ val intent = Intent().apply {
+ data = Uri.parse(pickedUri)
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
-
- // 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)
+ setResult(RESULT_OK, intent)
+ } else {
+ setResult(RESULT_CANCELED)
}
+ finish()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.N_MR1)
+ private fun setupShortcuts() {
+ // do not use 'route' as extra key, as the Flutter framework acts on it
+
+ val search = ShortcutInfoCompat.Builder(this, "search")
+ .setShortLabel(getString(R.string.search_shortcut_short_label))
+ .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_search))
+ .setIntent(
+ Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
+ .putExtra("page", "/search")
+ )
+ .build()
+
+ val videos = ShortcutInfoCompat.Builder(this, "videos")
+ .setShortLabel(getString(R.string.videos_shortcut_short_label))
+ .setIcon(IconCompat.createWithResource(this, R.mipmap.ic_shortcut_movie))
+ .setIntent(
+ Intent(Intent.ACTION_MAIN, null, this, MainActivity::class.java)
+ .putExtra("page", "/collection")
+ .putExtra("filters", arrayOf("{\"type\":\"mime\",\"mime\":\"video/*\"}"))
+ )
+ .build()
+
+ ShortcutManagerCompat.setDynamicShortcuts(this, listOf(videos, search))
+ }
+
+ companion object {
+ private val LOG_TAG = LogUtils.createTag(MainActivity::class.java)
+ const val VIEWER_CHANNEL = "deckers.thibault/aves/viewer"
}
}
\ 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 6930273bc..c4b8e3377 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,8 @@ import androidx.core.content.FileProvider
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.model.FieldMap
import deckers.thibault.aves.utils.BitmapUtils.getBytes
import deckers.thibault.aves.utils.LogUtils
import io.flutter.plugin.common.MethodCall
@@ -28,8 +30,8 @@ import kotlin.math.roundToInt
class AppAdapterHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
- "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { getAppIcon(call, Coresult(result)) }
- "getAppNames" -> GlobalScope.launch(Dispatchers.IO) { getAppNames(Coresult(result)) }
+ "getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
+ "getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) }
"edit" -> {
val title = call.argument("title")
val uri = call.argument("uri")?.let { Uri.parse(it) }
@@ -61,46 +63,51 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
}
}
- private fun getAppNames(result: MethodChannel.Result) {
- val nameMap = HashMap()
- val intent = Intent(Intent.ACTION_MAIN, null)
- .addCategory(Intent.CATEGORY_LAUNCHER)
- .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+ private fun getPackages(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
+ val packages = HashMap()
- // apps tend to use their name in English when creating folders
- // so we get their names in English as well as the current locale
- val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
+ fun addPackageDetails(intent: Intent) {
+ // apps tend to use their name in English when creating folders
+ // so we get their names in English as well as the current locale
+ val englishConfig = Configuration().apply { setLocale(Locale.ENGLISH) }
- val pm = context.packageManager
- for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
- val ai = resolveInfo.activityInfo.applicationInfo
- val isSystemPackage = ai.flags and ApplicationInfo.FLAG_SYSTEM != 0
- if (!isSystemPackage) {
- val packageName = ai.packageName
-
- val currentLabel = pm.getApplicationLabel(ai).toString()
- nameMap[currentLabel] = packageName
-
- val labelRes = ai.labelRes
- if (labelRes != 0) {
- try {
- val resources = pm.getResourcesForApplication(ai)
- // `updateConfiguration` is deprecated but it seems to be the only way
- // to query resources from another app with a specific locale.
- // The following methods do not work:
- // - `resources.getConfiguration().setLocale(...)`
- // - getting a package manager from a custom context with `context.createConfigurationContext(config)`
- @Suppress("DEPRECATION")
- resources.updateConfiguration(englishConfig, resources.displayMetrics)
- val englishLabel = resources.getString(labelRes)
- nameMap[englishLabel] = packageName
- } catch (e: PackageManager.NameNotFoundException) {
- Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
+ val pm = context.packageManager
+ for (resolveInfo in pm.queryIntentActivities(intent, 0)) {
+ val appInfo = resolveInfo.activityInfo.applicationInfo
+ val packageName = appInfo.packageName
+ if (!packages.containsKey(packageName)) {
+ val currentLabel = pm.getApplicationLabel(appInfo).toString()
+ val englishLabel: String? = appInfo.labelRes.takeIf { it != 0 }?.let { labelRes ->
+ var englishLabel: String? = null
+ try {
+ val resources = pm.getResourcesForApplication(appInfo)
+ // `updateConfiguration` is deprecated but it seems to be the only way
+ // to query resources from another app with a specific locale.
+ // The following methods do not work:
+ // - `resources.getConfiguration().setLocale(...)`
+ // - getting a package manager from a custom context with `context.createConfigurationContext(config)`
+ @Suppress("DEPRECATION")
+ resources.updateConfiguration(englishConfig, resources.displayMetrics)
+ englishLabel = resources.getString(labelRes)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.w(LOG_TAG, "failed to get app label in English for packageName=$packageName", e)
+ }
+ englishLabel
}
+ packages[packageName] = hashMapOf(
+ "packageName" to packageName,
+ "categoryLauncher" to intent.hasCategory(Intent.CATEGORY_LAUNCHER),
+ "isSystem" to (appInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0),
+ "currentLabel" to currentLabel,
+ "englishLabel" to englishLabel,
+ )
}
}
}
- result.success(nameMap)
+
+ addPackageDetails(Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER))
+ addPackageDetails(Intent(Intent.ACTION_MAIN))
+ result.success(ArrayList(packages.values))
}
private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt
index d2db62561..5207bde7d 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/Coresult.kt
@@ -1,9 +1,11 @@
package deckers.thibault.aves.channel.calls
+import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlin.reflect.KSuspendFunction2
// ensure `result` methods are called on the main looper thread
class Coresult internal constructor(private val methodResult: MethodChannel.Result) : MethodChannel.Result {
@@ -20,4 +22,24 @@ class Coresult internal constructor(private val methodResult: MethodChannel.Resu
override fun notImplemented() {
mainScope.launch { methodResult.notImplemented() }
}
+
+ companion object {
+ fun safe(call: MethodCall, result: MethodChannel.Result, function: (call: MethodCall, result: MethodChannel.Result) -> Unit) {
+ val res = Coresult(result)
+ try {
+ function(call, res)
+ } catch (e: Exception) {
+ res.error("safe-exception", e.message, e.stackTraceToString())
+ }
+ }
+
+ suspend fun safesus(call: MethodCall, result: MethodChannel.Result, function: KSuspendFunction2) {
+ val res = Coresult(result)
+ try {
+ function(call, res)
+ } catch (e: Exception) {
+ res.error("safe-exception", e.message, e.stackTraceToString())
+ }
+ }
+ }
}
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 22e4c84ca..6b4401073 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
@@ -12,10 +12,11 @@ import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.file.FileTypeDirectory
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.ExifInterfaceHelper
import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper
import deckers.thibault.aves.metadata.Metadata
-import deckers.thibault.aves.model.provider.FieldMap
+import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
@@ -37,12 +38,12 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
when (call.method) {
"getContextDirs" -> result.success(getContextDirs())
"getEnv" -> result.success(System.getenv())
- "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { getBitmapFactoryInfo(call, Coresult(result)) }
- "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getContentResolverMetadata(call, Coresult(result)) }
- "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { getExifInterfaceMetadata(call, Coresult(result)) }
- "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { getMediaMetadataRetrieverMetadata(call, Coresult(result)) }
- "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { getMetadataExtractorSummary(call, Coresult(result)) }
- "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { getTiffStructure(call, Coresult(result)) }
+ "getBitmapFactoryInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getBitmapFactoryInfo) }
+ "getContentResolverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverMetadata) }
+ "getExifInterfaceMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifInterfaceMetadata) }
+ "getMediaMetadataRetrieverMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMediaMetadataRetrieverMetadata) }
+ "getMetadataExtractorSummary" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMetadataExtractorSummary) }
+ "getTiffStructure" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getTiffStructure) }
else -> result.notImplemented()
}
}
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 de0ee5245..a9c831565 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
@@ -5,8 +5,13 @@ 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
+import deckers.thibault.aves.channel.calls.fetchers.RegionFetcher
+import deckers.thibault.aves.channel.calls.fetchers.ThumbnailFetcher
+import deckers.thibault.aves.channel.calls.fetchers.TiffRegionFetcher
import deckers.thibault.aves.model.ExifOrientationOp
-import deckers.thibault.aves.model.provider.FieldMap
+import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.model.provider.ImageProvider.ImageOpCallback
import deckers.thibault.aves.model.provider.ImageProviderFactory.getProvider
import deckers.thibault.aves.model.provider.MediaStoreImageProvider
@@ -26,17 +31,14 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
- "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { getObsoleteEntries(call, Coresult(result)) }
- "getImageEntry" -> GlobalScope.launch(Dispatchers.IO) { getImageEntry(call, Coresult(result)) }
- "getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { getThumbnail(call, Coresult(result)) }
- "getRegion" -> GlobalScope.launch(Dispatchers.IO) { getRegion(call, Coresult(result)) }
- "clearSizedThumbnailDiskCache" -> {
- GlobalScope.launch(Dispatchers.IO) { Glide.get(activity).clearDiskCache() }
- result.success(null)
- }
- "rename" -> GlobalScope.launch(Dispatchers.IO) { rename(call, Coresult(result)) }
- "rotate" -> GlobalScope.launch(Dispatchers.IO) { rotate(call, Coresult(result)) }
- "flip" -> GlobalScope.launch(Dispatchers.IO) { flip(call, Coresult(result)) }
+ "getObsoleteEntries" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getObsoleteEntries) }
+ "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) }
+ "clearSizedThumbnailDiskCache" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::clearSizedThumbnailDiskCache) }
+ "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) }
else -> result.notImplemented()
}
}
@@ -58,7 +60,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val isFlipped = call.argument("isFlipped")
val widthDip = call.argument("widthDip")
val heightDip = call.argument("heightDip")
- val page = call.argument("page")
+ val pageId = call.argument("pageId")
val defaultSizeDip = call.argument("defaultSizeDip")
if (uri == null || mimeType == null || dateModifiedSecs == null || rotationDegrees == null || isFlipped == null || widthDip == null || heightDip == null || defaultSizeDip == null) {
@@ -76,7 +78,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
isFlipped,
width = (widthDip * density).roundToInt(),
height = (heightDip * density).roundToInt(),
- page = page,
+ pageId = pageId,
defaultSize = (defaultSizeDip * density).roundToInt(),
result,
).fetch()
@@ -85,7 +87,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
val uri = call.argument("uri")?.let { Uri.parse(it) }
val mimeType = call.argument("mimeType")
- val page = call.argument("page")
+ val pageId = call.argument("pageId")
val sampleSize = call.argument("sampleSize")
val x = call.argument("regionX")
val y = call.argument("regionY")
@@ -102,43 +104,49 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
val regionRect = Rect(x, y, x + width, y + height)
when (mimeType) {
MimeTypes.TIFF -> TiffRegionFetcher(activity).fetch(
- uri,
- sampleSize,
- regionRect,
- page = page ?: 0,
- result,
+ uri = uri,
+ page = pageId ?: 0,
+ sampleSize = sampleSize,
+ regionRect = regionRect,
+ result = result,
)
else -> regionFetcher.fetch(
- uri,
- mimeType,
- sampleSize,
- regionRect,
- Size(imageWidth, imageHeight),
- result,
+ uri = uri,
+ mimeType = mimeType,
+ pageId = pageId,
+ sampleSize = sampleSize,
+ regionRect = regionRect,
+ imageSize = Size(imageWidth, imageHeight),
+ result = result,
)
}
}
- private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) {
+ private suspend fun getEntry(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument("mimeType") // MIME type is optional
val uri = call.argument("uri")?.let { Uri.parse(it) }
if (uri == null) {
- result.error("getImageEntry-args", "failed because of missing arguments", null)
+ result.error("getEntry-args", "failed because of missing arguments", null)
return
}
val provider = getProvider(uri)
if (provider == null) {
- result.error("getImageEntry-provider", "failed to find provider for uri=$uri", null)
+ result.error("getEntry-provider", "failed to find provider for uri=$uri", null)
return
}
provider.fetchSingle(activity, uri, mimeType, object : ImageOpCallback {
override fun onSuccess(fields: FieldMap) = result.success(fields)
- override fun onFailure(throwable: Throwable) = result.error("getImageEntry-failure", "failed to get entry for uri=$uri", throwable.message)
+ override fun onFailure(throwable: Throwable) = result.error("getEntry-failure", "failed to get entry for uri=$uri", throwable.message)
})
}
+ private fun clearSizedThumbnailDiskCache(@Suppress("UNUSED_PARAMETER") call: MethodCall, result: MethodChannel.Result) {
+ Glide.get(activity).clearDiskCache()
+ result.success(null)
+ }
+
private suspend fun rename(call: MethodCall, result: MethodChannel.Result) {
val entryMap = call.argument("entry")
val newName = call.argument("newName")
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 f0a95b2d6..940ab0cba 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
@@ -1,8 +1,15 @@
package deckers.thibault.aves.channel.calls
+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.os.Build
+import android.provider.MediaStore
import android.util.Log
import androidx.exifinterface.media.ExifInterface
import com.adobe.internal.xmp.XMPException
@@ -18,6 +25,7 @@ import com.drew.metadata.iptc.IptcDirectory
import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory
import com.drew.metadata.webp.WebpDirectory
import com.drew.metadata.xmp.XmpDirectory
+import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
import deckers.thibault.aves.metadata.*
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
@@ -38,13 +46,14 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff
import deckers.thibault.aves.metadata.XMP.getSafeDateMillis
import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText
import deckers.thibault.aves.metadata.XMP.isPanorama
-import deckers.thibault.aves.model.provider.FieldMap
+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.isHeifLike
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isMultimedia
import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
@@ -66,14 +75,15 @@ import kotlin.math.roundToLong
class MetadataHandler(private val context: Context) : MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
- "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { getAllMetadata(call, Coresult(result)) }
- "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { getCatalogMetadata(call, Coresult(result)) }
- "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { getOverlayMetadata(call, Coresult(result)) }
- "getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { getMultiPageInfo(call, Coresult(result)) }
- "getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { getPanoramaInfo(call, Coresult(result)) }
- "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { getEmbeddedPictures(call, Coresult(result)) }
- "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { getExifThumbnails(call, Coresult(result)) }
- "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { extractXmpDataProp(call, Coresult(result)) }
+ "getAllMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAllMetadata) }
+ "getCatalogMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getCatalogMetadata) }
+ "getOverlayMetadata" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getOverlayMetadata) }
+ "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) }
+ "getEmbeddedPictures" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getEmbeddedPictures) }
+ "getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
+ "extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
else -> result.notImplemented()
}
}
@@ -430,7 +440,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
}
- if (mimeType == MimeTypes.HEIC || mimeType == MimeTypes.HEIF) {
+ if (isHeifLike(mimeType)) {
retriever.getSafeInt(MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS) {
if (it > 1) flags = flags or MASK_IS_MULTIPAGE
}
@@ -521,21 +531,57 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
return
}
- val pages = HashMap()
+ val pages = ArrayList