diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index 0a5a6b574..201fde2a8 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -49,14 +49,7 @@ import org.oxycblt.auxio.home.list.GenreListFragment import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.tabs.AdaptiveTabStrategy import org.oxycblt.auxio.list.selection.SelectionFragment -import org.oxycblt.auxio.music.Album -import org.oxycblt.auxio.music.Artist -import org.oxycblt.auxio.music.Genre -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.music.MusicMode -import org.oxycblt.auxio.music.MusicViewModel -import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.music.Sort +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.ui.NavigationViewModel @@ -322,7 +315,7 @@ class HomeFragment : private fun updateIndexerState(state: Indexer.State?) { val binding = requireBinding() when (state) { - is Indexer.State.Complete -> setupCompleteState(binding, state.response) + is Indexer.State.Complete -> setupCompleteState(binding, state.result) is Indexer.State.Indexing -> setupIndexingState(binding, state.indexing) null -> { logD("Indexer is in indeterminate state") @@ -331,54 +324,43 @@ class HomeFragment : } } - private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) { - if (response is Indexer.Response.Ok) { + private fun setupCompleteState( + binding: FragmentHomeBinding, + result: Result + ) { + if (result.isSuccess) { logD("Received ok response") binding.homeFab.show() binding.homeIndexingContainer.visibility = View.INVISIBLE } else { logD("Received non-ok response") val context = requireContext() + val throwable = unlikelyToBeNull(result.exceptionOrNull()) binding.homeIndexingContainer.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.INVISIBLE - when (response) { - is Indexer.Response.Err -> { - logD("Updating UI to Response.Err state") - binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed) - // Configure the action to act as a reload trigger. - binding.homeIndexingAction.apply { - visibility = View.VISIBLE - text = context.getString(R.string.lbl_retry) - setOnClickListener { musicModel.refresh() } + if (throwable is Indexer.NoPermissionException) { + logD("Updating UI to permission request state") + binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms) + // Configure the action to act as a permission launcher. + binding.homeIndexingAction.apply { + visibility = View.VISIBLE + text = context.getString(R.string.lbl_grant) + setOnClickListener { + requireNotNull(storagePermissionLauncher) { + "Permission launcher was not available" + } + .launch(Indexer.PERMISSION_READ_AUDIO) } } - is Indexer.Response.NoMusic -> { - // TODO: Move this state to the list fragments (quality of life) - logD("Updating UI to Response.NoMusic state") - binding.homeIndexingStatus.text = context.getString(R.string.err_no_music) - // Configure the action to act as a reload trigger. - binding.homeIndexingAction.apply { - visibility = View.VISIBLE - text = context.getString(R.string.lbl_retry) - setOnClickListener { musicModel.refresh() } - } + } else { + logD("Updating UI to error state") + binding.homeIndexingStatus.text = context.getString(R.string.err_index_failed) + // Configure the action to act as a reload trigger. + binding.homeIndexingAction.apply { + visibility = View.VISIBLE + text = context.getString(R.string.lbl_retry) + setOnClickListener { musicModel.refresh() } } - is Indexer.Response.NoPerms -> { - logD("Updating UI to Response.NoPerms state") - binding.homeIndexingStatus.text = context.getString(R.string.err_no_perms) - // Configure the action to act as a permission launcher. - binding.homeIndexingAction.apply { - visibility = View.VISIBLE - text = context.getString(R.string.lbl_grant) - setOnClickListener { - requireNotNull(storagePermissionLauncher) { - "Permission launcher was not available" - } - .launch(Indexer.PERMISSION_READ_AUDIO) - } - } - } - else -> {} } } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt index bb6205dc5..8df230e71 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt @@ -48,9 +48,9 @@ class MusicViewModel : ViewModel(), Indexer.Listener { override fun onIndexerStateChanged(state: Indexer.State?) { _indexerState.value = state - if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) { + if (state is Indexer.State.Complete) { // New state is a completed library, update the statistics values. - val library = state.response.library + val library = state.result.getOrNull() ?: return _statistics.value = Statistics( library.songs.size, diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt index ba9e3c9a0..6a8d24456 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/Indexer.kt @@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.logW * @author Alexander Capehart (OxygenCobalt) */ class Indexer private constructor() { - private var lastResponse: Response? = null + private var lastResponse: Result? = null private var indexingState: Indexing? = null private var controller: Controller? = null private var listener: Listener? = null @@ -148,28 +148,14 @@ class Indexer private constructor() { * be written, but no cache entries will be loaded into the new library. */ suspend fun index(context: Context, withCache: Boolean) { - if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == - PackageManager.PERMISSION_DENIED) { - // No permissions, signal that we can't do anything. - emitCompletion(Response.NoPerms) - return - } - - val response = + val result = try { val start = System.currentTimeMillis() val library = indexImpl(context, withCache) - if (library != null) { - // Successfully loaded a library. - logD( - "Music indexing completed successfully in " + - "${System.currentTimeMillis() - start}ms") - Response.Ok(library) - } else { - // Loaded a library, but it contained no music. - logE("No music found") - Response.NoMusic - } + logD( + "Music indexing completed successfully in " + + "${System.currentTimeMillis() - start}ms") + Result.success(library) } catch (e: CancellationException) { // Got cancelled, propagate upwards to top-level co-routine. logD("Loading routine was cancelled") @@ -178,10 +164,9 @@ class Indexer private constructor() { // Music loading process failed due to something we have not handled. logE("Music indexing failed") logE(e.stackTraceToString()) - Response.Err(e) + Result.failure(e) } - - emitCompletion(response) + emitCompletion(result) } /** @@ -212,9 +197,15 @@ class Indexer private constructor() { * @param context [Context] required to load music. * @param withCache Whether to use the cache or not when loading. If false, the cache will still * be written, but no cache entries will be loaded into the new library. - * @return A newly-loaded [MusicStore.Library], or null if nothing was loaded. + * @return A newly-loaded [MusicStore.Library]. May be empty. */ - private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library? { + private suspend fun indexImpl(context: Context, withCache: Boolean): MusicStore.Library { + if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) == + PackageManager.PERMISSION_DENIED) { + // No permissions, signal that we can't do anything. + throw NoPermissionException() + } + // Create the chain of extractors. Each extractor builds on the previous and // enables version-specific features in order to create the best possible music // experience. @@ -237,11 +228,6 @@ class Indexer private constructor() { val metadataExtractor = MetadataExtractor(context, mediaStoreExtractor) val songs = buildSongs(metadataExtractor, Settings(context)) - if (songs.isEmpty()) { - // No songs, nothing else to do. - return null - } - // Build the rest of the music library from the song list. This is much more powerful // and reliable compared to using MediaStore to obtain grouping information. val buildStart = System.currentTimeMillis() @@ -249,7 +235,6 @@ class Indexer private constructor() { val artists = buildArtists(songs, albums) val genres = buildGenres(songs) logD("Successfully built library in ${System.currentTimeMillis() - buildStart}ms") - return MusicStore.Library(songs, albums, artists, genres) } @@ -395,10 +380,10 @@ class Indexer private constructor() { * Emit a new [State.Complete] state. This can be used to signal the completion of the music * loading process to external code. Will check if the callee has not been canceled and thus has * the ability to emit a new state - * @param response The new [Response] to emit, representing the outcome of the music loading + * @param result The new [Response] to emit, representing the outcome of the music loading * process. */ - private suspend fun emitCompletion(response: Response) { + private suspend fun emitCompletion(result: Result) { yield() // Swap to the Main thread so that downstream callbacks don't crash from being on // a background thread. Does not occur in emitIndexing due to efficiency reasons. @@ -406,10 +391,10 @@ class Indexer private constructor() { synchronized(this) { // Do not check for redundancy here, as we actually need to notify a switch // from Indexing -> Complete and not Indexing -> Indexing or Complete -> Complete. - lastResponse = response + lastResponse = result indexingState = null // Signal that the music loading process has been completed. - val state = State.Complete(response) + val state = State.Complete(result) controller?.onIndexerStateChanged(state) listener?.onIndexerStateChanged(state) } @@ -427,10 +412,10 @@ class Indexer private constructor() { /** * Music loading has completed. - * @param response The outcome of the music loading process. + * @param result The outcome of the music loading process. * @see Response */ - data class Complete(val response: Response) : State() + data class Complete(val result: Result) : State() } /** @@ -451,25 +436,10 @@ class Indexer private constructor() { class Songs(val current: Int, val total: Int) : Indexing() } - /** Represents the possible outcomes of the music loading process. */ - sealed class Response { - /** - * Music load was successful and produced a [MusicStore.Library]. - * @param library The loaded [MusicStore.Library]. - */ - data class Ok(val library: MusicStore.Library) : Response() - - /** - * Music loading encountered an unexpected error. - * @param throwable The error thrown. - */ - data class Err(val throwable: Throwable) : Response() - - /** Music loading occurred, but resulted in no music. */ - object NoMusic : Response() - - /** Music loading could not occur due to a lack of storage permissions. */ - object NoPerms : Response() + /** Thrown when the required permissions to load the music library have not been granted yet. */ + class NoPermissionException : Exception() { + override val message: String + get() = "Not granted permissions to load music library" } /** diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 428e125b2..2f89bbe3b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -129,11 +129,11 @@ class IndexerService : override fun onIndexerStateChanged(state: Indexer.State?) { when (state) { + is Indexer.State.Indexing -> updateActiveSession(state.indexing) is Indexer.State.Complete -> { - if (state.response is Indexer.Response.Ok && - state.response.library != musicStore.library) { + val newLibrary = state.result.getOrNull() + if (newLibrary != null && newLibrary != musicStore.library) { logD("Applying new library") - val newLibrary = state.response.library // We only care if the newly-loaded library is going to replace a previously // loaded library. if (musicStore.library != null) { @@ -152,9 +152,6 @@ class IndexerService : // handled right now. updateIdleSession() } - is Indexer.State.Indexing -> { - updateActiveSession(state.indexing) - } null -> { // Null is the indeterminate state that occurs on app startup or after // the cancellation of a load, so in that case we want to stop foreground