playback: mostly hide playback mode details

Mostly hide the code that handles starting playback based on a given
mode into their respective ViewModels.

Again, makes testing easier.
This commit is contained in:
Alexander Capehart 2023-01-06 19:58:52 -07:00
parent ac9f50c0a0
commit 6fa53ab873
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 96 additions and 69 deletions

View file

@ -11,6 +11,7 @@
- Fixed crash that would occur in music folders dialog when user does not have a working
file manager
- Fixed notification not updating due to settings changes
- Fixed genre picker from repeatedly showing up when device rotates
#### What's Changed
- Implemented new queue system

View file

@ -309,7 +309,7 @@ class MainFragment :
navModel.mainNavigateTo(
MainNavigationAction.Directions(
MainFragmentDirections.actionPickPlaybackGenre(song.uid)))
playbackModel.finishPlaybackArtistPicker()
playbackModel.finishPlaybackGenrePicker()
}
}

View file

@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.*
* @author Alexander Capehart (OxygenCobalt)
*/
class AlbumDetailFragment :
ListFragment<Music, FragmentDetailBinding>(), AlbumDetailAdapter.Listener {
ListFragment<Song, FragmentDetailBinding>(), AlbumDetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels()
// Information about what album to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an album.
@ -121,21 +121,12 @@ class AlbumDetailFragment :
}
}
override fun onRealClick(item: Music) {
val song = requireIs<Song>(item)
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
// "Play from shown item" and "Play from album" functionally have the same
// behavior since a song can only have one album.
null,
MusicMode.ALBUMS -> playbackModel.playFromAlbum(song)
MusicMode.SONGS -> playbackModel.playFromAll(song)
MusicMode.ARTISTS -> playbackModel.playFromArtist(song)
MusicMode.GENRES -> playbackModel.playFromGenre(song)
}
override fun onRealClick(item: Song) {
// There can only be one album, so a null mode and an ALBUMS mode will function the same.
playbackModel.playFrom(item, detailModel.playbackMode ?: MusicMode.ALBUMS)
}
override fun onOpenMenu(item: Music, anchor: View) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
override fun onOpenMenu(item: Song, anchor: View) {
openMusicMenu(anchor, R.menu.menu_album_song_actions, item)
}

View file

@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ListFragment] that shows information about an [Artist].
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener {
class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
private val detailModel: DetailViewModel by activityViewModels()
// Information about what artist to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an artist.
@ -122,20 +122,18 @@ class ArtistDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detai
override fun onRealClick(item: Music) {
when (item) {
is Album -> navModel.exploreNavigateTo(item)
is Song -> {
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
val playbackMode = detailModel.playbackMode
if (playbackMode != null) {
playbackModel.playFrom(item, playbackMode)
} else {
// When configured to play from the selected item, we already have an Artist
// to play from.
null ->
playbackModel.playFromArtist(
item, unlikelyToBeNull(detailModel.currentArtist.value))
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> playbackModel.playFromGenre(item)
playbackModel.playFromArtist(item,
unlikelyToBeNull(detailModel.currentArtist.value))
}
}
is Album -> navModel.exploreNavigateTo(item)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}

View file

@ -37,6 +37,7 @@ import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.*
/**
@ -50,6 +51,7 @@ class DetailViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private var currentSongJob: Job? = null
@ -125,6 +127,12 @@ class DetailViewModel(application: Application) :
currentGenre.value?.let(::refreshGenreList)
}
/**
* The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently
* shown item.
*/
val playbackMode: MusicMode? get() = playbackSettings.inParentPlaybackMode
init {
musicStore.addListener(this)
}

View file

@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A [ListFragment] that shows information for a particular [Genre].
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener {
class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), DetailAdapter.Listener<Music> {
private val detailModel: DetailViewModel by activityViewModels()
// Information about what genre to display is initially within the navigation arguments
// as a UID, as that is the only safe way to parcel an genre.
@ -122,18 +122,17 @@ class GenreDetailFragment : ListFragment<Music, FragmentDetailBinding>(), Detail
override fun onRealClick(item: Music) {
when (item) {
is Artist -> navModel.exploreNavigateTo(item)
is Song ->
when (PlaybackSettings.from(requireContext()).inParentPlaybackMode) {
// When configured to play from the selected item, we already have a Genre
is Song -> {
val playbackMode = detailModel.playbackMode
if (playbackMode != null) {
playbackModel.playFrom(item, playbackMode)
} else {
// When configured to play from the selected item, we already have an Artist
// to play from.
null ->
playbackModel.playFromGenre(
item, unlikelyToBeNull(detailModel.currentGenre.value))
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> playbackModel.playFromGenre(item)
playbackModel.playFromArtist(item,
unlikelyToBeNull(detailModel.currentArtist.value))
}
}
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}

View file

@ -48,7 +48,7 @@ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listene
* An extension to [DetailAdapter.Listener] that enables interactions specific to the album
* detail view.
*/
interface Listener : DetailAdapter.Listener {
interface Listener : DetailAdapter.Listener<Song> {
/**
* Called when the artist name in the [Album] header was clicked, requesting navigation to
* it's parent artist.

View file

@ -32,6 +32,7 @@ import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
@ -42,7 +43,7 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
class ArtistDetailAdapter(private val listener: Listener<Music>) : DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
// Support an artist header, and special artist albums/songs.
@ -109,7 +110,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
* @param artist The new [Artist] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
fun bind(artist: Artist, listener: DetailAdapter.Listener) {
fun bind(artist: Artist, listener: DetailAdapter.Listener<*>) {
binding.detailCover.bind(artist)
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
binding.detailName.text = artist.resolveName(binding.context)

View file

@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.inflater
* @author Alexander Capehart (OxygenCobalt)
*/
abstract class DetailAdapter(
private val listener: Listener,
private val listener: Listener<*>,
itemCallback: DiffUtil.ItemCallback<Item>
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
// Safe to leak this since the listener will not fire during initialization
@ -89,7 +89,7 @@ abstract class DetailAdapter(
}
/** An extended [SelectableListListener] for [DetailAdapter] implementations. */
interface Listener : SelectableListListener<Music> {
interface Listener<in T : Music> : SelectableListListener<T> {
// TODO: Split off into sub-listeners if a collapsing toolbar is implemented.
/**
* Called when the play button in a detail header is pressed, requesting that the current
@ -139,7 +139,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
* @param sortHeader The new [SortHeader] to bind.
* @param listener An [DetailAdapter.Listener] to bind interactions to.
*/
fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener) {
fun bind(sortHeader: SortHeader, listener: DetailAdapter.Listener<*>) {
binding.headerTitle.text = binding.context.getString(sortHeader.titleRes)
binding.headerButton.apply {
// Add a Tooltip based on the content description so that the purpose of this

View file

@ -30,6 +30,7 @@ import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
@ -40,7 +41,7 @@ import org.oxycblt.auxio.util.inflater
* @param listener A [DetailAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
*/
class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFF_CALLBACK) {
class GenreDetailAdapter(private val listener: Listener<Music>) : DetailAdapter(listener, DIFF_CALLBACK) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
// Support the Genre header and generic Artist/Song items. There's nothing about
@ -105,7 +106,7 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
* @param genre The new [Song] to bind.
* @param listener A [DetailAdapter.Listener] to bind interactions to.
*/
fun bind(genre: Genre, listener: DetailAdapter.Listener) {
fun bind(genre: Genre, listener: DetailAdapter.Listener<*>) {
binding.detailCover.bind(genre)
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = genre.resolveName(binding.context)

View file

@ -26,6 +26,7 @@ import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.logD
/**
@ -37,6 +38,7 @@ class HomeViewModel(application: Application) :
private val musicStore = MusicStore.getInstance()
private val homeSettings = HomeSettings.from(application)
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val _songsList = MutableStateFlow(listOf<Song>())
/** A list of [Song]s, sorted by the preferred [Sort], to be shown in the home view. */
@ -62,11 +64,14 @@ class HomeViewModel(application: Application) :
val genresList: StateFlow<List<Genre>>
get() = _genresList
/** The [MusicMode] to use when playing a [Song] from the UI. */
val playbackMode: MusicMode get() = playbackSettings.inListPlaybackMode
/**
* A list of [MusicMode] corresponding to the current [Tab] configuration, excluding invisible
* [Tab]s.
*/
var currentTabModes: List<MusicMode> = makeTabModes()
var currentTabModes = makeTabModes()
private set
private val _currentTabMode = MutableStateFlow(currentTabModes[0])

View file

@ -130,12 +130,7 @@ class SongListFragment :
}
override fun onRealClick(item: Song) {
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> playbackModel.playFromGenre(item)
}
playbackModel.playFrom(item, homeModel.playbackMode)
}
override fun onOpenMenu(item: Song, anchor: View) {

View file

@ -65,7 +65,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
// Set up actions
binding.playbackPlayPause.setOnClickListener { playbackModel.toggleIsPlaying() }
setupSecondaryActions(binding, PlaybackSettings.from(context).barAction)
setupSecondaryActions(binding, playbackModel.currentBarAction)
// Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources.

View file

@ -36,7 +36,6 @@ import org.oxycblt.auxio.util.context
*/
class PlaybackViewModel(application: Application) :
AndroidViewModel(application), PlaybackStateManager.Listener {
private val homeSettings = HomeSettings.from(application)
private val musicSettings = MusicSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private val playbackManager = PlaybackStateManager.getInstance()
@ -84,6 +83,9 @@ class PlaybackViewModel(application: Application) :
val genrePickerSong: StateFlow<Song?>
get() = _genrePlaybackPickerSong
/** The current action to show on the playback bar. */
val currentBarAction: ActionMode get() = playbackSettings.barAction
/**
* The current audio session ID of the internal player. Null if no [InternalPlayer] is
* available.
@ -143,6 +145,29 @@ class PlaybackViewModel(application: Application) :
// --- PLAYING FUNCTIONS ---
/** Shuffle all songs in the music library. */
fun shuffleAll() {
playImpl(null, null, true)
}
/**
* Play a [Song] from the [MusicParent] outlined by the given [MusicMode].
* - If [MusicMode.SONGS], the [Song] is played from all songs.
* - If [MusicMode.ALBUMS], the [Song] is played from it's [Album].
* - If [MusicMode.ARTISTS], the [Song] is played from one of it's [Artist]s.
* - If [MusicMode.GENRES], the [Song] is played from one of it's [Genre]s.
* @param song The [Song] to play.
* @param playbackMode The [MusicMode] to play from.
*/
fun playFrom(song: Song, playbackMode: MusicMode) {
when (playbackMode) {
MusicMode.SONGS -> playFromAll(song)
MusicMode.ALBUMS -> playFromAlbum(song)
MusicMode.ARTISTS -> playFromArtist(song)
MusicMode.GENRES -> playFromGenre(song)
}
}
/**
* Play the given [Song] from all songs in the music library.
* @param song The [Song] to play.
@ -151,11 +176,6 @@ class PlaybackViewModel(application: Application) :
playImpl(song, null)
}
/** Shuffle all songs in the music library. */
fun shuffleAll() {
playImpl(null, null, true)
}
/**
* Play a [Song] from it's [Album].
* @param song The [Song] to play.
@ -203,6 +223,15 @@ class PlaybackViewModel(application: Application) :
}
}
/**
* Mark the [Genre] playback choice process as complete. This should occur when the [Genre]
* choice dialog is opened after this flag is detected.
* @see playFromGenre
*/
fun finishPlaybackGenrePicker() {
_genrePlaybackPickerSong.value = null
}
/**
* Play an [Album].
* @param album The [Album] to play.

View file

@ -136,14 +136,8 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
override fun onRealClick(item: Music) {
when (item) {
is Song ->
when (PlaybackSettings.from(requireContext()).inListPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.GENRES -> playbackModel.playFromGenre(item)
}
is MusicParent -> navModel.exploreNavigateTo(item)
is Song -> playbackModel.playFrom(item, searchModel.playbackMode)
}
}

View file

@ -34,6 +34,7 @@ import org.oxycblt.auxio.music.*
import org.oxycblt.auxio.music.Library
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
@ -44,7 +45,8 @@ import org.oxycblt.auxio.util.logD
class SearchViewModel(application: Application) :
AndroidViewModel(application), MusicStore.Listener {
private val musicStore = MusicStore.getInstance()
private val settings = SearchSettings.from(application)
private val searchSettings = SearchSettings.from(application)
private val playbackSettings = PlaybackSettings.from(application)
private var lastQuery: String? = null
private var currentSearchJob: Job? = null
@ -53,6 +55,9 @@ class SearchViewModel(application: Application) :
val searchResults: StateFlow<List<Item>>
get() = _searchResults
/** The [MusicMode] to use when playing a [Song] from the UI. */
val playbackMode: MusicMode get() = playbackSettings.inListPlaybackMode
init {
musicStore.addListener(this)
}
@ -97,7 +102,7 @@ class SearchViewModel(application: Application) :
private fun searchImpl(library: Library, query: String): List<Item> {
val sort = Sort(Sort.Mode.ByName, true)
val filterMode = settings.searchFilterMode
val filterMode = searchSettings.searchFilterMode
val results = mutableListOf<Item>()
// Note: A null filter mode maps to the "All" filter option, hence the check.
@ -182,7 +187,7 @@ class SearchViewModel(application: Application) :
*/
@IdRes
fun getFilterOptionId() =
when (settings.searchFilterMode) {
when (searchSettings.searchFilterMode) {
MusicMode.SONGS -> R.id.option_filter_songs
MusicMode.ALBUMS -> R.id.option_filter_albums
MusicMode.ARTISTS -> R.id.option_filter_artists
@ -207,7 +212,7 @@ class SearchViewModel(application: Application) :
else -> error("Invalid option ID provided")
}
logD("Updating filter mode to $newFilterMode")
settings.searchFilterMode = newFilterMode
searchSettings.searchFilterMode = newFilterMode
search(lastQuery)
}