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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue