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

View file

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

View file

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

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.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()

View file

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

View file

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

View file

@ -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()

View file

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