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: