music: rework callback configuration
Rework the asynchronous aspects of MusicStore to rely on a more conventional callback system. In preparation for automatic rescanning, it would be more elegant for music consumers to use a callback, so that updates can be registered easier than if a co-routine continued to be used. Convert MusicStore to this new model and rework the accesses of MusicStore in-app to line up with this new system.
This commit is contained in:
parent
3a19d822ce
commit
180faa6f50
14 changed files with 218 additions and 257 deletions
|
@ -38,7 +38,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||||
import org.oxycblt.auxio.util.logW
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
* A wrapper around the home fragment that shows the playback fragment and controls the more
|
||||||
|
@ -122,32 +122,30 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
|
||||||
|
|
||||||
// Error, show the error to the user
|
// Error, show the error to the user
|
||||||
is MusicStore.Response.Err -> {
|
is MusicStore.Response.Err -> {
|
||||||
logW("Received Error")
|
logD("Received Response.Err")
|
||||||
|
Snackbar.make(binding.root, R.string.err_load_failed, Snackbar.LENGTH_INDEFINITE)
|
||||||
val errorRes =
|
.apply {
|
||||||
when (response.kind) {
|
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
|
||||||
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
|
show()
|
||||||
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
|
|
||||||
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val snackbar =
|
is MusicStore.Response.NoMusic -> {
|
||||||
Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE)
|
logD("Received Response.NoMusic")
|
||||||
|
Snackbar.make(binding.root, R.string.err_no_music, Snackbar.LENGTH_INDEFINITE)
|
||||||
when (response.kind) {
|
.apply {
|
||||||
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
|
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
|
||||||
snackbar.setAction(R.string.lbl_retry) {
|
show()
|
||||||
musicModel.reloadMusic(requireContext())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
MusicStore.ErrorKind.NO_PERMS -> {
|
}
|
||||||
snackbar.setAction(R.string.lbl_grant) {
|
is MusicStore.Response.NoPerms -> {
|
||||||
|
logD("Received Response.NoPerms")
|
||||||
|
Snackbar.make(binding.root, R.string.err_no_perms, Snackbar.LENGTH_INDEFINITE)
|
||||||
|
.apply {
|
||||||
|
setAction(R.string.lbl_grant) {
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
}
|
}
|
||||||
|
show()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.show()
|
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ViewModel that stores data for the [DetailFragment]s. This includes:
|
* ViewModel that stores data for the [DetailFragment]s. This includes:
|
||||||
|
@ -40,6 +41,7 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel() {
|
class DetailViewModel : ViewModel() {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mCurrentAlbum = MutableLiveData<Album?>()
|
private val mCurrentAlbum = MutableLiveData<Album?>()
|
||||||
|
@ -87,9 +89,9 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
fun setAlbumId(id: Long) {
|
fun setAlbumId(id: Long) {
|
||||||
if (mCurrentAlbum.value?.id == id) return
|
if (mCurrentAlbum.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val album =
|
val album =
|
||||||
requireNotNull(musicStore.albums.find { it.id == id }) { "Invalid album id provided " }
|
requireNotNull(library.albums.find { it.id == id }) { "Invalid album id provided " }
|
||||||
|
|
||||||
mCurrentAlbum.value = album
|
mCurrentAlbum.value = album
|
||||||
refreshAlbumData(album)
|
refreshAlbumData(album)
|
||||||
|
@ -97,16 +99,18 @@ class DetailViewModel : ViewModel() {
|
||||||
|
|
||||||
fun setArtistId(id: Long) {
|
fun setArtistId(id: Long) {
|
||||||
if (mCurrentArtist.value?.id == id) return
|
if (mCurrentArtist.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val artist = requireNotNull(musicStore.artists.find { it.id == id }) {}
|
val artist =
|
||||||
|
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
|
||||||
mCurrentArtist.value = artist
|
mCurrentArtist.value = artist
|
||||||
refreshArtistData(artist)
|
refreshArtistData(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setGenreId(id: Long) {
|
fun setGenreId(id: Long) {
|
||||||
if (mCurrentGenre.value?.id == id) return
|
if (mCurrentGenre.value?.id == id) return
|
||||||
val musicStore = MusicStore.requireInstance()
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
val genre = requireNotNull(musicStore.genres.find { it.id == id })
|
val genre =
|
||||||
|
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
|
||||||
mCurrentGenre.value = genre
|
mCurrentGenre.value = genre
|
||||||
refreshGenreData(genre)
|
refreshGenreData(genre)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ package org.oxycblt.auxio.home
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.oxycblt.auxio.home.tabs.Tab
|
import org.oxycblt.auxio.home.tabs.Tab
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
|
@ -38,7 +36,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
|
* The ViewModel for managing [HomeFragment]'s data, sorting modes, and tab state.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
class HomeViewModel : ViewModel(), SettingsManager.Callback, MusicStore.Callback {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mSongs = MutableLiveData(listOf<Song>())
|
private val mSongs = MutableLiveData(listOf<Song>())
|
||||||
|
@ -78,15 +77,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
val fastScrolling: LiveData<Boolean> = mFastScrolling
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
musicStore.addCallback(this)
|
||||||
settingsManager.addCallback(this)
|
settingsManager.addCallback(this)
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
val musicStore = MusicStore.awaitInstance()
|
|
||||||
mSongs.value = settingsManager.libSongSort.songs(musicStore.songs)
|
|
||||||
mAlbums.value = settingsManager.libAlbumSort.albums(musicStore.albums)
|
|
||||||
mArtists.value = settingsManager.libArtistSort.artists(musicStore.artists)
|
|
||||||
mGenres.value = settingsManager.libGenreSort.genres(musicStore.genres)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the current tab based off of the new ViewPager position. */
|
/** Update the current tab based off of the new ViewPager position. */
|
||||||
|
@ -142,6 +134,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
|
|
||||||
// --- OVERRIDES ---
|
// --- OVERRIDES ---
|
||||||
|
|
||||||
|
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||||
|
if (response is MusicStore.Response.Ok) {
|
||||||
|
val library = response.library
|
||||||
|
mSongs.value = settingsManager.libSongSort.songs(library.songs)
|
||||||
|
mAlbums.value = settingsManager.libAlbumSort.albums(library.albums)
|
||||||
|
mArtists.value = settingsManager.libArtistSort.artists(library.artists)
|
||||||
|
mGenres.value = settingsManager.libGenreSort.genres(library.genres)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
|
override fun onLibTabsUpdate(libTabs: Array<Tab>) {
|
||||||
tabs = visibleTabs
|
tabs = visibleTabs
|
||||||
mRecreateTabs.value = true
|
mRecreateTabs.value = true
|
||||||
|
@ -149,6 +151,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
|
musicStore.addCallback(this)
|
||||||
settingsManager.removeCallback(this)
|
settingsManager.removeCallback(this)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,15 +89,24 @@ import org.oxycblt.auxio.util.logD
|
||||||
*
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicLoader {
|
object Indexer {
|
||||||
data class Library(
|
/**
|
||||||
val genres: List<Genre>,
|
* The album_artist MediaStore field has existed since at least API 21, but until API 30 it was
|
||||||
val artists: List<Artist>,
|
* a proprietary extension for Google Play Music and was not documented. Since this field
|
||||||
val albums: List<Album>,
|
* probably works on all versions Auxio supports, we suppress the warning about using a
|
||||||
val songs: List<Song>
|
* possibly-unsupported constant.
|
||||||
)
|
*/
|
||||||
|
@Suppress("InlinedApi")
|
||||||
|
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
||||||
|
|
||||||
fun load(context: Context): Library? {
|
/**
|
||||||
|
* Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See
|
||||||
|
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
||||||
|
*/
|
||||||
|
private val Context.contentResolverSafe: ContentResolver
|
||||||
|
get() = applicationContext.contentResolver
|
||||||
|
|
||||||
|
fun run(context: Context): MusicStore.Library? {
|
||||||
val songs = loadSongs(context)
|
val songs = loadSongs(context)
|
||||||
if (songs.isEmpty()) return null
|
if (songs.isEmpty()) return null
|
||||||
|
|
||||||
|
@ -118,16 +127,9 @@ class MusicLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Library(genres, artists, albums, songs)
|
return MusicStore.Library(genres, artists, albums, songs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a content resolver in a way that does not mangle metadata on certain OEM skins. See
|
|
||||||
* https://github.com/OxygenCobalt/Auxio/issues/50 for more info.
|
|
||||||
*/
|
|
||||||
private val Context.contentResolverSafe: ContentResolver
|
|
||||||
get() = applicationContext.contentResolver
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the initial query over the song database, including excluded directory checks. The songs
|
* Does the initial query over the song database, including excluded directory checks. The songs
|
||||||
* returned by this function are **not** well-formed. The companion [buildAlbums],
|
* returned by this function are **not** well-formed. The companion [buildAlbums],
|
||||||
|
@ -404,15 +406,4 @@ class MusicLoader {
|
||||||
|
|
||||||
return genreSongs.ifEmpty { null }
|
return genreSongs.ifEmpty { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* The album_artist MediaStore field has existed since at least API 21, but until API 30 it
|
|
||||||
* was a proprietary extension for Google Play Music and was not documented. Since this
|
|
||||||
* field probably works on all versions Auxio supports, we suppress the warning about using
|
|
||||||
* a possibly-unsupported constant.
|
|
||||||
*/
|
|
||||||
@Suppress("InlinedApi")
|
|
||||||
private const val AUDIO_COLUMN_ALBUM_ARTIST = MediaStore.Audio.AudioColumns.ALBUM_ARTIST
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -29,9 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
/**
|
/** [Item] variant that represents a music item. */
|
||||||
* [Item] variant that represents a music item.
|
|
||||||
*/
|
|
||||||
sealed class Music : Item() {
|
sealed class Music : Item() {
|
||||||
/** The raw name of this item. Null if unknown. */
|
/** The raw name of this item. Null if unknown. */
|
||||||
abstract val rawName: String?
|
abstract val rawName: String?
|
||||||
|
@ -116,8 +114,8 @@ data class Song(
|
||||||
get() = internalMediaStoreArtistName ?: album.artist.rawName
|
get() = internalMediaStoreArtistName ?: album.artist.rawName
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the artist name for this song in particular. First uses the artist tag, and
|
* Resolve the artist name for this song in particular. First uses the artist tag, and then
|
||||||
* then falls back to the album artist tag (i.e parent artist name)
|
* falls back to the album artist tag (i.e parent artist name)
|
||||||
*/
|
*/
|
||||||
fun resolveIndividualArtistName(context: Context) =
|
fun resolveIndividualArtistName(context: Context) =
|
||||||
internalMediaStoreArtistName ?: album.artist.resolveName(context)
|
internalMediaStoreArtistName ?: album.artist.resolveName(context)
|
||||||
|
|
|
@ -38,76 +38,95 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MusicStore private constructor() {
|
class MusicStore private constructor() {
|
||||||
private var mGenres = listOf<Genre>()
|
private var response: Response? = null
|
||||||
val genres: List<Genre>
|
val library: Library?
|
||||||
get() = mGenres
|
get() =
|
||||||
|
response?.let { currentResponse ->
|
||||||
|
if (currentResponse is Response.Ok) {
|
||||||
|
currentResponse.library
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var mArtists = listOf<Artist>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
val artists: List<Artist>
|
|
||||||
get() = mArtists
|
|
||||||
|
|
||||||
private var mAlbums = listOf<Album>()
|
fun addCallback(callback: Callback) {
|
||||||
val albums: List<Album>
|
callbacks.add(callback)
|
||||||
get() = mAlbums
|
}
|
||||||
|
|
||||||
private var mSongs = listOf<Song>()
|
fun removeCallback(callback: Callback) {
|
||||||
val songs: List<Song>
|
callbacks.remove(callback)
|
||||||
get() = mSongs
|
}
|
||||||
|
|
||||||
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
/** Load/Sort the entire music library. Should always be ran on a coroutine. */
|
||||||
private fun load(context: Context): Response {
|
suspend fun index(context: Context): Response {
|
||||||
logD("Starting initial music load")
|
logD("Starting initial music load")
|
||||||
|
val newResponse = withContext(Dispatchers.IO) { indexImpl(context) }.also { response = it }
|
||||||
|
for (callback in callbacks) {
|
||||||
|
callback.onMusicUpdate(newResponse)
|
||||||
|
}
|
||||||
|
return newResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun indexImpl(context: Context): Response {
|
||||||
val notGranted =
|
val notGranted =
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||||
PackageManager.PERMISSION_DENIED
|
PackageManager.PERMISSION_DENIED
|
||||||
|
|
||||||
if (notGranted) {
|
if (notGranted) {
|
||||||
return Response.Err(ErrorKind.NO_PERMS)
|
return Response.NoPerms
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
val response =
|
||||||
val start = System.currentTimeMillis()
|
try {
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
val library = Indexer.run(context)
|
||||||
|
if (library != null) {
|
||||||
|
logD(
|
||||||
|
"Music load completed successfully in ${System.currentTimeMillis() - start}ms")
|
||||||
|
Response.Ok(library)
|
||||||
|
} else {
|
||||||
|
logE("No music found")
|
||||||
|
Response.NoMusic
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logE("Music loading failed.")
|
||||||
|
logE(e.stackTraceToString())
|
||||||
|
Response.Err(e)
|
||||||
|
}
|
||||||
|
|
||||||
val loader = MusicLoader()
|
return response
|
||||||
val library = loader.load(context) ?: return Response.Err(ErrorKind.NO_MUSIC)
|
|
||||||
|
|
||||||
mSongs = library.songs
|
|
||||||
mAlbums = library.albums
|
|
||||||
mArtists = library.artists
|
|
||||||
mGenres = library.genres
|
|
||||||
|
|
||||||
logD("Music load completed successfully in ${System.currentTimeMillis() - start}ms")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logE("Music loading failed.")
|
|
||||||
logE(e.stackTraceToString())
|
|
||||||
return Response.Err(ErrorKind.FAILED)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.Ok(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find a song in a faster manner using an ID for its album as well. */
|
data class Library(
|
||||||
fun findSongFast(songId: Long, albumId: Long): Song? {
|
val genres: List<Genre>,
|
||||||
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
|
val artists: List<Artist>,
|
||||||
}
|
val albums: List<Album>,
|
||||||
|
val songs: List<Song>
|
||||||
/**
|
) {
|
||||||
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
|
/** Find a song in a faster manner using an ID for its album as well. */
|
||||||
* uri.
|
fun findSongFast(songId: Long, albumId: Long): Song? {
|
||||||
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
|
||||||
*/
|
|
||||||
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
|
||||||
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor
|
|
||||||
->
|
|
||||||
cursor.moveToFirst()
|
|
||||||
val fileName =
|
|
||||||
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
|
||||||
|
|
||||||
return songs.find { it.fileName == fileName }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
/**
|
||||||
|
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content
|
||||||
|
* uri.
|
||||||
|
* @return The corresponding [Song] for this [uri], null if there isn't one.
|
||||||
|
*/
|
||||||
|
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
||||||
|
resolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use {
|
||||||
|
cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
val fileName =
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||||
|
|
||||||
|
return songs.find { it.fileName == fileName }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,93 +136,31 @@ class MusicStore private constructor() {
|
||||||
* TODO: Add the exception to the "FAILED" ErrorKind
|
* TODO: Add the exception to the "FAILED" ErrorKind
|
||||||
*/
|
*/
|
||||||
sealed class Response {
|
sealed class Response {
|
||||||
class Ok(val musicStore: MusicStore) : Response()
|
class Ok(val library: Library) : Response()
|
||||||
class Err(val kind: ErrorKind) : Response()
|
class Err(throwable: Throwable) : Response()
|
||||||
|
object NoMusic : Response()
|
||||||
|
object NoPerms : Response()
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class ErrorKind {
|
interface Callback {
|
||||||
NO_PERMS,
|
fun onMusicUpdate(response: Response)
|
||||||
NO_MUSIC,
|
|
||||||
FAILED
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Volatile private var RESPONSE: Response? = null
|
@Volatile private var INSTANCE: MusicStore? = null
|
||||||
|
|
||||||
/**
|
fun getInstance(): MusicStore {
|
||||||
* Initialize the loading process for this instance. This must be ran on a background
|
val currentInstance = INSTANCE
|
||||||
* thread. If the instance has already been loaded successfully, then it will be returned
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
suspend fun initInstance(context: Context): Response {
|
|
||||||
val currentInstance = RESPONSE
|
|
||||||
|
|
||||||
if (currentInstance is Response.Ok) {
|
if (currentInstance != null) {
|
||||||
return currentInstance
|
return currentInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
val response =
|
synchronized(this) {
|
||||||
withContext(Dispatchers.IO) {
|
val newInstance = MusicStore()
|
||||||
val response = MusicStore().load(context)
|
INSTANCE = newInstance
|
||||||
synchronized(this) { RESPONSE = response }
|
return newInstance
|
||||||
response
|
|
||||||
}
|
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Await the successful creation of a [MusicStore] instance. The co-routine calling this
|
|
||||||
* will block until the successful creation of a [MusicStore], in which it will then be
|
|
||||||
* returned.
|
|
||||||
*/
|
|
||||||
suspend fun awaitInstance() =
|
|
||||||
withContext(Dispatchers.Default) {
|
|
||||||
// We have to do a withContext call so we don't block the JVM thread
|
|
||||||
val musicStore: MusicStore
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val response = RESPONSE
|
|
||||||
|
|
||||||
if (response is Response.Ok) {
|
|
||||||
musicStore = response.musicStore
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
musicStore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Maybe get a MusicStore instance. This is useful if you are running code while the loading
|
|
||||||
* process may still be going on.
|
|
||||||
*
|
|
||||||
* @return null if the music store instance is still loading or if the loading process has
|
|
||||||
* encountered an error. An instance is returned otherwise.
|
|
||||||
*/
|
|
||||||
fun maybeGetInstance(): MusicStore? {
|
|
||||||
val currentInstance = RESPONSE
|
|
||||||
|
|
||||||
return if (currentInstance is Response.Ok) {
|
|
||||||
currentInstance.musicStore
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Require a MusicStore instance. This function is dangerous and should only be used if it's
|
|
||||||
* guaranteed that the caller's code will only be called after the initial loading process.
|
|
||||||
*/
|
|
||||||
fun requireInstance(): MusicStore {
|
|
||||||
return requireNotNull(maybeGetInstance()) {
|
|
||||||
"Required MusicStore instance was not available"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Check if this instance has successfully loaded or not. */
|
|
||||||
fun loaded(): Boolean {
|
|
||||||
return maybeGetInstance() != null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,18 @@ import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
class MusicViewModel : ViewModel() {
|
class MusicViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
|
||||||
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
private val mLoaderResponse = MutableLiveData<MusicStore.Response?>(null)
|
||||||
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
|
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
|
||||||
|
|
||||||
private var isBusy = false
|
private var isBusy = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
musicStore.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate the loading process. This is done here since HomeFragment will be the first fragment
|
* Initiate the loading process. This is done here since HomeFragment will be the first fragment
|
||||||
* navigated to and because SnackBars will have the best UX here.
|
* navigated to and because SnackBars will have the best UX here.
|
||||||
|
@ -45,7 +51,7 @@ class MusicViewModel : ViewModel() {
|
||||||
mLoaderResponse.value = null
|
mLoaderResponse.value = null
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = MusicStore.initInstance(context)
|
val result = musicStore.index(context)
|
||||||
mLoaderResponse.value = result
|
mLoaderResponse.value = result
|
||||||
isBusy = false
|
isBusy = false
|
||||||
}
|
}
|
||||||
|
@ -56,4 +62,13 @@ class MusicViewModel : ViewModel() {
|
||||||
mLoaderResponse.value = null
|
mLoaderResponse.value = null
|
||||||
loadMusic(context)
|
loadMusic(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMusicUpdate(response: MusicStore.Response) {
|
||||||
|
mLoaderResponse.value = response
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
musicStore.removeCallback(this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,10 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS.
|
* - DO NOT REWRITE IT! THAT'S BAD AND WILL PROBABLY RE-INTRODUCE A TON OF BUGS.
|
||||||
*/
|
*/
|
||||||
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
private val playbackManager = PlaybackStateManager.getInstance()
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
private val mSong = MutableLiveData<Song?>()
|
private val mSong = MutableLiveData<Song?>()
|
||||||
private val mParent = MutableLiveData<MusicParent?>()
|
private val mParent = MutableLiveData<MusicParent?>()
|
||||||
|
@ -94,9 +98,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
val playbackMode: LiveData<PlaybackMode>
|
val playbackMode: LiveData<PlaybackMode>
|
||||||
get() = mMode
|
get() = mMode
|
||||||
|
|
||||||
private val playbackManager = PlaybackStateManager.getInstance()
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
playbackManager.addCallback(this)
|
playbackManager.addCallback(this)
|
||||||
|
|
||||||
|
@ -166,7 +167,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
*/
|
*/
|
||||||
fun playWithUri(uri: Uri, context: Context) {
|
fun playWithUri(uri: Uri, context: Context) {
|
||||||
// Check if everything is already running to run the URI play
|
// Check if everything is already running to run the URI play
|
||||||
if (playbackManager.isRestored && MusicStore.loaded()) {
|
if (playbackManager.isRestored && musicStore.library != null) {
|
||||||
playWithUriInternal(uri, context)
|
playWithUriInternal(uri, context)
|
||||||
} else {
|
} else {
|
||||||
logD("Cant play this URI right now, waiting")
|
logD("Cant play this URI right now, waiting")
|
||||||
|
@ -178,9 +179,8 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
||||||
/** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */
|
/** Play with a file URI. This is called after [playWithUri] once its deemed safe to do so. */
|
||||||
private fun playWithUriInternal(uri: Uri, context: Context) {
|
private fun playWithUriInternal(uri: Uri, context: Context) {
|
||||||
logD("Playing with uri $uri")
|
logD("Playing with uri $uri")
|
||||||
|
val library = musicStore.library ?: return
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
library.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
|
||||||
musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shuffle all songs */
|
/** Shuffle all songs */
|
||||||
|
|
|
@ -105,7 +105,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
* @param musicStore Required to transform database songs/parents into actual instances
|
* @param musicStore Required to transform database songs/parents into actual instances
|
||||||
* @return The stored [SavedState], null if there isn't one.
|
* @return The stored [SavedState], null if there isn't one.
|
||||||
*/
|
*/
|
||||||
fun readState(musicStore: MusicStore): SavedState? {
|
fun readState(library: MusicStore.Library): SavedState? {
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
|
||||||
var state: SavedState? = null
|
var state: SavedState? = null
|
||||||
|
@ -124,16 +124,16 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
val song =
|
val song =
|
||||||
cursor.getLongOrNull(songIndex)?.let { id -> musicStore.songs.find { it.id == id } }
|
cursor.getLongOrNull(songIndex)?.let { id -> library.songs.find { it.id == id } }
|
||||||
|
|
||||||
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
|
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
|
||||||
|
|
||||||
val parent =
|
val parent =
|
||||||
cursor.getLongOrNull(parentIndex)?.let { id ->
|
cursor.getLongOrNull(parentIndex)?.let { id ->
|
||||||
when (mode) {
|
when (mode) {
|
||||||
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id }
|
PlaybackMode.IN_GENRE -> library.genres.find { it.id == id }
|
||||||
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id }
|
PlaybackMode.IN_ARTIST -> library.artists.find { it.id == id }
|
||||||
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id }
|
PlaybackMode.IN_ALBUM -> library.albums.find { it.id == id }
|
||||||
PlaybackMode.ALL_SONGS -> null
|
PlaybackMode.ALL_SONGS -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
* Read a list of queue items from this database.
|
* Read a list of queue items from this database.
|
||||||
* @param musicStore Required to transform database songs into actual song instances
|
* @param musicStore Required to transform database songs into actual song instances
|
||||||
*/
|
*/
|
||||||
fun readQueue(musicStore: MusicStore): MutableList<Song> {
|
fun readQueue(library: MusicStore.Library): MutableList<Song> {
|
||||||
requireBackgroundThread()
|
requireBackgroundThread()
|
||||||
|
|
||||||
val queue = mutableListOf<Song>()
|
val queue = mutableListOf<Song>()
|
||||||
|
@ -198,8 +198,10 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
|
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
|
library.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let {
|
||||||
?.let { song -> queue.add(song) }
|
song ->
|
||||||
|
queue.add(song)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logE
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
// Playback
|
// Playback
|
||||||
private var mSong: Song? = null
|
private var mSong: Song? = null
|
||||||
set(value) {
|
set(value) {
|
||||||
|
@ -124,8 +127,6 @@ class PlaybackStateManager private constructor() {
|
||||||
val hasPlayed: Boolean
|
val hasPlayed: Boolean
|
||||||
get() = mHasPlayed
|
get() = mHasPlayed
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
|
||||||
|
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
@ -154,8 +155,7 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
when (mode) {
|
when (mode) {
|
||||||
PlaybackMode.ALL_SONGS -> {
|
PlaybackMode.ALL_SONGS -> {
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val musicStore = musicStore.library ?: return
|
||||||
|
|
||||||
mParent = null
|
mParent = null
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = musicStore.songs.toMutableList()
|
||||||
}
|
}
|
||||||
|
@ -211,10 +211,11 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
/** Shuffle all songs. */
|
/** Shuffle all songs. */
|
||||||
fun shuffleAll() {
|
fun shuffleAll() {
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
logD("RETARD. ${musicStore.library}")
|
||||||
|
val library = musicStore.library ?: return
|
||||||
|
|
||||||
mPlaybackMode = PlaybackMode.ALL_SONGS
|
mPlaybackMode = PlaybackMode.ALL_SONGS
|
||||||
mQueue = musicStore.songs.toMutableList()
|
mQueue = library.songs.toMutableList()
|
||||||
mParent = null
|
mParent = null
|
||||||
|
|
||||||
setShuffling(true, keepSong = false)
|
setShuffling(true, keepSong = false)
|
||||||
|
@ -370,13 +371,13 @@ class PlaybackStateManager private constructor() {
|
||||||
* @param keepSong Whether the current song should be kept as the queue is un-shuffled
|
* @param keepSong Whether the current song should be kept as the queue is un-shuffled
|
||||||
*/
|
*/
|
||||||
private fun resetShuffle(keepSong: Boolean) {
|
private fun resetShuffle(keepSong: Boolean) {
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val library = musicStore.library ?: return
|
||||||
val lastSong = mSong
|
val lastSong = mSong
|
||||||
|
|
||||||
mQueue =
|
mQueue =
|
||||||
when (mPlaybackMode) {
|
when (mPlaybackMode) {
|
||||||
PlaybackMode.ALL_SONGS ->
|
PlaybackMode.ALL_SONGS ->
|
||||||
settingsManager.libSongSort.songs(musicStore.songs).toMutableList()
|
settingsManager.libSongSort.songs(library.songs).toMutableList()
|
||||||
PlaybackMode.IN_ALBUM ->
|
PlaybackMode.IN_ALBUM ->
|
||||||
settingsManager.detailAlbumSort.album(mParent as Album).toMutableList()
|
settingsManager.detailAlbumSort.album(mParent as Album).toMutableList()
|
||||||
PlaybackMode.IN_ARTIST ->
|
PlaybackMode.IN_ARTIST ->
|
||||||
|
@ -496,7 +497,7 @@ class PlaybackStateManager private constructor() {
|
||||||
suspend fun restoreFromDatabase(context: Context) {
|
suspend fun restoreFromDatabase(context: Context) {
|
||||||
logD("Getting state from DB")
|
logD("Getting state from DB")
|
||||||
|
|
||||||
val musicStore = MusicStore.maybeGetInstance() ?: return
|
val library = musicStore.library ?: return
|
||||||
val start: Long
|
val start: Long
|
||||||
val playbackState: PlaybackStateDatabase.SavedState?
|
val playbackState: PlaybackStateDatabase.SavedState?
|
||||||
val queue: MutableList<Song>
|
val queue: MutableList<Song>
|
||||||
|
@ -504,8 +505,8 @@ class PlaybackStateManager private constructor() {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
start = System.currentTimeMillis()
|
start = System.currentTimeMillis()
|
||||||
val database = PlaybackStateDatabase.getInstance(context)
|
val database = PlaybackStateDatabase.getInstance(context)
|
||||||
playbackState = database.readState(musicStore)
|
playbackState = database.readState(library)
|
||||||
queue = database.readQueue(musicStore)
|
queue = database.readQueue(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
// Get off the IO coroutine since it will cause LiveData updates to throw an exception
|
||||||
|
|
|
@ -76,6 +76,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
|
||||||
*
|
*
|
||||||
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
|
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
|
||||||
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
|
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
|
||||||
|
*
|
||||||
|
* TODO: Move restore and file opening to service
|
||||||
*/
|
*/
|
||||||
class PlaybackService :
|
class PlaybackService :
|
||||||
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {
|
||||||
|
|
|
@ -115,11 +115,6 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
|
||||||
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
searchModel.setNavigating(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.searchRecycler.adapter = null
|
binding.searchRecycler.adapter = null
|
||||||
|
|
|
@ -40,8 +40,10 @@ import org.oxycblt.auxio.util.logD
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchViewModel : ViewModel() {
|
class SearchViewModel : ViewModel() {
|
||||||
|
private val musicStore = MusicStore.getInstance()
|
||||||
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
private val mSearchResults = MutableLiveData(listOf<Item>())
|
private val mSearchResults = MutableLiveData(listOf<Item>())
|
||||||
private var mIsNavigating = false
|
|
||||||
private var mFilterMode: DisplayMode? = null
|
private var mFilterMode: DisplayMode? = null
|
||||||
private var mLastQuery: String? = null
|
private var mLastQuery: String? = null
|
||||||
|
|
||||||
|
@ -51,8 +53,6 @@ class SearchViewModel : ViewModel() {
|
||||||
val filterMode: DisplayMode?
|
val filterMode: DisplayMode?
|
||||||
get() = mFilterMode
|
get() = mFilterMode
|
||||||
|
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
mFilterMode = settingsManager.searchFilterMode
|
mFilterMode = settingsManager.searchFilterMode
|
||||||
}
|
}
|
||||||
|
@ -61,10 +61,10 @@ class SearchViewModel : ViewModel() {
|
||||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||||
*/
|
*/
|
||||||
fun search(context: Context, query: String?) {
|
fun search(context: Context, query: String?) {
|
||||||
val musicStore = MusicStore.maybeGetInstance()
|
|
||||||
mLastQuery = query
|
mLastQuery = query
|
||||||
|
|
||||||
if (query.isNullOrEmpty() || musicStore == null) {
|
val library = musicStore.library
|
||||||
|
if (query.isNullOrEmpty() || library == null) {
|
||||||
logD("No music/query, ignoring search")
|
logD("No music/query, ignoring search")
|
||||||
mSearchResults.value = listOf()
|
mSearchResults.value = listOf()
|
||||||
return
|
return
|
||||||
|
@ -80,28 +80,28 @@ class SearchViewModel : ViewModel() {
|
||||||
// Note: a filter mode of null means to not filter at all.
|
// Note: a filter mode of null means to not filter at all.
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ARTISTS) {
|
||||||
musicStore.artists.filterByOrNull(context, query)?.let { artists ->
|
library.artists.filterByOrNull(context, query)?.let { artists ->
|
||||||
results.add(Header(-1, R.string.lbl_artists))
|
results.add(Header(-1, R.string.lbl_artists))
|
||||||
results.addAll(sort.artists(artists))
|
results.addAll(sort.artists(artists))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_ALBUMS) {
|
||||||
musicStore.albums.filterByOrNull(context, query)?.let { albums ->
|
library.albums.filterByOrNull(context, query)?.let { albums ->
|
||||||
results.add(Header(-2, R.string.lbl_albums))
|
results.add(Header(-2, R.string.lbl_albums))
|
||||||
results.addAll(sort.albums(albums))
|
results.addAll(sort.albums(albums))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_GENRES) {
|
||||||
musicStore.genres.filterByOrNull(context, query)?.let { genres ->
|
library.genres.filterByOrNull(context, query)?.let { genres ->
|
||||||
results.add(Header(-3, R.string.lbl_genres))
|
results.add(Header(-3, R.string.lbl_genres))
|
||||||
results.addAll(sort.genres(genres))
|
results.addAll(sort.genres(genres))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
if (mFilterMode == null || mFilterMode == DisplayMode.SHOW_SONGS) {
|
||||||
musicStore.songs.filterByOrNull(context, query)?.let { songs ->
|
library.songs.filterByOrNull(context, query)?.let { songs ->
|
||||||
results.add(Header(-4, R.string.lbl_songs))
|
results.add(Header(-4, R.string.lbl_songs))
|
||||||
results.addAll(sort.songs(songs))
|
results.addAll(sort.songs(songs))
|
||||||
}
|
}
|
||||||
|
@ -181,9 +181,4 @@ class SearchViewModel : ViewModel() {
|
||||||
|
|
||||||
return sb.toString()
|
return sb.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update the current navigation status to [isNavigating] */
|
|
||||||
fun setNavigating(isNavigating: Boolean) {
|
|
||||||
mIsNavigating = isNavigating
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<org.oxycblt.auxio.coil.StyledImageView
|
<org.oxycblt.auxio.coil.StyledImageView
|
||||||
android:id="@+id/playback_cover"
|
android:id="@+id/playback_cover"
|
||||||
style="@style/Widget.Auxio.Image.Full"
|
style="@style/Widget.Auxio.Image.Full"
|
||||||
android:layout_margin="@dimen/spacing_large"
|
android:layout_margin="@dimen/spacing_mid_large"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
style="@style/Widget.Auxio.TextView.Primary"
|
style="@style/Widget.Auxio.TextView.Primary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
|
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -43,8 +43,8 @@
|
||||||
style="@style/Widget.Auxio.TextView.Secondary"
|
style="@style/Widget.Auxio.TextView.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
app:layout_constraintBottom_toTopOf="@+id/playback_album"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -55,8 +55,8 @@
|
||||||
style="@style/Widget.Auxio.TextView.Secondary"
|
style="@style/Widget.Auxio.TextView.Secondary"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:layout_marginEnd="@dimen/spacing_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
@ -108,7 +108,7 @@
|
||||||
android:id="@+id/playback_loop"
|
android:id="@+id/playback_loop"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
android:contentDescription="@string/desc_change_loop"
|
android:contentDescription="@string/desc_change_loop"
|
||||||
android:src="@drawable/ic_loop"
|
android:src="@drawable/ic_loop"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
android:id="@+id/playback_skip_prev"
|
android:id="@+id/playback_skip_prev"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_large"
|
android:layout_marginEnd="@dimen/spacing_mid_large"
|
||||||
android:contentDescription="@string/desc_skip_prev"
|
android:contentDescription="@string/desc_skip_prev"
|
||||||
android:src="@drawable/ic_skip_prev"
|
android:src="@drawable/ic_skip_prev"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
|
@ -133,7 +133,7 @@
|
||||||
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
|
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="@dimen/spacing_large"
|
android:layout_marginBottom="@dimen/spacing_mid_large"
|
||||||
android:contentDescription="@string/desc_play_pause"
|
android:contentDescription="@string/desc_play_pause"
|
||||||
android:src="@drawable/sel_playing_state"
|
android:src="@drawable/sel_playing_state"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
@ -145,7 +145,7 @@
|
||||||
android:id="@+id/playback_skip_next"
|
android:id="@+id/playback_skip_next"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:contentDescription="@string/desc_skip_next"
|
android:contentDescription="@string/desc_skip_next"
|
||||||
android:src="@drawable/ic_skip_next"
|
android:src="@drawable/ic_skip_next"
|
||||||
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
|
||||||
|
@ -156,7 +156,7 @@
|
||||||
android:id="@+id/playback_shuffle"
|
android:id="@+id/playback_shuffle"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/spacing_large"
|
android:layout_marginStart="@dimen/spacing_mid_large"
|
||||||
android:contentDescription="@string/desc_shuffle"
|
android:contentDescription="@string/desc_shuffle"
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
app:hasIndicator="true"
|
app:hasIndicator="true"
|
||||||
|
|
Loading…
Reference in a new issue