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

View file

@ -50,21 +50,3 @@ sealed interface IndexingState {
*/ */
data class Completed(val error: Exception?) : 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 package org.oxycblt.auxio.music
import android.content.Context
import android.content.pm.PackageManager
import androidx.core.content.ContextCompat
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.music.MusicRepository.IndexingWorker
import org.oxycblt.auxio.music.info.Name import org.oxycblt.auxio.music.info.Name
import org.oxycblt.auxio.music.metadata.Separators import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.stack.Indexer import org.oxycblt.auxio.music.stack.Indexer
@ -163,7 +159,7 @@ interface MusicRepository {
* @param withCache Whether to load with the music cache or not. * @param withCache Whether to load with the music cache or not.
* @return The top-level music loading [Job] started. * @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. */ /** A listener for changes to the stored music information. */
interface UpdateListener { interface UpdateListener {
@ -191,12 +187,6 @@ interface MusicRepository {
/** A persistent worker that can load music in the background. */ /** A persistent worker that can load music in the background. */
interface IndexingWorker { 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 * Request that the music loading process ([index]) should be started. Any prior loads
* should be canceled. * should be canceled.
@ -327,12 +317,9 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
indexingWorker?.requestIndex(withCache) indexingWorker?.requestIndex(withCache)
} }
override fun index(worker: MusicRepository.IndexingWorker, withCache: Boolean) = override suspend fun index(worker: IndexingWorker, withCache: Boolean) {
worker.scope.launch { indexWrapper(worker.workerContext, this, withCache) }
private suspend fun indexWrapper(context: Context, scope: CoroutineScope, withCache: Boolean) {
try { try {
indexImpl(context, scope, withCache) indexImpl(withCache)
} catch (e: CancellationException) { } catch (e: CancellationException) {
// Got cancelled, propagate upwards to top-level co-routine. // Got cancelled, propagate upwards to top-level co-routine.
L.d("Loading routine was cancelled") 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) { private suspend fun indexImpl(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()
}
// Obtain configuration information // Obtain configuration information
val separators = Separators.from(musicSettings.separators) val separators = Separators.from(musicSettings.separators)
val nameFactory = val nameFactory =
@ -363,9 +342,10 @@ constructor(private val indexer: Indexer, private val musicSettings: MusicSettin
} else { } else {
Name.Known.SimpleFactory Name.Known.SimpleFactory
} }
val uris = musicSettings.musicLocations
val newLibrary = 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 // We want to make sure that all reads and writes are synchronized due to the sheer
// amount of consumers of MusicRepository. // amount of consumers of MusicRepository.

View file

@ -19,6 +19,7 @@
package org.oxycblt.auxio.music package org.oxycblt.auxio.music
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.core.content.edit import androidx.core.content.edit
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject import javax.inject.Inject
@ -36,6 +37,7 @@ import timber.log.Timber as L
interface MusicSettings : Settings<MusicSettings.Listener> { interface MusicSettings : Settings<MusicSettings.Listener> {
/** The configuration on how to handle particular directories in the music library. */ /** The configuration on how to handle particular directories in the music library. */
var musicDirs: MusicDirectories var musicDirs: MusicDirectories
var musicLocations: List<Uri>
/** Whether to exclude non-music audio files from the music library. */ /** Whether to exclude non-music audio files from the music library. */
val excludeNonMusic: Boolean val excludeNonMusic: Boolean
/** Whether to be actively watching for changes in the music library. */ /** 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 override val excludeNonMusic: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_exclude_non_music), true) 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.ForegroundListener import org.oxycblt.auxio.ForegroundListener
import org.oxycblt.auxio.ForegroundServiceNotification import org.oxycblt.auxio.ForegroundServiceNotification
@ -38,7 +39,7 @@ import timber.log.Timber as L
class IndexingHolder class IndexingHolder
private constructor( private constructor(
override val workerContext: Context, private val workerContext: Context,
private val foregroundListener: ForegroundListener, private val foregroundListener: ForegroundListener,
private val playbackManager: PlaybackStateManager, private val playbackManager: PlaybackStateManager,
private val musicRepository: MusicRepository, private val musicRepository: MusicRepository,
@ -130,11 +131,10 @@ private constructor(
// Cancel the previous music loading job. // Cancel the previous music loading job.
currentIndexJob?.cancel() currentIndexJob?.cancel()
// Start a new music loading job on a co-routine. // 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() { override fun onIndexingStateChanged() {
foregroundListener.updateForeground(ForegroundListener.Change.INDEXER) foregroundListener.updateForeground(ForegroundListener.Change.INDEXER)
val state = musicRepository.indexingState 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.Music
import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song 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 { interface MutableLibrary : Library {
suspend fun createPlaylist(name: String, songs: List<Song>): MutableLibrary 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_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_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_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_separators" translatable="false">auxio_separators</string>
<string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string> <string name="set_key_auto_sort_names" translatable="false">auxio_auto_sort_names</string>