diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt index 893aca1f5..56cec51f5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ImageFileHandler.kt @@ -74,7 +74,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { } } - private fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { + private suspend fun getImageEntry(call: MethodCall, result: MethodChannel.Result) { val mimeType = call.argument("mimeType") // MIME type is optional val uri = call.argument("uri")?.let { Uri.parse(it) } if (uri == null) { @@ -94,7 +94,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler { }) } - private fun rename(call: MethodCall, result: MethodChannel.Result) { + private suspend fun rename(call: MethodCall, result: MethodChannel.Result) { val entryMap = call.argument("entry") val newName = call.argument("newName") if (entryMap == null || newName == null) { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt index d8d2c15b2..5f473165a 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageOpStreamHandler.kt @@ -62,7 +62,7 @@ class ImageOpStreamHandler(private val context: Context, private val arguments: handler.post { eventSink.endOfStream() } } - private fun move() { + private suspend fun move() { if (arguments !is Map<*, *> || entryMapList.isEmpty()) { endOfStream() return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt index 62e542d9b..ec2aa52c5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/MediaStoreStreamHandler.kt @@ -40,7 +40,7 @@ class MediaStoreStreamHandler(private val context: Context, arguments: Any?) : E handler.post { eventSink.endOfStream() } } - private fun fetchAll() { + private suspend fun fetchAll() { MediaStoreImageProvider().fetchAll(context, knownEntries ?: emptyMap()) { success(it) } endOfStream() } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt index 99b0f168e..006e61e2f 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ContentImageProvider.kt @@ -6,7 +6,7 @@ import android.provider.MediaStore import deckers.thibault.aves.model.SourceImageEntry internal class ContentImageProvider : ImageProvider() { - override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { if (mimeType == null) { callback.onFailure(Exception("MIME type is null for uri=$uri")) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt index 49faa641f..f47adb072 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/FileImageProvider.kt @@ -6,7 +6,7 @@ import deckers.thibault.aves.model.SourceImageEntry import java.io.File internal class FileImageProvider : ImageProvider() { - override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { if (mimeType == null) { callback.onFailure(Exception("MIME type is null for uri=$uri")) return diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index f8272734c..38d8e5fc8 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -21,9 +21,12 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.util.* +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine abstract class ImageProvider { - open fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + open suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { callback.onFailure(UnsupportedOperationException()) } @@ -31,11 +34,11 @@ abstract class ImageProvider { return Futures.immediateFailedFuture(UnsupportedOperationException()) } - open fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { + open suspend fun moveMultiple(context: Context, copy: Boolean, destinationDir: String, entries: List, callback: ImageOpCallback) { callback.onFailure(UnsupportedOperationException()) } - fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { + suspend fun rename(context: Context, oldPath: String, oldMediaUri: Uri, mimeType: String, newFilename: String, callback: ImageOpCallback) { val oldFile = File(oldPath) val newFile = File(oldFile.parent, newFilename) if (oldFile == newFile) { @@ -57,7 +60,11 @@ abstract class ImageProvider { } MediaScannerConnection.scanFile(context, arrayOf(oldPath), arrayOf(mimeType), null) - scanNewPath(context, newFile.path, mimeType, callback) + try { + callback.onSuccess(scanNewPath(context, newFile.path, mimeType)) + } catch (e: Exception) { + callback.onFailure(e) + } } fun changeOrientation(context: Context, path: String, uri: Uri, mimeType: String, op: ExifOrientationOp, callback: ImageOpCallback) { @@ -132,54 +139,56 @@ abstract class ImageProvider { } } - protected fun scanNewPath(context: Context, path: String, mimeType: String, callback: ImageOpCallback) { - MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? -> - var contentId: Long = 0 - var contentUri: Uri? = null - if (newUri != 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") - contentId = ContentUris.parseId(newUri) - if (isImage(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) - } else if (isVideo(mimeType)) { - contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) + protected suspend fun scanNewPath(context: Context, path: String, mimeType: String): FieldMap = + suspendCoroutine { cont -> + MediaScannerConnection.scanFile(context, arrayOf(path), arrayOf(mimeType)) { _, newUri: Uri? -> + var contentId: Long = 0 + var contentUri: Uri? = null + if (newUri != 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") + contentId = ContentUris.parseId(newUri) + if (isImage(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentId) + } else if (isVideo(mimeType)) { + contentUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentId) + } + } + if (contentUri == null) { + cont.resumeWithException(Exception("failed to get content URI of item at path=$path")) + return@scanFile } - } - if (contentUri == null) { - callback.onFailure(Exception("failed to get content URI of item at path=$path")) - return@scanFile - } - val newFields = HashMap() - // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store - val projection = arrayOf( - MediaStore.MediaColumns.DATE_MODIFIED, - MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.TITLE, - ) - try { - val cursor = context.contentResolver.query(contentUri, projection, null, null, null) - if (cursor != null && cursor.moveToFirst()) { - newFields["uri"] = contentUri.toString() - newFields["contentId"] = contentId - newFields["path"] = path - cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } - cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) } - cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) } - cursor.close() + val newFields = HashMap() + // we retrieve updated fields as the renamed/moved file became a new entry in the Media Store + val projection = arrayOf( + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.TITLE, + ) + try { + val cursor = context.contentResolver.query(contentUri, projection, null, null, null) + if (cursor != null && cursor.moveToFirst()) { + newFields["uri"] = contentUri.toString() + newFields["contentId"] = contentId + newFields["path"] = path + cursor.getColumnIndex(MediaStore.MediaColumns.DATE_MODIFIED).let { if (it != -1) newFields["dateModifiedSecs"] = cursor.getInt(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME).let { if (it != -1) newFields["displayName"] = cursor.getString(it) } + cursor.getColumnIndex(MediaStore.MediaColumns.TITLE).let { if (it != -1) newFields["title"] = cursor.getString(it) } + cursor.close() + } + } catch (e: Exception) { + cont.resumeWithException(e) + return@scanFile + } + + if (newFields.isEmpty()) { + cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri")) + } else { + cont.resume(newFields) } - } catch (e: Exception) { - callback.onFailure(e) - return@scanFile - } - if (newFields.isEmpty()) { - callback.onFailure(Exception("failed to get item details from provider at contentUri=$contentUri")) - } else { - callback.onSuccess(newFields) } } - } interface ImageOpCallback { fun onSuccess(fields: FieldMap) 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 eec2b34c1..2a92a0e26 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 @@ -20,13 +20,14 @@ import deckers.thibault.aves.utils.StorageUtils.createDirectoryIfAbsent import deckers.thibault.aves.utils.StorageUtils.ensureTrailingSeparator import deckers.thibault.aves.utils.StorageUtils.getDocumentFile import deckers.thibault.aves.utils.StorageUtils.requireAccessPermission +import kotlinx.coroutines.delay import java.io.File import java.io.FileNotFoundException import java.util.* import java.util.concurrent.ExecutionException class MediaStoreImageProvider : ImageProvider() { - fun fetchAll(context: Context, knownEntries: Map, handleNewEntry: NewEntryHandler) { + suspend fun fetchAll(context: Context, knownEntries: Map, handleNewEntry: NewEntryHandler) { val isModified = fun(contentId: Int, dateModifiedSecs: Int): Boolean { val knownDate = knownEntries[contentId] return knownDate == null || knownDate < dateModifiedSecs @@ -35,7 +36,7 @@ class MediaStoreImageProvider : ImageProvider() { fetchFrom(context, isModified, handleNewEntry, VIDEO_CONTENT_URI, VIDEO_PROJECTION) } - override fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { + override suspend fun fetchSingle(context: Context, uri: Uri, mimeType: String?, callback: ImageOpCallback) { val id = ContentUris.parseId(uri) val onSuccess = fun(entry: FieldMap) { entry["uri"] = uri.toString() @@ -84,7 +85,7 @@ class MediaStoreImageProvider : ImageProvider() { return foundContentIds } - private fun fetchFrom( + private suspend fun fetchFrom( context: Context, isValidEntry: NewEntryChecker, handleNewEntry: NewEntryHandler, @@ -159,7 +160,7 @@ class MediaStoreImageProvider : ImageProvider() { handleNewEntry(entryMap) // TODO TLAD is this necessary? if (newEntryCount % 30 == 0) { - Thread.sleep(10) + delay(10) } newEntryCount++ } @@ -212,7 +213,7 @@ class MediaStoreImageProvider : ImageProvider() { return future } - override fun moveMultiple( + override suspend fun moveMultiple( context: Context, copy: Boolean, destinationDir: String, @@ -266,7 +267,7 @@ class MediaStoreImageProvider : ImageProvider() { } } - private fun moveSingleByTreeDocAndScan( + private suspend fun moveSingleByTreeDocAndScan( context: Context, sourcePath: String, sourceUri: Uri, @@ -322,16 +323,13 @@ class MediaStoreImageProvider : ImageProvider() { } } - scanNewPath(context, destinationFullPath, mimeType, object : ImageOpCallback { - override fun onSuccess(fields: FieldMap) { - fields["deletedSource"] = deletedSource - future.set(fields) - } - - override fun onFailure(throwable: Throwable) { - future.setException(throwable) - } - }) + try { + val fields = scanNewPath(context, destinationFullPath, mimeType) + fields["deletedSource"] = deletedSource + future.set(fields) + } catch (e: Exception) { + future.setException(e) + } } } catch (e: Exception) { Log.e(LOG_TAG, "failed to ${(if (copy) "copy" else "move")} entry", e)