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:
parent
29fe849565
commit
b8cc153f07
2 changed files with 99 additions and 39 deletions
|
@ -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]. */
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue