playback: add controller role

Add a controller role to PlaybackStateManager.

The controller manages the internal player, and acts as an intermediary
between other internal objects that want to modify the player, but
don't actually have access to it.

This makes a bunch of future changes far easier.
This commit is contained in:
OxygenCobalt 2022-06-19 16:23:40 -06:00
parent 29fe849565
commit b8cc153f07
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
2 changed files with 99 additions and 39 deletions

View file

@ -21,6 +21,7 @@ import android.content.Context
import kotlin.math.max
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -29,6 +30,7 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
/**
* Master class (and possible god object) for the playback state.
@ -39,10 +41,13 @@ import org.oxycblt.auxio.util.logD
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use
* [org.oxycblt.auxio.playback.system.PlaybackService].
*
* Internal consumers should usually use [Callback], however the component that manages the player
* itself should instead operate as a [Controller].
*
* All access should be done with [PlaybackStateManager.getInstance].
* @author OxygenCobalt
*
* TODO: Add a controller role and move song loading/seeking to that
* TODO: Leverage synchronized here to prevent state issues.
*
* TODO: Bug test app behavior when playback stops
*/
@ -89,16 +94,16 @@ class PlaybackStateManager private constructor() {
// --- CALLBACKS ---
private val callbacks = mutableListOf<Callback>()
private var controller: Controller? = null
/** Add a callback to this instance. Make sure to remove it when done. */
fun addCallback(callback: Callback) {
if (isInitialized) {
callback.onNewPlayback(index, queue, parent)
callback.onSeek(positionMs)
callback.onPositionChanged(positionMs)
callback.onPlayingChanged(isPlaying)
callback.onRepeatChanged(repeatMode)
callback.onShuffledChanged(isShuffled)
callback.onPlayingChanged(isPlaying)
}
callbacks.add(callback)
@ -109,6 +114,34 @@ class PlaybackStateManager private constructor() {
callbacks.remove(callback)
}
/** Register a [PlaybackStateManager.Controller] with this instance. */
fun registerController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller != null) {
logW("Controller is already registered")
return
}
if (isInitialized) {
controller.loadSong(song)
controller.seekTo(positionMs)
controller.onPlayingChanged(isPlaying)
controller.onRepeatChanged(repeatMode)
controller.onPlayingChanged(isPlaying)
}
this.controller = controller
}
/** Unregister a [PlaybackStateManager.Controller] with this instance. */
fun unregisterController(controller: Controller) {
if (BuildConfig.DEBUG && this.controller !== controller) {
logW("Given controller did not match current controller")
return
}
this.controller = null
}
// --- PLAYING FUNCTIONS ---
/**
@ -177,7 +210,7 @@ class PlaybackStateManager private constructor() {
/** Go to the previous song, doing any checks that are needed. */
fun prev() {
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (settingsManager.rewindWithPrev && positionMs >= REWIND_THRESHOLD) {
if (controller?.shouldPrevRewind() == true) {
rewind()
isPlaying = true
} else {
@ -279,7 +312,12 @@ class PlaybackStateManager private constructor() {
* @param positionMs The new position in millis.
* @see seekTo
*/
fun synchronizePosition(positionMs: Long) {
fun synchronizePosition(controller: Controller, positionMs: Long) {
if (BuildConfig.DEBUG && this.controller !== controller) {
logW("Given controller did not match current controller")
return
}
// Don't accept any bugged positions that are over the duration of the song.
val maxDuration = song?.durationMs ?: -1
if (positionMs <= maxDuration) {
@ -289,13 +327,12 @@ class PlaybackStateManager private constructor() {
}
/**
* **Seek** to a [positionMs], this calls [PlaybackStateManager.Callback.onSeek] to notify
* elements that rely on that.
* **Seek** to a [positionMs].
* @param positionMs The position to seek to in millis.
*/
fun seekTo(positionMs: Long) {
this.positionMs = positionMs
notifySeekEvent()
controller?.seekTo(positionMs)
notifyPositionChanged()
}
@ -378,6 +415,7 @@ class PlaybackStateManager private constructor() {
// --- CALLBACKS ---
private fun notifyIndexMoved() {
controller?.loadSong(song)
for (callback in callbacks) {
callback.onIndexMoved(index)
}
@ -390,12 +428,14 @@ class PlaybackStateManager private constructor() {
}
private fun notifyNewPlayback() {
controller?.loadSong(song)
for (callback in callbacks) {
callback.onNewPlayback(index, queue, parent)
}
}
private fun notifyPlayingChanged() {
controller?.onPlayingChanged(isPlaying)
for (callback in callbacks) {
callback.onPlayingChanged(isPlaying)
}
@ -408,21 +448,38 @@ class PlaybackStateManager private constructor() {
}
private fun notifyRepeatModeChanged() {
controller?.onRepeatChanged(repeatMode)
for (callback in callbacks) {
callback.onRepeatChanged(repeatMode)
}
}
private fun notifyShuffledChanged() {
controller?.onShuffledChanged(isShuffled)
for (callback in callbacks) {
callback.onShuffledChanged(isShuffled)
}
}
private fun notifySeekEvent() {
for (callback in callbacks) {
callback.onSeek(positionMs)
}
/** Represents a class capable of managing the internal player. */
interface Controller {
/** Called when a new song should be loaded into the player. */
fun loadSong(song: Song?)
/** Seek to [positionMs] in the player. */
fun seekTo(positionMs: Long)
/** Called when the class wants to determine whether it should rewind or skip back. */
fun shouldPrevRewind(): Boolean
/** Called when the playing state is changed. */
fun onPlayingChanged(isPlaying: Boolean)
/** Called when the repeat mode is changed. */
fun onRepeatChanged(repeatMode: RepeatMode)
/** Called when the shuffled state is changed. */
fun onShuffledChanged(isShuffled: Boolean)
}
/**
@ -430,21 +487,29 @@ class PlaybackStateManager private constructor() {
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
*/
interface Callback {
/** Called when the index is moved, but the queue does not change. This changes the song. */
fun onIndexMoved(index: Int) {}
/** Called when the queue and/or index changed, but the song has not. */
fun onQueueChanged(index: Int, queue: List<Song>) {}
/** Called when playback is changed completely, with a new index, queue, and parent. */
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
/** Called when the playing state is changed. */
fun onPlayingChanged(isPlaying: Boolean) {}
fun onPositionChanged(positionMs: Long) {}
fun onRepeatChanged(repeatMode: RepeatMode) {}
fun onShuffledChanged(isShuffled: Boolean) {}
fun onSeek(positionMs: Long) {}
/** Called when the position is re-synchronized by the controller. */
fun onPositionChanged(positionMs: Long) {}
/** Called when the repeat mode is changed. */
fun onRepeatChanged(repeatMode: RepeatMode) {}
/** Called when the shuffled state is changed. */
fun onShuffledChanged(isShuffled: Boolean) {}
}
companion object {
private const val REWIND_THRESHOLD = 3000L
@Volatile private var INSTANCE: PlaybackStateManager? = null
/** Get/Instantiate the single instance of [PlaybackStateManager]. */

View file

@ -46,7 +46,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.PlaybackStateManager
@ -74,7 +73,7 @@ class PlaybackService :
Service(),
Player.Listener,
NotificationComponent.Callback,
PlaybackStateManager.Callback,
PlaybackStateManager.Controller,
SettingsManager.Callback {
// Player components
private lateinit var player: ExoPlayer
@ -111,7 +110,7 @@ class PlaybackService :
positionScope.launch {
while (true) {
playbackManager.synchronizePosition(player.currentPosition)
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
delay(POS_POLL_INTERVAL)
}
}
@ -139,7 +138,7 @@ class PlaybackService :
// --- PLAYBACKSTATEMANAGER SETUP ---
playbackManager.addCallback(this)
playbackManager.registerController(this)
settingsManager.addCallback(this)
logD("Service created")
@ -166,7 +165,7 @@ class PlaybackService :
// Pause just in case this destruction was unexpected.
playbackManager.isPlaying = false
playbackManager.removeCallback(this)
playbackManager.unregisterController(this)
settingsManager.removeCallback(this)
unregisterReceiver(systemReceiver)
serviceJob.cancel()
@ -218,7 +217,7 @@ class PlaybackService :
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
playbackManager.synchronizePosition(player.currentPosition)
playbackManager.synchronizePosition(this, player.currentPosition)
}
}
@ -239,17 +238,9 @@ class PlaybackService :
}
}
// --- PLAYBACK STATE CALLBACK OVERRIDES ---
// --- CONTROLLER OVERRIDES ---
override fun onIndexMoved(index: Int) {
loadSong(playbackManager.song)
}
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
loadSong(playbackManager.song)
}
private fun loadSong(song: Song?) {
override fun loadSong(song: Song?) {
if (song == null) {
// Clear if there's nothing to play.
logD("Nothing playing, stopping playback")
@ -264,6 +255,14 @@ class PlaybackService :
notificationComponent.updateMetadata(song, playbackManager.parent)
}
override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs)
}
override fun shouldPrevRewind() =
settingsManager.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
override fun onPlayingChanged(isPlaying: Boolean) {
player.playWhenReady = isPlaying
notificationComponent.updatePlaying(isPlaying)
@ -281,11 +280,6 @@ class PlaybackService :
}
}
override fun onSeek(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs)
}
// --- SETTINGSMANAGER OVERRIDES ---
override fun onReplayGainSettingsChanged() {
@ -432,6 +426,7 @@ class PlaybackService :
companion object {
private const val POS_POLL_INTERVAL = 1000L
private const val REWIND_THRESHOLD = 3000L
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"