fixed opening HEIC from downloads content URI on Android R
This commit is contained in:
parent
b68bb86a58
commit
63c06c09fc
8 changed files with 62 additions and 38 deletions
|
@ -1,5 +1,6 @@
|
|||
package deckers.thibault.aves
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.SearchManager
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
|
@ -95,31 +96,34 @@ class MainActivity : FlutterActivity() {
|
|||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
when (requestCode) {
|
||||
DOCUMENT_TREE_ACCESS_REQUEST -> {
|
||||
val treeUri = data?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
onPermissionResult(requestCode, null)
|
||||
return
|
||||
}
|
||||
DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode)
|
||||
DELETE_PERMISSION_REQUEST -> onDeletePermissionResult(resultCode)
|
||||
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> onPermissionResult(requestCode, data?.data)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@SuppressLint("WrongConstant")
|
||||
private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
|
||||
val treeUri = data?.data
|
||||
if (resultCode != RESULT_OK || treeUri == null) {
|
||||
onPermissionResult(requestCode, null)
|
||||
return
|
||||
}
|
||||
|
||||
// resume pending action
|
||||
onPermissionResult(requestCode, treeUri)
|
||||
}
|
||||
DELETE_PERMISSION_REQUEST -> {
|
||||
// delete permission may be requested on Android 10+ only
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
|
||||
}
|
||||
}
|
||||
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> {
|
||||
onPermissionResult(requestCode, data?.data)
|
||||
}
|
||||
// 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
|
||||
onPermissionResult(requestCode, treeUri)
|
||||
}
|
||||
|
||||
private fun onDeletePermissionResult(resultCode: Int) {
|
||||
// delete permission may be requested on Android 10+ only
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
|
@ -105,7 +104,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)) {
|
||||
if (StorageUtils.isMediaStoreContentUri(uri)) {
|
||||
uri.tryParseId()?.let { id ->
|
||||
contentUri = when {
|
||||
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package deckers.thibault.aves.channel.calls
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
|
@ -692,7 +691,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)) {
|
||||
if (StorageUtils.isMediaStoreContentUri(uri)) {
|
||||
uri.tryParseId()?.let { id ->
|
||||
contentUri = when {
|
||||
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
|
|
|
@ -24,6 +24,7 @@ import deckers.thibault.aves.utils.MimeTypes.isHeic
|
|||
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.StorageUtils
|
||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
|
@ -130,7 +131,7 @@ class ThumbnailFetcher internal constructor(
|
|||
svgFetch -> SvgThumbnail(context, uri)
|
||||
tiffFetch -> TiffImage(context, uri, pageId)
|
||||
multiTrackFetch -> MultiTrackImage(context, uri, pageId)
|
||||
else -> uri
|
||||
else -> StorageUtils.getGlideSafeUri(uri, mimeType)
|
||||
}
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
|
|
|
@ -120,7 +120,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
|||
} else if (mimeType == MimeTypes.TIFF) {
|
||||
TiffImage(activity, uri, pageId)
|
||||
} else {
|
||||
uri
|
||||
StorageUtils.getGlideSafeUri(uri, mimeType)
|
||||
}
|
||||
|
||||
val target = Glide.with(activity)
|
||||
|
|
|
@ -142,7 +142,7 @@ abstract class ImageProvider {
|
|||
} else if (sourceMimeType == MimeTypes.TIFF) {
|
||||
TiffImage(context, sourceUri, pageId)
|
||||
} else {
|
||||
sourceUri
|
||||
StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType)
|
||||
}
|
||||
|
||||
// request a fresh image with the highest quality format
|
||||
|
|
|
@ -2,18 +2,17 @@ package deckers.thibault.aves.model.provider
|
|||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import deckers.thibault.aves.utils.StorageUtils
|
||||
import java.util.*
|
||||
|
||||
object ImageProviderFactory {
|
||||
fun getProvider(uri: Uri): ImageProvider? {
|
||||
return when (uri.scheme?.lowercase(Locale.ROOT)) {
|
||||
ContentResolver.SCHEME_CONTENT -> {
|
||||
// a URI's authority is [userinfo@]host[:port]
|
||||
// but we only want the host when comparing to Media Store's "authority"
|
||||
return when (uri.host?.lowercase(Locale.ROOT)) {
|
||||
MediaStore.AUTHORITY -> MediaStoreImageProvider()
|
||||
else -> ContentImageProvider()
|
||||
if (StorageUtils.isMediaStoreContentUri(uri)) {
|
||||
MediaStoreImageProvider()
|
||||
} else {
|
||||
ContentImageProvider()
|
||||
}
|
||||
}
|
||||
ContentResolver.SCHEME_FILE -> FileImageProvider()
|
||||
|
|
|
@ -3,6 +3,7 @@ package deckers.thibault.aves.utils
|
|||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.MediaMetadataRetriever
|
||||
|
@ -15,7 +16,10 @@ import android.text.TextUtils
|
|||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.commonsware.cwac.document.DocumentFileCompat
|
||||
import deckers.thibault.aves.utils.MimeTypes.isImage
|
||||
import deckers.thibault.aves.utils.MimeTypes.isVideo
|
||||
import deckers.thibault.aves.utils.PermissionManager.getGrantedDirForPath
|
||||
import deckers.thibault.aves.utils.UriUtils.tryParseId
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
|
@ -395,7 +399,7 @@ object StorageUtils {
|
|||
return !onPrimaryVolume
|
||||
}
|
||||
|
||||
private fun isMediaStoreContentUri(uri: Uri?): Boolean {
|
||||
fun isMediaStoreContentUri(uri: Uri?): Boolean {
|
||||
uri ?: return false
|
||||
// a URI's authority is [userinfo@]host[:port]
|
||||
// but we only want the host when comparing to Media Store's "authority"
|
||||
|
@ -407,7 +411,7 @@ object StorageUtils {
|
|||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) {
|
||||
val path = uri.path
|
||||
path ?: return uri
|
||||
// from Android R, accessing the original URI for a file media content yields a `SecurityException`
|
||||
// from Android R, accessing the original URI for a `file` or `downloads` media content yields a `SecurityException`
|
||||
if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) {
|
||||
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
|
||||
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
||||
|
@ -418,6 +422,24 @@ object StorageUtils {
|
|||
return uri
|
||||
}
|
||||
|
||||
// As of Glide v4.12.0, a special loader `QMediaStoreUriLoader` is automatically used
|
||||
// to work around a bug from Android Q where metadata redaction corrupts HEIC images.
|
||||
// This loader relies on `MediaStore.setRequireOriginal` but this yields a `SecurityException`
|
||||
// for some content URIs (e.g. `content://media/external_primary/downloads/...`)
|
||||
// so we build a typical `images` or `videos` content URI from the original content ID.
|
||||
fun getGlideSafeUri(uri: Uri, mimeType: String): Uri {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) {
|
||||
uri.tryParseId()?.let { id ->
|
||||
return when {
|
||||
isImage(mimeType) -> ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
|
||||
isVideo(mimeType) -> ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, id)
|
||||
else -> uri
|
||||
}
|
||||
}
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
fun openInputStream(context: Context, uri: Uri): InputStream? {
|
||||
val effectiveUri = getOriginalUri(context, uri)
|
||||
return try {
|
||||
|
|
Loading…
Reference in a new issue