music: integrate new loader into services

This commit is contained in:
Alexander Capehart 2024-11-26 13:55:37 -07:00
parent b0c6dd2b74
commit 4618996fc5
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 43 additions and 95 deletions

View file

@ -62,9 +62,6 @@ import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicType
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.NoAudioPermissionException
import org.oxycblt.auxio.music.NoMusicException
import org.oxycblt.auxio.music.PERMISSION_READ_AUDIO
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.PlaylistDecision
import org.oxycblt.auxio.music.PlaylistMessage
@ -331,48 +328,19 @@ class HomeFragment :
binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.INVISIBLE
binding.homeIndexingActions.visibility = View.VISIBLE
when (error) {
is NoAudioPermissionException -> {
L.d("Showing permission prompt")
binding.homeIndexingStatus.setText(R.string.err_no_perms)
// Configure the action to act as a permission launcher.
binding.homeIndexingTry.apply {
text = context.getString(R.string.lbl_grant)
setOnClickListener {
requireNotNull(storagePermissionLauncher) {
"Permission launcher was not available"
}
.launch(PERMISSION_READ_AUDIO)
}
}
binding.homeIndexingMore.visibility = View.GONE
}
is NoMusicException -> {
L.d("Showing no music error")
binding.homeIndexingStatus.setText(R.string.err_no_music)
// Configure the action to act as a reload trigger.
binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.refresh() }
}
binding.homeIndexingMore.visibility = View.GONE
}
else -> {
L.d("Showing generic error")
binding.homeIndexingStatus.setText(R.string.err_index_failed)
// Configure the action to act as a reload trigger.
binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.rescan() }
}
binding.homeIndexingMore.apply {
visibility = View.VISIBLE
setOnClickListener {
findNavController().navigateSafe(HomeFragmentDirections.reportError(error))
}
}
L.d("Showing generic error")
binding.homeIndexingStatus.setText(R.string.err_index_failed)
// Configure the action to act as a reload trigger.
binding.homeIndexingTry.apply {
visibility = View.VISIBLE
text = context.getString(R.string.lbl_retry)
setOnClickListener { musicModel.rescan() }
}
binding.homeIndexingMore.apply {
visibility = View.VISIBLE
setOnClickListener {
findNavController().navigateSafe(HomeFragmentDirections.reportError(error))
}
}
}

View file

@ -50,21 +50,3 @@ sealed interface IndexingState {
*/
data class Completed(val error: Exception?) : IndexingState
}
/**
* Thrown by the music loader when [PERMISSION_READ_AUDIO] was not granted.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class NoAudioPermissionException : Exception() {
override val message = "Storage permissions are required to load music"
}
/**
* Thrown when no music was found.
*
* @author Alexander Capehart (OxygenCobalt)
*/
class NoMusicException : Exception() {
override val message = "No music was found on the device"
}

View file

