playback: add framework for multi-parent playback

Add some functions to eventually enable multi-parent playback.

PlaybackMode is still used in some places, however will steadily be
phased out hopefully.
This commit is contained in:
Alexander Capehart 2022-09-06 13:12:25 -06:00
parent e5d7cdc340
commit 457013d047
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
11 changed files with 86 additions and 45 deletions

View file

@ -3,7 +3,12 @@
## dev ## dev
#### What's Fixed #### What's Fixed
- Fixed issue wher the scroll popup would not display correctly in landscape mode [#230] - Fixed issue where the scroll popup would not display correctly in landscape mode [#230]
- Fixed issue where the playback progress would continue in the notification even if
audio focus was lost
#### Dev/Meta
- Completed migration to reactive playback system
## 2.6.3 ## 2.6.3

View file

@ -37,7 +37,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
@ -122,7 +121,12 @@ class AlbumDetailFragment :
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
if (item is Song) { if (item is Song) {
playbackModel.play(item, settings.detailPlaybackMode ?: PlaybackMode.IN_ALBUM) val playbackMode = settings.detailPlaybackMode
if (playbackMode != null) {
playbackModel.play(item, playbackMode)
} else {
playbackModel.playFromAlbum(item)
}
} }
} }

View file

@ -35,7 +35,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
@ -112,9 +111,16 @@ class ArtistDetailFragment :
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { when (item) {
is Song -> is Song -> {
playbackModel.play(item, settings.detailPlaybackMode ?: PlaybackMode.IN_ARTIST) val playbackMode = settings.detailPlaybackMode
if (playbackMode != null) {
playbackModel.play(item, playbackMode)
} else {
playbackModel.playFromArtist(item)
}
}
is Album -> navModel.exploreNavigateTo(item) is Album -> navModel.exploreNavigateTo(item)
} }
} }

View file

@ -36,7 +36,6 @@ import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.Sort import org.oxycblt.auxio.ui.Sort
import org.oxycblt.auxio.ui.fragment.MenuFragment import org.oxycblt.auxio.ui.fragment.MenuFragment
@ -113,12 +112,13 @@ class GenreDetailFragment :
} }
override fun onItemClick(item: Item) { override fun onItemClick(item: Item) {
when (item) { check(item is Song)
is Song ->
playbackModel.play(item, settings.detailPlaybackMode ?: PlaybackMode.IN_GENRE) val playbackMode = settings.detailPlaybackMode
is Album -> if (playbackMode != null) {
findNavController() playbackModel.play(item, playbackMode)
.navigate(ArtistDetailFragmentDirections.actionShowAlbum(item.id)) } else {
playbackModel.playFromGenre(item, unlikelyToBeNull(detailModel.currentGenre.value))
} }
} }

View file

@ -90,7 +90,37 @@ class PlaybackViewModel(application: Application) :
/** Play a [song] with the [mode] specified, */ /** Play a [song] with the [mode] specified, */
fun play(song: Song, mode: PlaybackMode) { fun play(song: Song, mode: PlaybackMode) {
playbackManager.play(song, mode, settings) // TODO: Remove this function when selection is implemented
val parent =
when (mode) {
PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_GENRE -> song.genres.maxBy { it.songs.size }
PlaybackMode.ALL_SONGS -> null
}
playbackManager.play(song, parent, settings)
}
/** Play a song from it's album. */
fun playFromAlbum(song: Song) {
playbackManager.play(song, song.album, settings)
}
/** Play a song from it's artist. */
fun playFromArtist(song: Song) {
playbackManager.play(song, song.album.artist, settings)
}
/** Play a song from the specific genre that contains the song. */
fun playFromGenre(song: Song, genre: Genre) {
if (!genre.songs.contains(song)) {
logE("Genre does not contain song, not playing")
return
}
playbackManager.play(song, genre, settings)
} }
/** /**

View file

@ -78,6 +78,8 @@ interface InternalPlayer {
/** Load this state into the analogous [PlaybackStateCompat.Builder]. */ /** Load this state into the analogous [PlaybackStateCompat.Builder]. */
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder = fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
builder.setState( builder.setState(
// State represents the user's preference, not the actual player state.
// Doing this produces a better experience in the media control UI.
if (isPlaying) { if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING PlaybackStateCompat.STATE_PLAYING
} else { } else {
@ -92,6 +94,9 @@ interface InternalPlayer {
}, },
creationTime) creationTime)
// Equality ignores the creation time to prevent functionally
// identical states from being equal.
override fun equals(other: Any?) = override fun equals(other: Any?) =
other is State && other is State &&
isPlaying == other.isPlaying && isPlaying == other.isPlaying &&
@ -109,9 +114,9 @@ interface InternalPlayer {
/** Create a new instance of this state. */ /** Create a new instance of this state. */
fun new(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) = fun new(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
State( State(
isPlaying,
// Minor sanity check: Make sure that advancing can't occur if the // Minor sanity check: Make sure that advancing can't occur if the
// main playing value is paused. // main playing value is paused.
isPlaying,
isPlaying && isAdvancing, isPlaying && isAdvancing,
positionMs, positionMs,
SystemClock.elapsedRealtime()) SystemClock.elapsedRealtime())

View file

@ -51,14 +51,13 @@ enum class PlaybackMode {
* Get a [PlaybackMode] for an int [constant] * Get a [PlaybackMode] for an int [constant]
* @return The mode, null if there isn't one for this. * @return The mode, null if there isn't one for this.
*/ */
fun fromInt(constant: Int): PlaybackMode? { fun fromInt(constant: Int) =
return when (constant) { when (constant) {
IntegerTable.PLAYBACK_MODE_ALL_SONGS -> ALL_SONGS IntegerTable.PLAYBACK_MODE_ALL_SONGS -> ALL_SONGS
IntegerTable.PLAYBACK_MODE_IN_ALBUM -> IN_ALBUM IntegerTable.PLAYBACK_MODE_IN_ALBUM -> IN_ALBUM
IntegerTable.PLAYBACK_MODE_IN_ARTIST -> IN_ARTIST IntegerTable.PLAYBACK_MODE_IN_ARTIST -> IN_ARTIST
IntegerTable.PLAYBACK_MODE_IN_GENRE -> IN_GENRE IntegerTable.PLAYBACK_MODE_IN_GENRE -> IN_GENRE
else -> null else -> null
} }
}
} }
} }

