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:
OxygenCobalt 2022-04-07 20:18:25 -06:00
parent 3a19d822ce
commit 180faa6f50
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
14 changed files with 218 additions and 257 deletions

View file

@ -38,7 +38,7 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
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
@ -122,32 +122,30 @@ class MainFragment : ViewBindingFragment<FragmentMainBinding>() {
// Error, show the error to the user
is MusicStore.Response.Err -> {
logW("Received Error")
val errorRes =
when (response.kind) {
MusicStore.ErrorKind.NO_MUSIC -> R.string.err_no_music
MusicStore.ErrorKind.NO_PERMS -> R.string.err_no_perms
MusicStore.ErrorKind.FAILED -> R.string.err_load_failed
logD("Received Response.Err")
Snackbar.make(binding.root, R.string.err_load_failed, Snackbar.LENGTH_INDEFINITE)
.apply {
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
show()
}
val snackbar =
Snackbar.make(binding.root, getString(errorRes), Snackbar.LENGTH_INDEFINITE)
when (response.kind) {
MusicStore.ErrorKind.FAILED, MusicStore.ErrorKind.NO_MUSIC -> {
snackbar.setAction(R.string.lbl_retry) {
musicModel.reloadMusic(requireContext())
}
}
is MusicStore.Response.NoMusic -> {
logD("Received Response.NoMusic")
Snackbar.make(binding.root, R.string.err_no_music, Snackbar.LENGTH_INDEFINITE)
.apply {
setAction(R.string.lbl_retry) { musicModel.reloadMusic(context) }
show()
}
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)
}
show()
}
}
snackbar.show()
}
null -> {}
}

View file

@ -31,6 +31,7 @@ import org.oxycblt.auxio.ui.Header
import org.oxycblt.auxio.ui.Item
import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* ViewModel that stores data for the [DetailFragment]s. This includes:
@ -40,6 +41,7 @@ import org.oxycblt.auxio.util.logD
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mCurrentAlbum = MutableLiveData<Album?>()
@ -87,9 +89,9 @@ class DetailViewModel : ViewModel() {
fun setAlbumId(id: Long) {
if (mCurrentAlbum.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val library = unlikelyToBeNull(musicStore.library)
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
refreshAlbumData(album)
@ -97,16 +99,18 @@ class DetailViewModel : ViewModel() {
fun setArtistId(id: Long) {
if (mCurrentArtist.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val artist = requireNotNull(musicStore.artists.find { it.id == id }) {}
val library = unlikelyToBeNull(musicStore.library)
val artist =
requireNotNull(library.artists.find { it.id == id }) { "Invalid artist id provided" }
mCurrentArtist.value = artist
refreshArtistData(artist)
}
fun setGenreId(id: Long) {
if (mCurrentGenre.value?.id == id) return
val musicStore = MusicStore.requireInstance()
val genre = requireNotNull(musicStore.genres.find { it.id == id })
val library = unlikelyToBeNull(musicStore.library)
val genre =
requireNotNull(library.genres.find { it.id == id }) { "Invalid genre id provided" }
mCurrentGenre.value = genre
refreshGenreData(genre)
}

View file

@ -20,8 +20,6 @@ package org.oxycblt.auxio.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Album
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.
* @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 mSongs = MutableLiveData(listOf<Song>())
@ -78,15 +77,8 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
val fastScrolling: LiveData<Boolean> = mFastScrolling
init {
musicStore.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. */
@ -142,6 +134,16 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
// --- 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>) {
tabs = visibleTabs
mRecreateTabs.value = true
@ -149,6 +151,7 @@ class HomeViewModel : ViewModel(), SettingsManager.Callback {
override fun onCleared() {
super.onCleared()
musicStore.addCallback(this)
settingsManager.removeCallback(this)
}
}

View file

@ -89,15 +89,24 @@ import org.oxycblt.auxio.util.logD
*
* @author OxygenCobalt
*/
class MusicLoader {
data class Library(
val genres: List<Genre>,
val artists: List<Artist>,
val albums: List<Album>,
val songs: List<Song>
)
object Indexer {
/**
* 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
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)
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
* returned by this function are **not** well-formed. The companion [buildAlbums],
@ -404,15 +406,4 @@ class MusicLoader {
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
}
}

View file

@ -29,9 +29,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
// --- MUSIC MODELS ---
/**
* [Item] variant that represents a music item.
*/
/** [Item] variant that represents a music item. */
sealed class Music : Item() {
/** The raw name of this item. Null if unknown. */
abstract val rawName: String?
@ -116,8 +114,8 @@ data class Song(
get() = internalMediaStoreArtistName ?: album.artist.rawName
/**
* Resolve the artist name for this song in particular. First uses the artist tag, and
* then falls back to the album artist tag (i.e parent artist name)
* Resolve the artist name for this song in particular. First uses the artist tag, and then
* falls back to the album artist tag (i.e parent artist name)
*/
fun resolveIndividualArtistName(context: Context) =
internalMediaStoreArtistName ?: album.artist.resolveName(context)

View file

@ -38,76 +38,95 @@ import org.oxycblt.auxio.util.logE
* @author OxygenCobalt
*/
class MusicStore private constructor() {
private var mGenres = listOf<Genre>()
val genres: List<Genre>
get() = mGenres
private var response: Response? = null
val library: Library?
get() =
response?.let { currentResponse ->
if (currentResponse is Response.Ok) {
currentResponse.library
} else {
null
}
}
private var mArtists = listOf<Artist>()
val artists: List<Artist>
get() = mArtists
private val callbacks = mutableListOf<Callback>()
private var mAlbums = listOf<Album>()
val albums: List<Album>
get() = mAlbums
fun addCallback(callback: Callback) {
callbacks.add(callback)
}
private var mSongs = listOf<Song>()
val songs: List<Song>
get() = mSongs
fun removeCallback(callback: Callback) {
callbacks.remove(callback)
}
/** 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")
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 =
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_DENIED
if (notGranted) {
return Response.Err(ErrorKind.NO_PERMS)
return Response.NoPerms
}
try {
val start = System.currentTimeMillis()
val response =
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()
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)
return response
}
/** Find a song in a faster manner using an ID for its album as well. */
fun findSongFast(songId: Long, albumId: Long): Song? {
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
}
/**
* 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 }
data class Library(
val genres: List<Genre>,
val artists: List<Artist>,
val albums: List<Album>,
val songs: List<Song>
) {
/** Find a song in a faster manner using an ID for its album as well. */
fun findSongFast(songId: Long, albumId: Long): Song? {
return albums.find { it.id == albumId }?.songs?.find { it.id == songId }
}
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
*/
sealed class Response {
class Ok(val musicStore: MusicStore) : Response()
class Err(val kind: ErrorKind) : Response()
class Ok(val library: Library) : Response()
class Err(throwable: Throwable) : Response()
object NoMusic : Response()
object NoPerms : Response()
}
enum class ErrorKind {
NO_PERMS,
NO_MUSIC,
FAILED
interface Callback {
fun onMusicUpdate(response: Response)
}
companion object {
@Volatile private var RESPONSE: Response? = null
@Volatile private var INSTANCE: MusicStore? = null
/**
* Initialize the loading process for this instance. This must be ran on a background
* thread. If the instance has already been loaded successfully, then it will be returned
* immediately.
*/
suspend fun initInstance(context: Context): Response {
val currentInstance = RESPONSE
fun getInstance(): MusicStore {
val currentInstance = INSTANCE
if (currentInstance is Response.Ok) {
if (currentInstance != null) {
return currentInstance
}
val response =
withContext(Dispatchers.IO) {
val response = MusicStore().load(context)
synchronized(this) { RESPONSE = response }
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
synchronized(this) {
val newInstance = MusicStore()
INSTANCE = newInstance
return newInstance
}
/**
* 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
}
}
}

View file

@ -25,12 +25,18 @@ import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
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)
val loaderResponse: LiveData<MusicStore.Response?> = mLoaderResponse
private var isBusy = false
init {
musicStore.addCallback(this)
}
/**
* 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.
@ -45,7 +51,7 @@ class MusicViewModel : ViewModel() {
mLoaderResponse.value = null
viewModelScope.launch {
val result = MusicStore.initInstance(context)
val result = musicStore.index(context)
mLoaderResponse.value = result
isBusy = false
}
@ -56,4 +62,13 @@ class MusicViewModel : ViewModel() {
mLoaderResponse.value = null
loadMusic(context)
}
override fun onMusicUpdate(response: MusicStore.Response) {
mLoaderResponse.value = response
}
override fun onCleared() {
super.onCleared()
musicStore.removeCallback(this)
}
}

View file

@ -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.
*/
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val playbackManager = PlaybackStateManager.getInstance()
// Playback
private val mSong = MutableLiveData<Song?>()
private val mParent = MutableLiveData<MusicParent?>()
@ -94,9 +98,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
val playbackMode: LiveData<PlaybackMode>
get() = mMode
private val playbackManager = PlaybackStateManager.getInstance()
private val settingsManager = SettingsManager.getInstance()
init {
playbackManager.addCallback(this)
@ -166,7 +167,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
*/
fun playWithUri(uri: Uri, context: Context) {
// 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)
} else {
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. */
private fun playWithUriInternal(uri: Uri, context: Context) {
logD("Playing with uri $uri")
val musicStore = MusicStore.maybeGetInstance() ?: return
musicStore.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
val library = musicStore.library ?: return
library.findSongForUri(uri, context.contentResolver)?.let { song -> playSong(song) }
}
/** Shuffle all songs */

View file

@ -105,7 +105,7 @@ class PlaybackStateDatabase(context: Context) :
* @param musicStore Required to transform database songs/parents into actual instances
* @return The stored [SavedState], null if there isn't one.
*/
fun readState(musicStore: MusicStore): SavedState? {
fun readState(library: MusicStore.Library): SavedState? {
requireBackgroundThread()
var state: SavedState? = null
@ -124,16 +124,16 @@ class PlaybackStateDatabase(context: Context) :
cursor.moveToFirst()
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 parent =
cursor.getLongOrNull(parentIndex)?.let { id ->
when (mode) {
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.id == id }
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.id == id }
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.id == id }
PlaybackMode.IN_GENRE -> library.genres.find { it.id == id }
PlaybackMode.IN_ARTIST -> library.artists.find { it.id == id }
PlaybackMode.IN_ALBUM -> library.albums.find { it.id == id }
PlaybackMode.ALL_SONGS -> null
}
}
@ -186,7 +186,7 @@ class PlaybackStateDatabase(context: Context) :
* Read a list of queue items from this database.
* @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()
val queue = mutableListOf<Song>()
@ -198,8 +198,10 @@ class PlaybackStateDatabase(context: Context) :
val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
while (cursor.moveToNext()) {
musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))
?.let { song -> queue.add(song) }
library.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let {
song ->
queue.add(song)
}
}
}

View file

@ -43,6 +43,9 @@ import org.oxycblt.auxio.util.logE
* @author OxygenCobalt
*/
class PlaybackStateManager private constructor() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
// Playback
private var mSong: Song? = null
set(value) {
@ -124,8 +127,6 @@ class PlaybackStateManager private constructor() {
val hasPlayed: Boolean
get() = mHasPlayed
private val settingsManager = SettingsManager.getInstance()
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>()
@ -154,8 +155,7 @@ class PlaybackStateManager private constructor() {
when (mode) {
PlaybackMode.ALL_SONGS -> {
val musicStore = MusicStore.maybeGetInstance() ?: return
val musicStore = musicStore.library ?: return
mParent = null
mQueue = musicStore.songs.toMutableList()
}
@ -211,10 +211,11 @@ class PlaybackStateManager private constructor() {
/** Shuffle all songs. */
fun shuffleAll() {
val musicStore = MusicStore.maybeGetInstance() ?: return
logD("RETARD. ${musicStore.library}")
val library = musicStore.library ?: return
mPlaybackMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList()
mQueue = library.songs.toMutableList()
mParent = null
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
*/
private fun resetShuffle(keepSong: Boolean) {
val musicStore = MusicStore.maybeGetInstance() ?: return
val library = musicStore.library ?: return
val lastSong = mSong
mQueue =
when (mPlaybackMode) {
PlaybackMode.ALL_SONGS ->
settingsManager.libSongSort.songs(musicStore.songs).toMutableList()
settingsManager.libSongSort.songs(library.songs).toMutableList()
PlaybackMode.IN_ALBUM ->
settingsManager.detailAlbumSort.album(mParent as Album).toMutableList()
PlaybackMode.IN_ARTIST ->
@ -496,7 +497,7 @@ class PlaybackStateManager private constructor() {
suspend fun restoreFromDatabase(context: Context) {
logD("Getting state from DB")
val musicStore = MusicStore.maybeGetInstance() ?: return
val library = musicStore.library ?: return
val start: Long
val playbackState: PlaybackStateDatabase.SavedState?
val queue: MutableList<Song>
@ -504,8 +505,8 @@ class PlaybackStateManager private constructor() {
withContext(Dispatchers.IO) {
start = System.currentTimeMillis()
val database = PlaybackStateDatabase.getInstance(context)
playbackState = database.readState(musicStore)
queue = database.readQueue(musicStore)
playbackState = database.readState(library)
queue = database.readQueue(library)
}
// Get off the IO coroutine since it will cause LiveData updates to throw an exception

View file

@ -76,6 +76,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
*
* TODO: Move all external exposal from passing around PlaybackStateManager to passing around the
* MediaMetadata instance. Generally makes it easier to encapsulate this class.
*
* TODO: Move restore and file opening to service
*/
class PlaybackService :
Service(), Player.Listener, PlaybackStateManager.Callback, SettingsManager.Callback {

View file

@ -115,11 +115,6 @@ class SearchFragment : ViewBindingFragment<FragmentSearchBinding>(), MenuItemLis
musicModel.loaderResponse.observe(viewLifecycleOwner, ::handleLoaderResponse)
}
override fun onResume() {
super.onResume()
searchModel.setNavigating(false)
}
override fun onDestroyBinding(binding: FragmentSearchBinding) {
super.onDestroyBinding(binding)
binding.searchRecycler.adapter = null

View file

@ -40,8 +40,10 @@ import org.oxycblt.auxio.util.logD
* @author OxygenCobalt
*/
class SearchViewModel : ViewModel() {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
private val mSearchResults = MutableLiveData(listOf<Item>())
private var mIsNavigating = false
private var mFilterMode: DisplayMode? = null
private var mLastQuery: String? = null
@ -51,8 +53,6 @@ class SearchViewModel : ViewModel() {
val filterMode: DisplayMode?
get() = mFilterMode
private val settingsManager = SettingsManager.getInstance()
init {
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].
*/
fun search(context: Context, query: String?) {
val musicStore = MusicStore.maybeGetInstance()
mLastQuery = query
if (query.isNullOrEmpty() || musicStore == null) {
val library = musicStore.library
if (query.isNullOrEmpty() || library == null) {
logD("No music/query, ignoring search")
mSearchResults.value = listOf()
return
@ -80,28 +80,28 @@ class SearchViewModel : ViewModel() {
// Note: a filter mode of null means to not filter at all.
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.addAll(sort.artists(artists))
}
}
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.addAll(sort.albums(albums))
}
}
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.addAll(sort.genres(genres))
}
}
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.addAll(sort.songs(songs))
}
@ -181,9 +181,4 @@ class SearchViewModel : ViewModel() {
return sb.toString()
}
/** Update the current navigation status to [isNavigating] */
fun setNavigating(isNavigating: Boolean) {
mIsNavigating = isNavigating
}
}

View file

@ -18,7 +18,7 @@
<org.oxycblt.auxio.coil.StyledImageView
android:id="@+id/playback_cover"
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_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -30,8 +30,8 @@
style="@style/Widget.Auxio.TextView.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
app:layout_constraintBottom_toTopOf="@+id/playback_artist"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -43,8 +43,8 @@
style="@style/Widget.Auxio.TextView.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
app:layout_constraintBottom_toTopOf="@+id/playback_album"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -55,8 +55,8 @@
style="@style/Widget.Auxio.TextView.Secondary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
app:layout_constraintBottom_toTopOf="@+id/playback_seek_bar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -108,7 +108,7 @@
android:id="@+id/playback_loop"
android:layout_width="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:src="@drawable/ic_loop"
app:hasIndicator="true"
@ -121,7 +121,7 @@
android:id="@+id/playback_skip_prev"
android:layout_width="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:src="@drawable/ic_skip_prev"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
@ -133,7 +133,7 @@
style="@style/Widget.Auxio.FloatingActionButton.PlayPause"
android:layout_width="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:src="@drawable/sel_playing_state"
app:layout_constraintBottom_toBottomOf="parent"
@ -145,7 +145,7 @@
android:id="@+id/playback_skip_next"
android:layout_width="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:src="@drawable/ic_skip_next"
app:layout_constraintBottom_toBottomOf="@+id/playback_play_pause"
@ -156,7 +156,7 @@
android:id="@+id/playback_shuffle"
android:layout_width="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:src="@drawable/ic_shuffle"
app:hasIndicator="true"