@ -18,17 +18,13 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.Indexer
@ -163,7 +159,7 @@ interface MusicRepository {
* @param withCache Whether to load with the music cache or not.
* @return The top-level music loading [Job] started.
*/
fun index(worker: IndexingWorker, withCache: Boolean): Job
suspend fun index(worker: IndexingWorker, withCache: Boolean)
/** A listener for changes to the stored music information. */
interface UpdateListener {
@ -191,12 +187,6 @@ interface MusicRepository {
/** A persistent worker that can load music in the background. */
interface IndexingWorker {
/** A [Context] required to read device storage */
val workerContext: Context
/** The [CoroutineScope] to perform coroutine music loading work on. */
val scope: CoroutineScope
/**
* Request that the music loading process ([index]) should be started. Any prior loads
* should be canceled.
@ -327,12 +317,9 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
indexingWorker?.requestIndex(withCache)
}
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) =
worker.scope.launch { indexWrapper(worker.workerContext, this, withCache) }
private suspend fun indexWrapper(context: Context, scope: CoroutineScope, withCache: Boolean) {
override suspend fun index(worker: IndexingWorker, withCache: Boolean) {
try {
indexImpl(context, scope, withCache)
indexImpl(withCache)
} catch (e: CancellationException) {
// Got cancelled, propagate upwards to top-level co-routine.
L.d("Loading routine was cancelled")
@ -346,15 +333,7 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
}
}
private suspend fun indexImpl(context: Context, scope: CoroutineScope, withCache: Boolean) {
// Make sure we have permissions before going forward. Theoretically this would be better
// done at the UI level, but that intertwines logic and display too much.
if (ContextCompat.checkSelfPermission(context, PERMISSION_READ_AUDIO) ==
PackageManager.PERMISSION_DENIED) {
L.e("Permissions were not granted")
throw NoAudioPermissionException()
}
private suspend fun indexImpl(withCache: Boolean) {
// Obtain configuration information
val separators = Separators.from(musicSettings.separators)
val nameFactory =
@ -363,9 +342,10 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
} else {
Name.Known.SimpleFactory
}
val uris = musicSettings.musicLocations
val newLibrary =
indexer.run(listOf(), Interpretation(nameFactory, separators), ::emitIndexingProgress)
indexer.run(uris, Interpretation(nameFactory, separators), ::emitIndexingProgress)
// We want to make sure that all reads and writes are synchronized due to the sheer
// amount of consumers of MusicRepository.

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music
import android.content.Context
import android.net.Uri
import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
@ -36,6 +37,7 @@ import timber.log.Timber as L
interface MusicSettings : Settings<MusicSettings.Listener> {
/** The configuration on how to handle particular directories in the music library. */
var musicDirs: MusicDirectories
var musicLocations: List<Uri>
/** Whether to exclude non-music audio files from the music library. */
val excludeNonMusic: Boolean
/** Whether to be actively watching for changes in the music library. */
@ -79,6 +81,21 @@ constructor(
}
}
override var musicLocations: List<Uri>
get() {
val dirs =
sharedPreferences.getStringSet(getString(R.string.set_key_music_locations), null)
?: emptySet()
return dirs.map { Uri.parse(it) }
}
set(value) {
sharedPreferences.edit {
putStringSet(
getString(R.string.set_key_music_locations), value.map(Uri::toString).toSet())
apply()
}
}
override val excludeNonMusic: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true)

View file

@ -25,6 +25,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.ForegroundListener
import org.oxycblt.auxio.ForegroundServiceNotification
@ -38,7 +39,7 @@ import timber.log.Timber as L
class IndexingHolder
private constructor(
override val workerContext: Context,
private val workerContext: Context,
private val foregroundListener: ForegroundListener,
private val playbackManager: PlaybackStateManager,
private val musicRepository: MusicRepository,
@ -130,11 +131,10 @@ private constructor(
// Cancel the previous music loading job.
currentIndexJob?.cancel()
// Start a new music loading job on a co-routine.
currentIndexJob = musicRepository.index(this, withCache)
currentIndexJob =
indexScope.launch { musicRepository.index(this@IndexingHolder, withCache) }
}
override val scope = indexScope
override fun onIndexingStateChanged() {
foregroundListener.updateForeground(ForegroundListener.Change.INDEXER)
val state = musicRepository.indexingState

View file

@ -22,7 +22,7 @@ import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.stack.explore.fs.Path
import org.oxycblt.auxio.music.stack.explore.fs.Pathi
interface MutableLibrary : Library {
suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary

View file

@ -18,6 +18,7 @@
<string name="set_key_square_covers" translatable="false">auxio_square_covers</string>
<string name="set_key_music_dirs_include" translatable="false">auxio_include_dirs</string>
<string name="set_key_exclude_non_music" translatable="false">auxio_exclude_non_music</string>
<string name="set_key_music_locations" translatable="false">auxio_music_locations</string>
<string name="set_key_separators" translatable="false">auxio_separators</string>
<string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string>