From 020c63f4991445fdb6a97a0ef5a70c3f74ebdaa8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 31 Jan 2021 16:11:11 +0900 Subject: [PATCH] safer content URI parsing --- .../aves/channel/calls/DebugHandler.kt | 6 ++---- .../aves/channel/calls/MetadataHandler.kt | 6 ++---- .../calls/fetchers/ThumbnailFetcher.kt | 4 ++-- .../thibault/aves/model/SourceEntry.kt | 14 ++------------ .../aves/model/provider/ImageProvider.kt | 15 +++++++++------ .../model/provider/MediaStoreImageProvider.kt | 19 +++++++++++-------- .../deckers/thibault/aves/utils/UriUtils.kt | 18 ++++++++++++++++++ 7 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/utils/UriUtils.kt 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 6b4401073..0de4bf39f 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 @@ -23,6 +23,7 @@ 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 deckers.thibault.aves.utils.UriUtils.tryParseId import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -96,8 +97,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler { var contentUri: Uri = uri if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { - try { - val id = ContentUris.parseId(uri) + uri.tryParseId()?.let { id -> contentUri = when { isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) @@ -106,8 +106,6 @@ class DebugHandler(private val context: Context) : MethodCallHandler { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentUri = MediaStore.setRequireOriginal(contentUri) } - } catch (e: NumberFormatException) { - // ignore } } 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 940ab0cba..505b50f4b 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 @@ -61,6 +61,7 @@ import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern import deckers.thibault.aves.utils.StorageUtils +import deckers.thibault.aves.utils.UriUtils.tryParseId import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler @@ -639,8 +640,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { var contentUri: Uri = uri if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { - try { - val id = ContentUris.parseId(uri) + uri.tryParseId()?.let { id -> contentUri = when { isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id) @@ -649,8 +649,6 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { contentUri = MediaStore.setRequireOriginal(contentUri) } - } catch (e: NumberFormatException) { - // ignore } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt index e631af9eb..47d31bfaa 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/ThumbnailFetcher.kt @@ -1,6 +1,5 @@ package deckers.thibault.aves.channel.calls.fetchers -import android.content.ContentUris import android.content.Context import android.graphics.Bitmap import android.net.Uri @@ -23,6 +22,7 @@ import deckers.thibault.aves.utils.MimeTypes.isHeifLike import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide +import deckers.thibault.aves.utils.UriUtils.tryParseId import io.flutter.plugin.common.MethodChannel class ThumbnailFetcher internal constructor( @@ -94,7 +94,7 @@ class ThumbnailFetcher internal constructor( } private fun getByMediaStore(): Bitmap? { - val contentId = ContentUris.parseId(uri) + val contentId = uri.tryParseId() ?: return null val resolver = context.contentResolver return if (isVideo(mimeType)) { @Suppress("DEPRECATION") 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 5a51905da..bfbdc737c 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 @@ -1,7 +1,6 @@ package deckers.thibault.aves.model import android.content.ContentResolver -import android.content.ContentUris import android.content.Context import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever @@ -25,9 +24,9 @@ import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong -import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils +import deckers.thibault.aves.utils.UriUtils.tryParseId import org.beyka.tiffbitmapfactory.TiffBitmapFactory import java.io.IOException @@ -93,16 +92,7 @@ class SourceEntry { // ignore when the ID is not a number // e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg private val contentId: Long? - get() { - if (uri.scheme == ContentResolver.SCHEME_CONTENT) { - try { - return ContentUris.parseId(uri) - } catch (e: Exception) { - // ignore - } - } - return null - } + get() = if (uri.scheme == ContentResolver.SCHEME_CONTENT) uri.tryParseId() else null val isSized: Boolean get() = width ?: 0 > 0 && height ?: 0 > 0 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index f92ba728c..da1fa2c49 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -25,6 +25,7 @@ import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.getDocumentFile +import deckers.thibault.aves.utils.UriUtils.tryParseId import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -292,16 +293,18 @@ 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? -> - var contentId: Long = 0 + var contentId: Long? = null var contentUri: Uri? = null if (newUri != 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") - contentId = ContentUris.parseId(newUri) - if (MimeTypes.isImage(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) - } else if (MimeTypes.isVideo(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) + contentId = newUri.tryParseId() + if (contentId != null) { + if (MimeTypes.isImage(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) + } else if (MimeTypes.isVideo(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) + } } } if (contentUri == null) { 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 cf90f1b5d..b2386f961 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 @@ -19,6 +19,7 @@ import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission +import deckers.thibault.aves.utils.UriUtils.tryParseId import kotlinx.coroutines.delay import java.io.File import java.util.* @@ -34,19 +35,21 @@ class MediaStoreImageProvider : ImageProvider() { } override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { - val id = ContentUris.parseId(uri) + val id = uri.tryParseId() val onSuccess = fun(entry: FieldMap) { entry["uri"] = uri.toString() callback.onSuccess(entry) } val alwaysValid = { _: Int, _: Int -> true } - if (mimeType == null || isImage(mimeType)) { - val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id) - if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return - } - if (mimeType == null || isVideo(mimeType)) { - val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id) - if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return + if (id != null) { + if (mimeType == null || isImage(mimeType)) { + val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id) + if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return + } + if (mimeType == null || isVideo(mimeType)) { + val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id) + if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return + } } // the uri can be a file media URI (e.g. "content://0@media/external/file/30050") // without an equivalent image/video if it is shared from a file browser diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/UriUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/UriUtils.kt new file mode 100644 index 000000000..6ac38aeb6 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/UriUtils.kt @@ -0,0 +1,18 @@ +package deckers.thibault.aves.utils + +import android.content.ContentUris +import android.net.Uri +import android.util.Log + +object UriUtils { + private val LOG_TAG = LogUtils.createTag(UriUtils::class.java) + + fun Uri.tryParseId(): Long? { + try { + return ContentUris.parseId(this) + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to parse ID from contentUri=$this") + } + return null + } +} \ No newline at end of file