fixed failing scan of items copied to SD card on older devices
This commit is contained in:
parent
1ae2ceb62a
commit
2db168051e
3 changed files with 82 additions and 59 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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} = ?"
|
||||
|
|
18
pubspec.lock
18
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:
|
||||
|
|
Loading…
Reference in a new issue