safer content URI parsing
This commit is contained in:
parent
aab4800d9b
commit
020c63f499
7 changed files with 46 additions and 36 deletions
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue