music: integrate new loader into services
This commit is contained in:
parent
b0c6dd2b74
commit
4618996fc5
7 changed files with 43 additions and 95 deletions
|
@ -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,34 +328,7 @@ 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 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")
|
L.d("Showing generic error")
|
||||||
binding.homeIndexingStatus.setText(R.string.err_index_failed)
|
binding.homeIndexingStatus.setText(R.string.err_index_failed)
|
||||||
// Configure the action to act as a reload trigger.
|
// Configure the action to act as a reload trigger.
|
||||||
|
@ -374,8 +344,6 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupIndexingState(binding: FragmentHomeBinding, progress: IndexingProgress) {
|
private fun setupIndexingState(binding: FragmentHomeBinding, progress: IndexingProgress) {
|
||||||
// Remove all content except for the progress indicator.
|
// Remove all content except for the progress indicator.
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue