diff --git a/CHANGELOG.md b/CHANGELOG.md
index 120d0bf6b..ad89e996e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
## [v1.7.1] - 2022-10-09
diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
index d13143269..1e92c2e86 100644
--- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
+++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt
@@ -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()
- 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, 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()
+ 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} = ?"
diff --git a/pubspec.lock b/pubspec.lock
index ac700ef9d..c5bc51a83 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -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: