fixed failing scan of items copied to SD card on older devices

This commit is contained in:
Thibault Deckers 2022-10-22 20:05:57 +02:00
parent 1ae2ceb62a
commit 2db168051e
3 changed files with 82 additions and 59 deletions

View file

@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file.
### Fixed
- rendering of panoramas with inconsistent metadata
- failing scan of items copied to SD card on older devices
## <a id="v1.7.1"></a>[v1.7.1] - 2022-10-09

View file

@ -32,6 +32,7 @@ import java.io.File
import java.io.OutputStream
import java.util.*
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
@ -784,61 +785,82 @@ class MediaStoreImageProvider : ImageProvider() {
}
suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap =
suspendCoroutine { cont ->
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
fun scanUri(uri: Uri?): FieldMap? {
uri ?: return null
suspendCoroutine { cont -> tryScanNewPath(context, path = path, mimeType = mimeType, cont) }
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf(
MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val newFields = HashMap<String, Any?>()
newFields["uri"] = uri.toString()
newFields["contentId"] = uri.tryParseId()
newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.close()
return newFields
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
}
return null
}
if (newUri != null) {
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, contentId)
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields)
} else {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
}
} else {
cont.resumeWithException(Exception("failed to get URI of item at path=$path"))
private fun tryScanNewPath(context: Context, path: String, mimeType: String, cont: Continuation<FieldMap>, iteration: Int = 0) {
// `scanFile` may (e.g. when copying to SD card on Android 10 (API 29)):
// 1) yield no URI,
// 2) yield a temporary URI that fails when queried,
// 3) yield a temporary URI that succeeds when queried right away, but the Media Store actually won't have an entry for it until device reboot.
if (iteration > 5) {
// give up
cont.resumeWithException(Exception("failed to scan new path=$path after $iteration iterations"))
return
} else if (iteration > 0) {
// waiting and retrying just once usually works out for cases 1) and 2)
Thread.sleep(iteration * 100L)
} else if (iteration == 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// waiting before the first scan usually works out for case 3)
StorageUtils.getVolumePath(context, path)?.let { volumePath ->
if (volumePath != StorageUtils.getPrimaryVolumePath(context)) {
Thread.sleep(100L)
}
}
}
MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? ->
fun scanUri(uri: Uri?): FieldMap? {
uri ?: return null
// we retrieve updated fields as the renamed/moved file became a new entry in the Media Store
val projection = arrayOf(
MediaStore.MediaColumns.DATE_ADDED,
MediaStore.MediaColumns.DATE_MODIFIED,
)
try {
val cursor = context.contentResolver.query(uri, projection, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
val newFields = HashMap<String, Any?>()
newFields["uri"] = uri.toString()
newFields["contentId"] = uri.tryParseId()
newFields["path"] = path
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_ADDED).let { if (it != -1) newFields["dateAddedSecs"] = cursor.getInt(it) }
cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) }
cursor.close()
return newFields
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to scan uri=$uri", e)
}
return null
}
if (newUri != null) {
var contentUri: Uri? = null
// `newURI` is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872")
// but we need an image/video media URI (e.g. "content://media/external/images/media/62872")
val contentId = newUri.tryParseId()
if (contentId != null) {
if (isImage(mimeType)) {
contentUri = ContentUris.withAppendedId(IMAGE_CONTENT_URI, contentId)
} else if (isVideo(mimeType)) {
contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, contentId)
}
}
// prefer image/video content URI, fallback to original URI (possibly a file content URI)
val newFields = scanUri(contentUri) ?: scanUri(newUri)
if (newFields != null) {
cont.resume(newFields)
return@scanFile
}
}
tryScanNewPath(context, path = path, mimeType = mimeType, cont, iteration + 1)
}
}
fun getContentUriForPath(context: Context, path: String): Uri? {
val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaColumns.PATH} = ?"

View file

@ -14,7 +14,7 @@ packages:
name: _flutterfire_internals
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
version: "1.0.5"
analyzer:
dependency: transitive
description:
@ -126,14 +126,14 @@ packages:
name: cloud_firestore_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "5.8.1"
version: "5.8.2"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
collection:
dependency: "direct main"
description:
@ -147,7 +147,7 @@ packages:
name: connectivity_plus
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
connectivity_plus_platform_interface:
dependency: transitive
description:
@ -284,7 +284,7 @@ packages:
name: firebase_core
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0"
firebase_core_platform_interface:
dependency: transitive
description:
@ -305,14 +305,14 @@ packages:
name: firebase_crashlytics
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
version: "3.0.2"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.3.2"
version: "3.3.3"
flex_color_picker:
dependency: "direct main"
description:
@ -545,7 +545,7 @@ packages:
name: lints
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.0.1"
lists:
dependency: transitive
description:
@ -724,7 +724,7 @@ packages:
name: pdf
url: "https://pub.dartlang.org"
source: hosted
version: "3.8.3"
version: "3.8.4"
percent_indicator:
dependency: "direct main"
description: