From c98ca8712f627c869272fcbfa566da99173b6fe5 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Thu, 11 May 2023 12:56:25 -0600 Subject: [PATCH] music: re-add fallible async execution Re-add the tryAsync wrapper function to the loading process to properly handle music loading errors. Turns out there is basically no other way to properly do this except for the insane exception -> Result -> exception hack. --- .../oxycblt/auxio/music/MusicRepository.kt | 42 ++++++++++++++----- .../auxio/music/fs/MediaStoreExtractor.kt | 1 - .../auxio/music/metadata/TagExtractor.kt | 2 - .../java/org/oxycblt/auxio/util/LangUtil.kt | 18 ++++---- 4 files changed, 40 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index ac0bf498b..2dfc66d80 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -25,15 +25,19 @@ import java.util.* import javax.inject.Inject import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel +import org.oxycblt.auxio.R import org.oxycblt.auxio.music.cache.CacheRepository import org.oxycblt.auxio.music.device.DeviceLibrary import org.oxycblt.auxio.music.device.RawSong import org.oxycblt.auxio.music.fs.MediaStoreExtractor import org.oxycblt.auxio.music.metadata.TagExtractor import org.oxycblt.auxio.music.user.UserLibrary +import org.oxycblt.auxio.util.fallible import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logW +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * Primary manager of music information and loading. @@ -272,14 +276,14 @@ constructor( // Do the initial query of the cache and media databases in parallel. logD("Starting queries") - val mediaStoreQueryJob = worker.scope.async { mediaStoreExtractor.query() } + val mediaStoreQueryJob = worker.scope.tryAsync { mediaStoreExtractor.query() } val cache = if (withCache) { cacheRepository.readCache() } else { null } - val query = mediaStoreQueryJob.await() + val query = mediaStoreQueryJob.await().getOrThrow() // Now start processing the queried song information in parallel. Songs that can't be // received from the cache are consisted incomplete and pushed to a separate channel @@ -288,11 +292,15 @@ constructor( val completeSongs = Channel(Channel.UNLIMITED) val incompleteSongs = Channel(Channel.UNLIMITED) val mediaStoreJob = - worker.scope.async { - mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) + worker.scope.tryAsync { + mediaStoreExtractor.consume(query, cache, incompleteSongs, completeSongs) }.also { + incompleteSongs.close() } val metadataJob = - worker.scope.async { tagExtractor.consume(incompleteSongs, completeSongs) } + worker.scope.tryAsync { + tagExtractor.consume(incompleteSongs, completeSongs) + completeSongs.close() + } // Await completed raw songs as they are processed. val rawSongs = LinkedList() @@ -301,8 +309,8 @@ constructor( emitLoading(IndexingProgress.Songs(rawSongs.size, query.projectedTotal)) } // These should be no-ops - mediaStoreJob.await() - metadataJob.await() + mediaStoreJob.await().getOrThrow() + metadataJob.await().getOrThrow() if (rawSongs.isEmpty()) { logE("Music library was empty") @@ -315,21 +323,33 @@ constructor( emitLoading(IndexingProgress.Indeterminate) val deviceLibraryChannel = Channel() val deviceLibraryJob = - worker.scope.async(Dispatchers.Main) { + worker.scope.tryAsync(Dispatchers.Main) { deviceLibraryFactory.create(rawSongs).also { deviceLibraryChannel.send(it) } } - val userLibraryJob = worker.scope.async { userLibraryFactory.read(deviceLibraryChannel) } + val userLibraryJob = worker.scope.tryAsync { userLibraryFactory.read(deviceLibraryChannel) } if (cache == null || cache.invalidated) { cacheRepository.writeCache(rawSongs) } - val deviceLibrary = deviceLibraryJob.await() - val userLibrary = userLibraryJob.await() + val deviceLibrary = deviceLibraryJob.await().getOrThrow() + val userLibrary = userLibraryJob.await().getOrThrow() withContext(Dispatchers.Main) { emitComplete(null) emitData(deviceLibrary, userLibrary) } } + private inline fun CoroutineScope.tryAsync( + context: CoroutineContext = EmptyCoroutineContext, + crossinline block: suspend () -> R + ) = + async(context) { + try { + Result.success(block()) + } catch (e: Exception) { + Result.failure(e) + } + } + private suspend fun emitLoading(progress: IndexingProgress) { yield() synchronized(this) { diff --git a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt index 4603af11e..0df80983b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/fs/MediaStoreExtractor.kt @@ -210,7 +210,6 @@ private abstract class BaseMediaStoreExtractor( // Free the cursor and signal that no more incomplete songs will be produced by // this extractor. query.close() - incompleteSongs.close() } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt index 7e31400ff..bbc2971a0 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/metadata/TagExtractor.kt @@ -87,8 +87,6 @@ class TagExtractorImpl @Inject constructor(private val tagWorkerFactory: TagWork } } } while (ongoingTasks) - - completeSongs.close() } private companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt index 047c24c21..bb68cecee 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/LangUtil.kt @@ -35,16 +35,16 @@ fun unlikelyToBeNull(value: T?) = } /** - * Require that the given data is a specific type [T]. - * - * @param data The data to check. - * @return A data casted to [T]. - * @throws IllegalStateException If the data cannot be casted to [T]. + * Maps a try expression to a [Result]. + * @param block The code to execute + * @return A [Result] representing the outcome of [block]'s execution. */ -inline fun requireIs(data: Any?): T { - check(data is T) { "Unexpected datatype: ${data?.let { it::class.simpleName }}" } - return data -} +inline fun fallible(block: () -> R) = + try { + Result.success(block()) + } catch (e: Exception) { + Result.failure(e) + } /** * Aliases a check to ensure that the given number is non-zero.