fixed opening HEIC from downloads content URI on Android R

This commit is contained in:
Thibault Deckers 2021-07-30 09:45:36 +09:00
parent b68bb86a58
commit 63c06c09fc
8 changed files with 62 additions and 38 deletions

View file

@ -1,5 +1,6 @@
package deckers.thibault.aves package deckers.thibault.aves
import android.annotation.SuppressLint
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@ -95,31 +96,34 @@ class MainActivity : FlutterActivity() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
DOCUMENT_TREE_ACCESS_REQUEST -> { DOCUMENT_TREE_ACCESS_REQUEST -> onDocumentTreeAccessResult(data, resultCode, requestCode)
val treeUri = data?.data DELETE_PERMISSION_REQUEST -> onDeletePermissionResult(resultCode)
if (resultCode != RESULT_OK || treeUri == null) { CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> onPermissionResult(requestCode, data?.data)
onPermissionResult(requestCode, null) }
return }
}
// save access permissions across reboots @SuppressLint("WrongConstant")
val takeFlags = (data.flags private fun onDocumentTreeAccessResult(data: Intent?, resultCode: Int, requestCode: Int) {
and (Intent.FLAG_GRANT_READ_URI_PERMISSION val treeUri = data?.data
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) if (resultCode != RESULT_OK || treeUri == null) {
contentResolver.takePersistableUriPermission(treeUri, takeFlags) onPermissionResult(requestCode, null)
return
}
// resume pending action // save access permissions across reboots
onPermissionResult(requestCode, treeUri) val takeFlags = (data.flags
} and (Intent.FLAG_GRANT_READ_URI_PERMISSION
DELETE_PERMISSION_REQUEST -> { or Intent.FLAG_GRANT_WRITE_URI_PERMISSION))
// delete permission may be requested on Android 10+ only contentResolver.takePersistableUriPermission(treeUri, takeFlags)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
MediaStoreImageProvider.pendingDeleteCompleter?.complete(resultCode == RESULT_OK) // resume pending action
} onPermissionResult(requestCode, treeUri)
} }
CREATE_FILE_REQUEST, OPEN_FILE_REQUEST, SELECT_DIRECTORY_REQUEST -> {
onPermissionResult(requestCode, data?.data) 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)
} }
} }

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -105,7 +104,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 (StorageUtils.isMediaStoreContentUri(uri)) {
uri.tryParseId()?.let { id -> uri.tryParseId()?.let { id ->
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)

View file

@ -1,6 +1,5 @@
package deckers.thibault.aves.channel.calls package deckers.thibault.aves.channel.calls
import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
@ -692,7 +691,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 (StorageUtils.isMediaStoreContentUri(uri)) {
uri.tryParseId()?.let { id -> uri.tryParseId()?.let { id ->
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)

View file

@ -24,6 +24,7 @@ import deckers.thibault.aves.utils.MimeTypes.isHeic
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.StorageUtils
import deckers.thibault.aves.utils.UriUtils.tryParseId import deckers.thibault.aves.utils.UriUtils.tryParseId
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@ -130,7 +131,7 @@ class ThumbnailFetcher internal constructor(
svgFetch -> SvgThumbnail(context, uri) svgFetch -> SvgThumbnail(context, uri)
tiffFetch -> TiffImage(context, uri, pageId) tiffFetch -> TiffImage(context, uri, pageId)
multiTrackFetch -> MultiTrackImage(context, uri, pageId) multiTrackFetch -> MultiTrackImage(context, uri, pageId)
else -> uri else -> StorageUtils.getGlideSafeUri(uri, mimeType)
} }
Glide.with(context) Glide.with(context)
.asBitmap() .asBitmap()

View file

@ -120,7 +120,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
} else if (mimeType == MimeTypes.TIFF) { } else if (mimeType == MimeTypes.TIFF) {
TiffImage(activity, uri, pageId) TiffImage(activity, uri, pageId)
} else { } else {
uri StorageUtils.getGlideSafeUri(uri, mimeType)
} }
val target = Glide.with(activity) val target = Glide.with(activity)

View file

@ -142,7 +142,7 @@ abstract class ImageProvider {
} else if (sourceMimeType == MimeTypes.TIFF) { } else if (sourceMimeType == MimeTypes.TIFF) {
TiffImage(context, sourceUri, pageId) TiffImage(context, sourceUri, pageId)
} else { } else {
sourceUri StorageUtils.getGlideSafeUri(sourceUri, sourceMimeType)
} }
// request a fresh image with the highest quality format // request a fresh image with the highest quality format

View file

@ -2,18 +2,17 @@ package deckers.thibault.aves.model.provider
import android.content.ContentResolver import android.content.ContentResolver
import android.net.Uri import android.net.Uri
import android.provider.MediaStore import deckers.thibault.aves.utils.StorageUtils
import java.util.* import java.util.*
object ImageProviderFactory { object ImageProviderFactory {
fun getProvider(uri: Uri): ImageProvider? { fun getProvider(uri: Uri): ImageProvider? {
return when (uri.scheme?.lowercase(Locale.ROOT)) { return when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_CONTENT -> { ContentResolver.SCHEME_CONTENT -> {
// a URI's authority is [userinfo@]host[:port] if (StorageUtils.isMediaStoreContentUri(uri)) {
// but we only want the host when comparing to Media Store's "authority" MediaStoreImageProvider()
return when (uri.host?.lowercase(Locale.ROOT)) { } else {
MediaStore.AUTHORITY -> MediaStoreImageProvider() ContentImageProvider()
else -> ContentImageProvider()
} }
} }
ContentResolver.SCHEME_FILE -> FileImageProvider() ContentResolver.SCHEME_FILE -> FileImageProvider()

View file

@ -3,6 +3,7 @@ package deckers.thibault.aves.utils
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
@ -15,7 +16,10 @@ import android.text.TextUtils
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.commonsware.cwac.document.DocumentFileCompat 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.PermissionManager.getGrantedDirForPath
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.InputStream import java.io.InputStream
@ -395,7 +399,7 @@ object StorageUtils {
return !onPrimaryVolume return !onPrimaryVolume
} }
private fun isMediaStoreContentUri(uri: Uri?): Boolean { fun isMediaStoreContentUri(uri: Uri?): Boolean {
uri ?: return false uri ?: return false
// a URI's authority is [userinfo@]host[:port] // a URI's authority is [userinfo@]host[:port]
// but we only want the host when comparing to Media Store's "authority" // 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)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && isMediaStoreContentUri(uri)) {
val path = uri.path val path = uri.path
path ?: return uri 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/")) { if (path.startsWith("/external/images/") || path.startsWith("/external/video/")) {
// "Caller must hold ACCESS_MEDIA_LOCATION permission to access original" // "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"
if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) { if (context.checkSelfPermission(Manifest.permission.ACCESS_MEDIA_LOCATION) == PackageManager.PERMISSION_GRANTED) {
@ -418,6 +422,24 @@ object StorageUtils {
return uri 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? { fun openInputStream(context: Context, uri: Uri): InputStream? {
val effectiveUri = getOriginalUri(context, uri) val effectiveUri = getOriginalUri(context, uri)
return try { return try {