#506 ignore sync error when copying file

This commit is contained in:
Thibault Deckers 2023-01-30 13:39:35 +01:00
parent abeb3f8efe
commit 0e56ce5e2d
5 changed files with 47 additions and 29 deletions

View file

@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file.
### Fixed ### Fixed
- SD card access grant on Android Lollipop - SD card access grant on Android Lollipop
- copying to SD card in some cases
## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18 ## <a id="v1.7.10"></a>[v1.7.10] - 2023-01-18

View file

@ -67,7 +67,7 @@ import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeRational
import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString import deckers.thibault.aves.metadata.metadataextractor.Helper.getSafeString
import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir import deckers.thibault.aves.metadata.metadataextractor.Helper.isPngTextDir
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
@ -104,8 +104,8 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) } "getPanoramaInfo" -> ioScope.launch { safe(call, result, ::getPanoramaInfo) }
"getIptc" -> ioScope.launch { safe(call, result, ::getIptc) } "getIptc" -> ioScope.launch { safe(call, result, ::getIptc) }
"getXmp" -> ioScope.launch { safe(call, result, ::getXmp) } "getXmp" -> ioScope.launch { safe(call, result, ::getXmp) }
"hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentResolverProp) } "hasContentResolverProp" -> ioScope.launch { safe(call, result, ::hasContentProp) }
"getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentResolverProp) } "getContentResolverProp" -> ioScope.launch { safe(call, result, ::getContentPropValue) }
"getDate" -> ioScope.launch { safe(call, result, ::getDate) } "getDate" -> ioScope.launch { safe(call, result, ::getDate) }
"getDescription" -> ioScope.launch { safe(call, result, ::getDescription) } "getDescription" -> ioScope.launch { safe(call, result, ::getDescription) }
else -> result.notImplemented() else -> result.notImplemented()
@ -1047,10 +1047,10 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
result.success(xmpStrings) result.success(xmpStrings)
} }
private fun hasContentResolverProp(call: MethodCall, result: MethodChannel.Result) { private fun hasContentProp(call: MethodCall, result: MethodChannel.Result) {
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (prop == null) { if (prop == null) {
result.error("hasContentResolverProp-args", "missing arguments", null) result.error("hasContentProp-args", "missing arguments", null)
return return
} }
@ -1058,27 +1058,27 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
when (prop) { when (prop) {
"owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q "owner_package_name" -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
else -> { else -> {
result.error("hasContentResolverProp-unknown", "unknown property=$prop", null) result.error("hasContentProp-unknown", "unknown property=$prop", null)
return return
} }
} }
) )
} }
private fun getContentResolverProp(call: MethodCall, result: MethodChannel.Result) { private fun getContentPropValue(call: MethodCall, result: MethodChannel.Result) {
val mimeType = call.argument<String>("mimeType") val mimeType = call.argument<String>("mimeType")
val uri = call.argument<String>("uri")?.let { Uri.parse(it) } val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
val prop = call.argument<String>("prop") val prop = call.argument<String>("prop")
if (mimeType == null || uri == null || prop == null) { if (mimeType == null || uri == null || prop == null) {
result.error("getContentResolverProp-args", "missing arguments", null) result.error("getContentPropValue-args", "missing arguments", null)
return return
} }
try { try {
val value = context.queryContentResolverProp(uri, mimeType, prop) val value = context.queryContentPropValue(uri, mimeType, prop)
result.success(value?.toString()) result.success(value?.toString())
} catch (e: Exception) { } catch (e: Exception) {
result.error("getContentResolverProp-query", "failed to query prop for uri=$uri", e.message) result.error("getContentPropValue-query", "failed to query prop for uri=$uri", e.message)
} }
} }

View file

