#506 ignore sync error when copying file
This commit is contained in:
parent
abeb3f8efe
commit
0e56ce5e2d
5 changed files with 47 additions and 29 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,14 +50,12 @@ 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 {
|
||||||
|
val cursor = contentResolver.query(contentUri, arrayOf(column), null, null, null)
|
||||||
|
if (cursor == null || !cursor.moveToFirst()) {
|
||||||
|
Log.w(LOG_TAG, "failed to get cursor for contentUri=$contentUri column=$column")
|
||||||
|
} else {
|
||||||
value = when (cursor.getType(0)) {
|
value = when (cursor.getType(0)) {
|
||||||
Cursor.FIELD_TYPE_NULL -> null
|
Cursor.FIELD_TYPE_NULL -> null
|
||||||
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
|
Cursor.FIELD_TYPE_INTEGER -> cursor.getLong(0)
|
||||||
|
@ -59,10 +64,12 @@ object ContextUtils {
|
||||||
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
|
Cursor.FIELD_TYPE_BLOB -> cursor.getBlob(0)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.w(LOG_TAG, "failed to get value for contentUri=$contentUri key=$prop", e)
|
|
||||||
}
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue