From eb3a8f5626f87461e9e5dc6411496ac0ba265e79 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 10 Dec 2021 17:45:38 +0900 Subject: [PATCH] fixed selecting settings file to import on older devices --- .../deckers/thibault/aves/MainActivity.kt | 2 +- .../aves/channel/calls/AppAdapterHandler.kt | 4 +- .../channel/calls/MetadataFetchHandler.kt | 4 +- .../streams/StorageAccessStreamHandler.kt | 39 +++++++++++-------- .../thibault/aves/model/SourceEntry.kt | 2 +- .../model/provider/MediaStoreImageProvider.kt | 2 +- .../deckers/thibault/aves/utils/MimeTypes.kt | 8 ++-- lib/services/android_app_service.dart | 2 +- lib/services/android_debug_service.dart | 2 +- lib/services/storage_service.dart | 4 +- lib/widgets/settings/settings_page.dart | 4 +- 11 files changed, 40 insertions(+), 33 deletions(-) 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 d13ddcaaa..f90d01668 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/MainActivity.kt @@ -155,7 +155,7 @@ class MainActivity : FlutterActivity() { } } - @SuppressLint("WrongConstant") + @SuppressLint("WrongConstant", "ObsoleteSdkInt") private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) { val treeUri = data?.data if (resultCode != RESULT_OK || treeUri == null) { 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 df244191f..ea05c9733 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 @@ -272,13 +272,13 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } else { var mimeType = "*/*" if (mimeTypes.size == 1) { - // items have the same mime type & subtype + // items have the same MIME type & subtype mimeType = mimeTypes.first() } else { // items have different subtypes val mimeTypeTypes = mimeTypes.map { it.split("/") }.distinct() if (mimeTypeTypes.size == 1) { - // items have the same mime type + // items have the same MIME type mimeType = "${mimeTypeTypes.first()}/*" } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt index f7370c238..ec777d178 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataFetchHandler.kt @@ -411,8 +411,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler { // File type for (dir in metadata.getDirectoriesOfType(FileTypeDirectory::class.java)) { - // * `metadata-extractor` sometimes detects the wrong mime type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`) - // * the content resolver / media store sometimes reports the wrong mime type (e.g. `png` file as `jpeg`, `tiff` as `srw`) + // * `metadata-extractor` sometimes detects the wrong MIME type (e.g. `pef` file as `tiff`, `mpeg` as `dvd`) + // * 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 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt index 35c564e73..42755cf30 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/StorageAccessStreamHandler.kt @@ -1,5 +1,6 @@ package deckers.thibault.aves.channel.streams +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri @@ -10,6 +11,7 @@ import android.util.Log import deckers.thibault.aves.MainActivity import deckers.thibault.aves.PendingStorageAccessResultHandler import deckers.thibault.aves.utils.LogUtils +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.PermissionManager import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink @@ -90,9 +92,9 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? endOfStream() } + @SuppressLint("ObsoleteSdkInt") private fun createFile() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - // TODO TLAD [<=API18] create file error("createFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) return } @@ -133,24 +135,16 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? } - private fun openFile() { + @SuppressLint("ObsoleteSdkInt") + private suspend fun openFile() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { - // TODO TLAD [<=API18] open file error("openFile-sdk", "unsupported SDK version=${Build.VERSION.SDK_INT}", null) return } - val mimeType = args["mimeType"] as String? - if (mimeType == null) { - error("openFile-args", "failed because of missing arguments", null) - return - } + val mimeType = args["mimeType"] as String? // optional - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = mimeType - } - MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, { uri -> + fun onGranted(uri: Uri) { GlobalScope.launch(Dispatchers.IO) { activity.contentResolver.openInputStream(uri)?.use { input -> val buffer = ByteArray(BUFFER_SIZE) @@ -161,11 +155,24 @@ class StorageAccessStreamHandler(private val activity: Activity, arguments: Any? endOfStream() } } - }, { + } + + fun onDenied() { success(ByteArray(0)) endOfStream() - }) - activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST) + } + + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + setTypeAndNormalize(mimeType ?: MimeTypes.ANY) + } + if (intent.resolveActivity(activity.packageManager) != null) { + MainActivity.pendingStorageAccessResultHandlers[MainActivity.OPEN_FILE_REQUEST] = PendingStorageAccessResultHandler(null, ::onGranted, ::onDenied) + activity.startActivityForResult(intent, MainActivity.OPEN_FILE_REQUEST) + } else { + MainActivity.notifyError("failed to resolve activity for intent=$intent") + onDenied() + } } override fun onCancel(arguments: Any?) {} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt index cbba47781..67e429301 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceEntry.kt @@ -161,7 +161,7 @@ class SourceEntry { Metadata.openSafeInputStream(context, uri, sourceMimeType, sizeBytes)?.use { input -> val metadata = ImageMetadataReader.readMetadata(input) - // do not switch on specific mime types, as the reported mime type could be wrong + // do not switch on specific MIME types, as the reported MIME type could be wrong // (e.g. PNG registered as JPG) if (isVideo) { for (dir in metadata.getDirectoriesOfType(AviDirectory::class.java)) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 649228c7b..2811f3f22 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -175,7 +175,7 @@ class MediaStoreImageProvider : ImageProvider() { // but for single items, `contentUri` already contains the ID val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong()) // `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices) - // in that case we try to use the mime type provided along the URI + // in that case we try to use the MIME type provided along the URI val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType val width = cursor.getInt(widthColumn) val height = cursor.getInt(heightColumn) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 49613ac21..07d8064f6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -3,7 +3,7 @@ package deckers.thibault.aves.utils import androidx.exifinterface.media.ExifInterface object MimeTypes { - private const val IMAGE = "image" + const val ANY = "*/*"; // generic raster const val BMP = "image/bmp" @@ -45,8 +45,6 @@ object MimeTypes { // vector const val SVG = "image/svg+xml" - private const val VIDEO = "video" - private const val AVI = "video/avi" private const val AVI_VND = "video/vnd.avi" const val DVD = "video/dvd" @@ -58,9 +56,9 @@ object MimeTypes { private const val OGV = "video/ogg" private const val WEBM = "video/webm" - fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith(IMAGE) + fun isImage(mimeType: String?) = mimeType != null && mimeType.startsWith("image") - fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith(VIDEO) + fun isVideo(mimeType: String?) = mimeType != null && mimeType.startsWith("video") fun isHeic(mimeType: String?) = mimeType != null && (mimeType == HEIC || mimeType == HEIF) diff --git a/lib/services/android_app_service.dart b/lib/services/android_app_service.dart index b2d118d76..fa47d60ed 100644 --- a/lib/services/android_app_service.dart +++ b/lib/services/android_app_service.dart @@ -144,7 +144,7 @@ class PlatformAndroidAppService implements AndroidAppService { @override Future shareEntries(Iterable entries) async { - // loosen mime type to a generic one, so we can share with badly defined apps + // loosen MIME type to a generic one, so we can share with badly defined apps // e.g. Google Lens declares receiving "image/jpeg" only, but it can actually handle more formats final urisByMimeType = groupBy(entries, (e) => e.mimeTypeAnySubtype).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList())); try { diff --git a/lib/services/android_debug_service.dart b/lib/services/android_debug_service.dart index ee522c45c..6c7e445c6 100644 --- a/lib/services/android_debug_service.dart +++ b/lib/services/android_debug_service.dart @@ -133,7 +133,7 @@ class AndroidDebugService { static Future getMetadataExtractorSummary(AvesEntry entry) async { try { - // returns map with the mime type and tag count for each directory found by `metadata-extractor` + // returns map with the MIME type and tag count for each directory found by `metadata-extractor` final result = await platform.invokeMethod('getMetadataExtractorSummary', { 'mimeType': entry.mimeType, 'uri': entry.uri, diff --git a/lib/services/storage_service.dart b/lib/services/storage_service.dart index 4b3666e5f..a1fc9bd36 100644 --- a/lib/services/storage_service.dart +++ b/lib/services/storage_service.dart @@ -36,7 +36,7 @@ abstract class StorageService { // return whether operation succeeded (`null` if user cancelled) Future createFile(String name, String mimeType, Uint8List bytes); - Future openFile(String mimeType); + Future openFile([String? mimeType]); } class PlatformStorageService implements StorageService { @@ -231,7 +231,7 @@ class PlatformStorageService implements StorageService { } @override - Future openFile(String mimeType) async { + Future openFile([String? mimeType]) async { try { final completer = Completer.sync(); final sink = OutputBuffer(); diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index d11940701..e0cef8ed7 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -123,7 +123,9 @@ class _SettingsPageState extends State with FeedbackMixin { } break; case SettingsAction.import: - final bytes = await storageService.openFile(MimeTypes.json); + // specifying the JSON MIME type to restrict openable files is correct in theory, + // but older devices (e.g. SM-P580, API 27) that do not recognize JSON files as such would filter them out + final bytes = await storageService.openFile(); if (bytes.isNotEmpty) { try { await settings.fromJson(utf8.decode(bytes));