@ -15,7 +15,7 @@ import deckers.thibault.aves.metadata.Mp4ParserHelper.processBoxes
import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes import deckers.thibault.aves.metadata.Mp4ParserHelper.toBytes
import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler import deckers.thibault.aves.metadata.metadataextractor.SafeMp4UuidBoxHandler
import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader import deckers.thibault.aves.metadata.metadataextractor.SafeXmpReader
import deckers.thibault.aves.utils.ContextUtils.queryContentResolverProp import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MemoryUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
@ -130,7 +130,7 @@ object XMP {
) { ) {
if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (MimeTypes.isHeic(mimeType) && !foundXmp && StorageUtils.isMediaStoreContentUri(uri) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
try { try {
val xmpBytes = context.queryContentResolverProp(uri, mimeType, MediaStore.MediaColumns.XMP) val xmpBytes = context.queryContentPropValue(uri, mimeType, MediaStore.MediaColumns.XMP)
if (xmpBytes is ByteArray && xmpBytes.size > 0) { if (xmpBytes is ByteArray && xmpBytes.size > 0) {
val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS) val xmpMeta = XMPMetaFactory.parseFromBuffer(xmpBytes, SafeXmpReader.PARSE_OPTIONS)
processXmp(xmpMeta) processXmp(xmpMeta)

View file

@ -30,6 +30,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
import java.io.SyncFailedException
import java.util.* import java.util.*
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
@ -512,7 +513,16 @@ class MediaStoreImageProvider : ImageProvider() {
targetDir = targetDir, targetDir = targetDir,
targetDirDocFile = targetDirDocFile, targetDirDocFile = targetDirDocFile,
targetNameWithoutExtension = targetNameWithoutExtension, targetNameWithoutExtension = targetNameWithoutExtension,
) { output: OutputStream -> sourceDocFile.copyTo(output) } ) { output: OutputStream ->
try {
sourceDocFile.copyTo(output)
} catch (e: SyncFailedException) {
// The copied file is synced after writing, but it consistently fails in some cases
// (e.g. copying to SD card on Xiaomi 2201117PG with Android 11).
// It seems this failure can be safely ignored, as the new file is complete.
Log.w(LOG_TAG, "sync failure after copying from uri=$sourceUri, path=$sourcePath to targetDir=$targetDir", e)
}
}
if (!copy) { if (!copy) {
// delete original entry // delete original entry

View file

@ -7,6 +7,7 @@ import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import deckers.thibault.aves.utils.UriUtils.tryParseId import deckers.thibault.aves.utils.UriUtils.tryParseId
@ -30,7 +31,13 @@ object ContextUtils {
return am.getRunningServices(Integer.MAX_VALUE).any { it.service.className == serviceClass.name } return am.getRunningServices(Integer.MAX_VALUE).any { it.service.className == serviceClass.name }
} }
fun Context.queryContentResolverProp(uri: Uri, mimeType: String, prop: String): Any? { // `flag`: `DocumentsContract.Document.FLAG_SUPPORTS_COPY`, etc.
fun Context.queryDocumentProviderFlag(docUri: Uri, flag: Int): Boolean {
val flags = queryContentPropValue(docUri, "", DocumentsContract.Document.COLUMN_FLAGS) as Long?
return if (flags != null) (flags.toInt() and flag) == flag else false
}
fun Context.queryContentPropValue(uri: Uri, mimeType: String, column: String): Any? {
var contentUri: Uri = uri var contentUri: Uri = uri
if (StorageUtils.isMediaStoreContentUri(uri)) { if (StorageUtils.isMediaStoreContentUri(uri)) {
uri.tryParseId()?.let { id -> uri.tryParseId()?.let { id ->
@ -43,26 +50,26 @@ object ContextUtils {
} }
} }
// throws SQLiteException when the requested prop is not a known column
val cursor = contentResolver.query(contentUri, arrayOf(prop), null, null, null)
if (cursor == null || !cursor.moveToFirst()) {
throw Exception("failed to get cursor for contentUri=$contentUri")
}
var value: Any? = null var value: Any? = null
try { try {
value = when (cursor.getType(0)) { val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null)
Cursor.FIELD_TYPE_NULL -> null if (cursor == null || !cursor.moveToFirst()) {
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0) Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column")
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0) } else {
Cursor.FIELD_TYPE_STRING -> cursor.getString(0) value = when (cursor.getType(0)) {
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0) Cursor.FIELD_TYPE_NULL -> null
else -> null Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
Cursor.FIELD_TYPE_FLOAT -> cursor.getFloat(0)
Cursor.FIELD_TYPE_STRING -> cursor.getString(0)
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
else -> null
}
cursor.close()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri key=$prop", e) // throws SQLiteException/IllegalArgumentException when the requested prop is not a known column
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri column=$column", e)
} }
cursor.close()
return value return value
} }
} }