View file

@ -144,22 +144,13 @@ class PlaybackStateManager private constructor() {
// --- PLAYING FUNCTIONS --- // --- PLAYING FUNCTIONS ---
/** Play a [song]. */ /** Play a song from a parent that contains the song. */
@Synchronized @Synchronized
fun play(song: Song, playbackMode: PlaybackMode, settings: Settings) { fun play(song: Song, parent: MusicParent?, settings: Settings) {
val internalPlayer = internalPlayer ?: return val internalPlayer = internalPlayer ?: return
val library = musicStore.library ?: return val library = musicStore.library ?: return
parent = this.parent = parent
when (playbackMode) {
PlaybackMode.ALL_SONGS -> null
PlaybackMode.IN_ALBUM -> song.album
PlaybackMode.IN_ARTIST -> song.album.artist
PlaybackMode.IN_GENRE ->
song.genres.maxBy {
it.songs.size
} // TODO: Stopgap measure until I can rework this and add selection
}
applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song) applyNewQueue(library, settings, settings.keepShuffle && isShuffled, song)

View file

@ -54,9 +54,6 @@ import org.oxycblt.auxio.util.logD
* while also keeping in mind the absurd rate limiting system in place just to have a sort-of * while also keeping in mind the absurd rate limiting system in place just to have a sort-of
* coherent state. And even then it will break if you skip too much. * coherent state. And even then it will break if you skip too much.
* *
* Google, please replace this API. No, don't paper it over with even more broken abstractions.
* Replace it. Please.
*
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MediaSessionComponent(private val context: Context, private val callback: Callback) : class MediaSessionComponent(private val context: Context, private val callback: Callback) :

View file

@ -51,27 +51,31 @@ private val Any.autoTag: String
/** /**
* I know that this will not stop you, but consider what you are doing with your life, plagiarizers. * I know that this will not stop you, but consider what you are doing with your life, plagiarizers.
*
* Do you want to live a fulfilling existence on this planet? Or do you want to spend your life * Do you want to live a fulfilling existence on this planet? Or do you want to spend your life
* taking work others did and making it objectively worse so you could arbitrage a fraction of a * taking work others did and making it objectively worse so you could arbitrage a fraction of a
* penny on every AdMob impression you get? You could do so many great things if you simply had the * penny on every AdMob impression you get?
* courage to come up with an idea of your own. If you still want to go on, I guess the only thing I
* can say is this:
* *
* JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE 六四事件 * You could do so many great things if you simply had the courage to come up with an idea of your
* own.
* *
* 2022 RUSSIAN INVASION OF UKRAINE Вторжение России на Украину * If you still want to go on, I guess the only thing I can say is this:
* *
* WOMEN'S RIGHTS IN THE ISLAMIC REPUBLIC OF IRAN حقوق زنان در ایران * JUNE 1989 TIANAMEN SQUARE PROTESTS AND MASSACRE / 六四事件
* *
* UYGHUR GENOCIDE/XINJIANG INTERNMENT CAMPS 新疆种族灭绝指控/新疆再教育營 * 2022 RUSSIAN INVASION OF UKRAINE / ВТОРЖЕНИЕ РОССИИ НА УКРАИНУ
*
* WOMEN'S RIGHTS IN THE ISLAMIC REPUBLIC OF IRAN / حقوق زنان در ایران
*
* UYGHUR GENOCIDE/XINJIANG INTERNMENT CAMPS / 新疆种族灭绝指控/新疆再教育營
* *
* KASHMIR INDEPENDENCE MOVEMENT * KASHMIR INDEPENDENCE MOVEMENT
* *
* FREE TIBET 西藏自由 * FREE TIBET / 西藏自由
* *
* 1915-1916 ARMENIAN GENOCIDE Ermeni Kırımı * 1915-1916 ARMENIAN GENOCIDE / ERMENI KIRIMI
* *
* 2018 TORTURE AND ASSASSINATION OF JAMAL KHASHOGGI مقتل جمال خاشقجي * 2018 TORTURE AND ASSASSINATION OF JAMAL KHASHOGGI / مقتل جمال خاشقجي
* *
* UNITED ARAB EMIRATES ENSLAVED MIGRANT WORKERS * UNITED ARAB EMIRATES ENSLAVED MIGRANT WORKERS
*/ */

View file

@ -53,10 +53,10 @@ fun Long.msToDs() = floorDiv(100)
/** Converts a long in milliseconds to a long in seconds */ /** Converts a long in milliseconds to a long in seconds */
fun Long.msToSecs() = floorDiv(1000) fun Long.msToSecs() = floorDiv(1000)
/** Converts a long in deciseconds to a long in milliseconds. */ /** Converts a long in deci-seconds to a long in milliseconds. */
fun Long.dsToMs() = times(100) fun Long.dsToMs() = times(100)
/** Converts a long in deciseconds to a long in seconds. */ /** Converts a long in deci-seconds to a long in seconds. */
fun Long.dsToSecs() = floorDiv(10) fun Long.dsToSecs() = floorDiv(10)
/** Converts a long in seconds to a long in milliseconds. */ /** Converts a long in seconds to a long in milliseconds. */