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.