safer content URI parsing

This commit is contained in:
Thibault Deckers 2021-01-31 16:11:11 +09:00
parent aab4800d9b
commit 020c63f499
7 changed files with 46 additions and 36 deletions

View file

@ -23,6 +23,7 @@ import deckers.thibault.aves.utils.MimeTypes.isSupportedByExifInterface
import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
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 io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -96,8 +97,7 @@ class DebugHandler(private val context: Context) : MethodCallHandler {
var contentUri: Uri = uri var contentUri: Uri = uri
if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) {
try { uri.tryParseId()?.let { id ->
val id = ContentUris.parseId(uri)
contentUri = when { contentUri = when {
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentUri = MediaStore.setRequireOriginal(contentUri) contentUri = MediaStore.setRequireOriginal(contentUri)
} }
} catch (e: NumberFormatException) {
// ignore
} }
} }

View file

@ -61,6 +61,7 @@ import deckers.thibault.aves.utils.MimeTypes.isSupportedByMetadataExtractor
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
import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -639,8 +640,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
var contentUri: Uri = uri var contentUri: Uri = uri
if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) { if (uri.scheme == ContentResolver.SCHEME_CONTENT && MediaStore.AUTHORITY.equals(uri.host, ignoreCase = true)) {
try { uri.tryParseId()?.let { id ->
val id = ContentUris.parseId(uri)
contentUri = when { contentUri = when {
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentUri = MediaStore.setRequireOriginal(contentUri) contentUri = MediaStore.setRequireOriginal(contentUri)
} }
} catch (e: NumberFormatException) {
// ignore
} }
} }

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls.fetchers package deckers.thibault.aves.channel.calls.fetchers
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri 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.isVideo
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail
import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide
import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
class ThumbnailFetcher internal constructor( class ThumbnailFetcher internal constructor(
@ -94,7 +94,7 @@ class ThumbnailFetcher internal constructor(
} }
private fun getByMediaStore(): Bitmap? { private fun getByMediaStore(): Bitmap? {
val contentId = ContentUris.parseId(uri) val contentId = uri.tryParseId() ?: return null
val resolver = context.contentResolver val resolver = context.contentResolver
return if (isVideo(mimeType)) { return if (isVideo(mimeType)) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View file

@ -1,7 +1,6 @@
package deckers.thibault.aves.model package deckers.thibault.aves.model
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.media.MediaMetadataRetriever 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.getSafeDateMillis
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt
import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeLong
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId
import org.beyka.tiffbitmapfactory.TiffBitmapFactory import org.beyka.tiffbitmapfactory.TiffBitmapFactory
import java.io.IOException import java.io.IOException
@ -93,16 +92,7 @@ class SourceEntry {
// ignore when the ID is not a number // ignore when the ID is not a number
// e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg // e.g. content://com.sec.android.app.myfiles.FileProvider/device_storage/20200109_162621.jpg
private val contentId: Long? private val contentId: Long?
get() { get() = if (uri.scheme == ContentResolver.SCHEME_CONTENT) uri.tryParseId() else null
if (uri.scheme == ContentResolver.SCHEME_CONTENT) {
try {
return ContentUris.parseId(uri)
} catch (e: Exception) {
// ignore
}
}
return null
}
val isSized: Boolean val isSized: Boolean
get() = width ?: 0 > 0 && height ?: 0 > 0 get() = width ?: 0 > 0 && height ?: 0 > 0

View file

@ -25,6 +25,7 @@ import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp import deckers.thibault.aves.utils.StorageUtils.copyFileToTemp
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.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
@ -292,18 +293,20 @@ abstract class ImageProvider {
protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap = protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
suspendCoroutine { cont -> suspendCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? -> MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
var contentId: Long = 0 var contentId: Long? = null
var contentUri: Uri? = null var contentUri: Uri? = null
if (newUri != null) { if (newUri != null) {
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") // `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") // but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
contentId = ContentUris.parseId(newUri) contentId = newUri.tryParseId()
if (contentId != null) {
if (MimeTypes.isImage(mimeType)) { if (MimeTypes.isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId)
} else if (MimeTypes.isVideo(mimeType)) { } else if (MimeTypes.isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId)
} }
} }
}
if (contentUri == null) { if (contentUri == null) {
cont.resumeWithException(Exception("failed to get content URI of item at path=$path")) cont.resumeWithException(Exception("failed to get content URI of item at path=$path"))
return@scanFile return@scanFile

View file

@ -19,6 +19,7 @@ import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent
import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator
import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.getDocumentFile
import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission
import deckers.thibault.aves.utils.UriUtils.tryParseId
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -34,12 +35,13 @@ class MediaStoreImageProvider : ImageProvider() {
} }
override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { 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) { val onSuccess = fun(entry: FieldMap) {
entry["uri"] = uri.toString() entry["uri"] = uri.toString()
callback.onSuccess(entry) callback.onSuccess(entry)
} }
val alwaysValid = { _: Int, _: Int -> true } val alwaysValid = { _: Int, _: Int -> true }
if (id != null) {
if (mimeType == null || isImage(mimeType)) { if (mimeType == null || isImage(mimeType)) {
val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id) val contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, id)
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return if (fetchFrom(context, alwaysValid, onSuccess, contentUri, IMAGE_PROJECTION) > 0) return
@ -48,6 +50,7 @@ class MediaStoreImageProvider : ImageProvider() {
val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id) val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id)
if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return 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") // 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 // without an equivalent image/video if it is shared from a file browser
// but the file is not publicly visible // but the file is not publicly visible

View file

@ -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
}
}