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 kotlin.math.max
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
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.music.Song
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master class (and possible god object) for the playback state.
|
* 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
|
* - If you want to use the playback state with the ExoPlayer instance or system-side things, use
|
||||||
* [org.oxycblt.auxio.playback.system.PlaybackService].
|
* [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].
|
* All access should be done with [PlaybackStateManager.getInstance].
|
||||||
* @author OxygenCobalt
|
* @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
|
* TODO: Bug test app behavior when playback stops
|
||||||
*/
|
*/
|
||||||
|
@ -89,16 +94,16 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
private val callbacks = mutableListOf<Callback>()
|
private val callbacks = mutableListOf<Callback>()
|
||||||
|
private var controller: Controller? = null
|
||||||
|
|
||||||
/** Add a callback to this instance. Make sure to remove it when done. */
|
/** Add a callback to this instance. Make sure to remove it when done. */
|
||||||
fun addCallback(callback: Callback) {
|
fun addCallback(callback: Callback) {
|
||||||
if (isInitialized) {
|
if (isInitialized) {
|
||||||
callback.onNewPlayback(index, queue, parent)
|
callback.onNewPlayback(index, queue, parent)
|
||||||
callback.onSeek(positionMs)
|
|
||||||
callback.onPositionChanged(positionMs)
|
callback.onPositionChanged(positionMs)
|
||||||
|
callback.onPlayingChanged(isPlaying)
|
||||||
callback.onRepeatChanged(repeatMode)
|
callback.onRepeatChanged(repeatMode)
|
||||||
callback.onShuffledChanged(isShuffled)
|
callback.onShuffledChanged(isShuffled)
|
||||||
callback.onPlayingChanged(isPlaying)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
callbacks.add(callback)
|
callbacks.add(callback)
|
||||||
|
@ -109,6 +114,34 @@ class PlaybackStateManager private constructor() {
|
||||||
callbacks.remove(callback)
|
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 ---
|
// --- PLAYING FUNCTIONS ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -177,7 +210,7 @@ class PlaybackStateManager private constructor() {
|
||||||
/** Go to the previous song, doing any checks that are needed. */
|
/** Go to the previous song, doing any checks that are needed. */
|
||||||
fun prev() {
|
fun prev() {
|
||||||
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
|
// 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()
|
rewind()
|
||||||
isPlaying = true
|
isPlaying = true
|
||||||
} else {
|
} else {
|
||||||
|
@ -279,7 +312,12 @@ class PlaybackStateManager private constructor() {
|
||||||
* @param positionMs The new position in millis.
|
* @param positionMs The new position in millis.
|
||||||
* @see seekTo
|
* @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.
|
// Don't accept any bugged positions that are over the duration of the song.
|
||||||
val maxDuration = song?.durationMs ?: -1
|
val maxDuration = song?.durationMs ?: -1
|
||||||
if (positionMs <= maxDuration) {
|
if (positionMs <= maxDuration) {
|
||||||
|
@ -289,13 +327,12 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* **Seek** to a [positionMs], this calls [PlaybackStateManager.Callback.onSeek] to notify
|
* **Seek** to a [positionMs].
|
||||||
* elements that rely on that.
|
|
||||||
* @param positionMs The position to seek to in millis.
|
* @param positionMs The position to seek to in millis.
|
||||||
*/
|
*/
|
||||||
fun seekTo(positionMs: Long) {
|
fun seekTo(positionMs: Long) {
|
||||||
this.positionMs = positionMs
|
this.positionMs = positionMs
|
||||||
notifySeekEvent()
|
controller?.seekTo(positionMs)
|
||||||
notifyPositionChanged()
|
notifyPositionChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,6 +415,7 @@ class PlaybackStateManager private constructor() {
|
||||||
// --- CALLBACKS ---
|
// --- CALLBACKS ---
|
||||||
|
|
||||||
private fun notifyIndexMoved() {
|
private fun notifyIndexMoved() {
|
||||||
|
controller?.loadSong(song)
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onIndexMoved(index)
|
callback.onIndexMoved(index)
|
||||||
}
|
}
|
||||||
|
@ -390,12 +428,14 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyNewPlayback() {
|
private fun notifyNewPlayback() {
|
||||||
|
controller?.loadSong(song)
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onNewPlayback(index, queue, parent)
|
callback.onNewPlayback(index, queue, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyPlayingChanged() {
|
private fun notifyPlayingChanged() {
|
||||||
|
controller?.onPlayingChanged(isPlaying)
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onPlayingChanged(isPlaying)
|
callback.onPlayingChanged(isPlaying)
|
||||||
}
|
}
|
||||||
|
@ -408,21 +448,38 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyRepeatModeChanged() {
|
private fun notifyRepeatModeChanged() {
|
||||||
|
controller?.onRepeatChanged(repeatMode)
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onRepeatChanged(repeatMode)
|
callback.onRepeatChanged(repeatMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifyShuffledChanged() {
|
private fun notifyShuffledChanged() {
|
||||||
|
controller?.onShuffledChanged(isShuffled)
|
||||||
for (callback in callbacks) {
|
for (callback in callbacks) {
|
||||||
callback.onShuffledChanged(isShuffled)
|
callback.onShuffledChanged(isShuffled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notifySeekEvent() {
|
/** Represents a class capable of managing the internal player. */
|
||||||
for (callback in callbacks) {
|
interface Controller {
|
||||||
callback.onSeek(positionMs)
|
/** 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].
|
* [PlaybackStateManager] using [addCallback], remove them on destruction with [removeCallback].
|
||||||
*/
|
*/
|
||||||
interface Callback {
|
interface Callback {
|
||||||
|
/** Called when the index is moved, but the queue does not change. This changes the song. */
|
||||||
fun onIndexMoved(index: Int) {}
|
fun onIndexMoved(index: Int) {}
|
||||||
|
|
||||||
|
/** Called when the queue and/or index changed, but the song has not. */
|
||||||
fun onQueueChanged(index: Int, queue: List<Song>) {}
|
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?) {}
|
fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {}
|
||||||
|
|
||||||
|
/** Called when the playing state is changed. */
|
||||||
fun onPlayingChanged(isPlaying: Boolean) {}
|
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 {
|
companion object {
|
||||||
private const val REWIND_THRESHOLD = 3000L
|
|
||||||
|
|
||||||
@Volatile private var INSTANCE: PlaybackStateManager? = null
|
@Volatile private var INSTANCE: PlaybackStateManager? = null
|
||||||
|
|
||||||
/** Get/Instantiate the single instance of [PlaybackStateManager]. */
|
/** Get/Instantiate the single instance of [PlaybackStateManager]. */
|
||||||
|
|
|
@ -46,7 +46,6 @@ import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.oxycblt.auxio.BuildConfig
|
import org.oxycblt.auxio.BuildConfig
|
||||||
import org.oxycblt.auxio.IntegerTable
|
import org.oxycblt.auxio.IntegerTable
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
|
||||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||||
|
@ -74,7 +73,7 @@ class PlaybackService :
|
||||||
Service(),
|
Service(),
|
||||||
Player.Listener,
|
Player.Listener,
|
||||||
NotificationComponent.Callback,
|
NotificationComponent.Callback,
|
||||||
PlaybackStateManager.Callback,
|
PlaybackStateManager.Controller,
|
||||||
SettingsManager.Callback {
|
SettingsManager.Callback {
|
||||||
// Player components
|
// Player components
|
||||||
private lateinit var player: ExoPlayer
|
private lateinit var player: ExoPlayer
|
||||||
|
@ -111,7 +110,7 @@ class PlaybackService :
|
||||||
|
|
||||||
positionScope.launch {
|
positionScope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
playbackManager.synchronizePosition(player.currentPosition)
|
playbackManager.synchronizePosition(this@PlaybackService, player.currentPosition)
|
||||||
delay(POS_POLL_INTERVAL)
|
delay(POS_POLL_INTERVAL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,7 +138,7 @@ class PlaybackService :
|
||||||
|
|
||||||
// --- PLAYBACKSTATEMANAGER SETUP ---
|
// --- PLAYBACKSTATEMANAGER SETUP ---
|
||||||
|
|
||||||
playbackManager.addCallback(this)
|
playbackManager.registerController(this)
|
||||||
settingsManager.addCallback(this)
|
settingsManager.addCallback(this)
|
||||||
|
|
||||||
logD("Service created")
|
logD("Service created")
|
||||||
|
@ -166,7 +165,7 @@ class PlaybackService :
|
||||||
// Pause just in case this destruction was unexpected.
|
// Pause just in case this destruction was unexpected.
|
||||||
playbackManager.isPlaying = false
|
playbackManager.isPlaying = false
|
||||||
|
|
||||||
playbackManager.removeCallback(this)
|
playbackManager.unregisterController(this)
|
||||||
settingsManager.removeCallback(this)
|
settingsManager.removeCallback(this)
|
||||||
unregisterReceiver(systemReceiver)
|
unregisterReceiver(systemReceiver)
|
||||||
serviceJob.cancel()
|
serviceJob.cancel()
|
||||||
|
@ -218,7 +217,7 @@ class PlaybackService :
|
||||||
reason: Int
|
reason: Int
|
||||||
) {
|
) {
|
||||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
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) {
|
override fun loadSong(song: Song?) {
|
||||||
loadSong(playbackManager.song)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewPlayback(index: Int, queue: List<Song>, parent: MusicParent?) {
|
|
||||||
loadSong(playbackManager.song)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadSong(song: Song?) {
|
|
||||||
if (song == null) {
|
if (song == null) {
|
||||||
// Clear if there's nothing to play.
|
// Clear if there's nothing to play.
|
||||||
logD("Nothing playing, stopping playback")
|
logD("Nothing playing, stopping playback")
|
||||||
|
@ -264,6 +255,14 @@ class PlaybackService :
|
||||||
notificationComponent.updateMetadata(song, playbackManager.parent)
|
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) {
|
override fun onPlayingChanged(isPlaying: Boolean) {
|
||||||
player.playWhenReady = isPlaying
|
player.playWhenReady = isPlaying
|
||||||
notificationComponent.updatePlaying(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 ---
|
// --- SETTINGSMANAGER OVERRIDES ---
|
||||||
|
|
||||||
override fun onReplayGainSettingsChanged() {
|
override fun onReplayGainSettingsChanged() {
|
||||||
|
@ -432,6 +426,7 @@ class PlaybackService :
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val POS_POLL_INTERVAL = 1000L
|
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_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
|
||||||
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
|
||||||
|
|
Loading…
Reference in a new issue