playback: reimplement state saving

This commit is contained in:
Alexander Capehart 2024-01-13 18:34:17 -07:00
parent 1d63ad5b7b
commit bd240f967e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 440 additions and 320 deletions

View file

@ -138,23 +138,18 @@ class IndexerService :
logD("Music changed, updating shared objects") logD("Music changed, updating shared objects")
// Wipe possibly-invalidated outdated covers // Wipe possibly-invalidated outdated covers
imageLoader.memoryCache?.clear() imageLoader.memoryCache?.clear()
// // Clear invalid models from PlaybackStateManager. This is not connected // Clear invalid models from PlaybackStateManager. This is not connected
// // to a listener as it is bad practice for a shared object to attach to // to a listener as it is bad practice for a shared object to attach to
// // the listener system of another. // the listener system of another.
// playbackManager.toSavedState()?.let { savedState -> playbackManager.toSavedState()?.let { savedState ->
// playbackManager.applySavedState( playbackManager.applySavedState(
// PlaybackStateManager.SavedState( savedState.copy(
// parent = heap =
// savedState.parent?.let { musicRepository.find(it.uid) as? savedState.heap.map { song ->
// MusicParent }, song?.let { deviceLibrary.findSong(it.uid) }
// queueState = }),
// savedState.queueState.remap { song -> true)
// deviceLibrary.findSong(requireNotNull(song).uid) }
// },
// positionMs = savedState.positionMs,
// repeatMode = savedState.repeatMode),
// true)
// }
} }
override fun onIndexingStateChanged() { override fun onIndexingStateChanged() {

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.playback.state.RepeatMode
* @author Alexander Capehart * @author Alexander Capehart
*/ */
@Database( @Database(
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class], entities = [PlaybackState::class, QueueHeapItem::class, QueueShuffledMappingItem::class],
version = 32, version = 38,
exportSchema = false) exportSchema = false)
@TypeConverters(Music.UID.TypeConverters::class) @TypeConverters(Music.UID.TypeConverters::class)
abstract class PersistenceDatabase : RoomDatabase() { abstract class PersistenceDatabase : RoomDatabase() {
@ -109,15 +109,16 @@ interface QueueDao {
/** /**
* Get the previously persisted queue mapping. * Get the previously persisted queue mapping.
* *
* @return A list of persisted [QueueMappingItem]s wrapping each heap item. * @return A list of persisted [QueueShuffledMappingItem]s wrapping each heap item.
*/ */
@Query("SELECT * FROM QueueMappingItem") suspend fun getMapping(): List<QueueMappingItem> @Query("SELECT * FROM QueueShuffledMappingItem")
suspend fun getShuffledMapping(): List<QueueShuffledMappingItem>
/** Delete any previously persisted queue heap entries. */ /** Delete any previously persisted queue heap entries. */
@Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap() @Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap()
/** Delete any previously persisted queue mapping entries. */ /** Delete any previously persisted queue mapping entries. */
@Query("DELETE FROM QueueMappingItem") suspend fun nukeMapping() @Query("DELETE FROM QueueShuffledMappingItem") suspend fun nukeShuffledMapping()
/** /**
* Insert new heap entries into the database. * Insert new heap entries into the database.
@ -129,10 +130,10 @@ interface QueueDao {
/** /**
* Insert new mapping entries into the database. * Insert new mapping entries into the database.
* *
* @param mapping The list of wrapped [QueueMappingItem] to insert. * @param mapping The list of wrapped [QueueShuffledMappingItem] to insert.
*/ */
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertMapping(mapping: List<QueueMappingItem>) suspend fun insertShuffledMapping(mapping: List<QueueShuffledMappingItem>)
} }
// TODO: Figure out how to get RepeatMode to map to an int instead of a string // TODO: Figure out how to get RepeatMode to map to an int instead of a string
@ -148,5 +149,4 @@ data class PlaybackState(
@Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID) @Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID)
@Entity @Entity data class QueueShuffledMappingItem(@PrimaryKey val id: Int, val index: Int)
data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int)

View file

@ -53,48 +53,37 @@ constructor(
override suspend fun readState(): PlaybackStateManager.SavedState? { override suspend fun readState(): PlaybackStateManager.SavedState? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null val deviceLibrary = musicRepository.deviceLibrary ?: return null
val playbackState: PlaybackState val playbackState: PlaybackState
val heap: List<QueueHeapItem> val heapItems: List<QueueHeapItem>
val mapping: List<QueueMappingItem> val mappingItems: List<QueueShuffledMappingItem>
try { try {
playbackState = playbackStateDao.getState() ?: return null playbackState = playbackStateDao.getState() ?: return null
heap = queueDao.getHeap() heapItems = queueDao.getHeap()
mapping = queueDao.getMapping() mappingItems = queueDao.getShuffledMapping()
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable read playback state") logE("Unable read playback state")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
return null return null
} }
val orderedMapping = mutableListOf<Int>() val heap = heapItems.map { deviceLibrary.findSong(it.uid) }
val shuffledMapping = mutableListOf<Int>() val shuffledMapping = mappingItems.map { it.index }
for (entry in mapping) {
orderedMapping.add(entry.orderedIndex)
shuffledMapping.add(entry.shuffledIndex)
}
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent } val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
logD("Successfully read playback state")
// return PlaybackStateManager.SavedState( return PlaybackStateManager.SavedState(
// parent = parent, positionMs = playbackState.positionMs,
// queueState = repeatMode = playbackState.repeatMode,
// Queue.SavedState( parent = parent,
// heap.map { deviceLibrary.findSong(it.uid) }, heap = heap,
// orderedMapping, shuffledMapping = shuffledMapping,
// shuffledMapping, index = playbackState.index,
// playbackState.index, songUid = playbackState.songUid)
// playbackState.songUid),
// positionMs = playbackState.positionMs,
// repeatMode = playbackState.repeatMode)
return null
} }
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean { override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
try { try {
playbackStateDao.nukeState() playbackStateDao.nukeState()
queueDao.nukeHeap() queueDao.nukeHeap()
queueDao.nukeMapping() queueDao.nukeShuffledMapping()
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to clear previous state") logE("Unable to clear previous state")
logE(e.stackTraceToString()) logE(e.stackTraceToString())
@ -107,29 +96,23 @@ constructor(
val playbackState = val playbackState =
PlaybackState( PlaybackState(
id = 0, id = 0,
index = state.queueState.index, index = state.index,
positionMs = state.positionMs, positionMs = state.positionMs,
repeatMode = state.repeatMode, repeatMode = state.repeatMode,
songUid = state.queueState.songUid, songUid = state.songUid,
parentUid = state.parent?.uid) parentUid = state.parent?.uid)
// Convert the remaining queue information do their database-specific counterparts. // Convert the remaining queue information do their database-specific counterparts.
val heap = val heap =
state.queueState.heap.mapIndexed { i, song -> state.heap.mapIndexed { i, song -> QueueHeapItem(i, requireNotNull(song).uid) }
QueueHeapItem(i, requireNotNull(song).uid)
}
val mapping = val shuffledMapping =
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed { state.shuffledMapping.mapIndexed { i, index -> QueueShuffledMappingItem(i, index) }
i,
pair ->
QueueMappingItem(i, pair.first, pair.second)
}
try { try {
playbackStateDao.insertState(playbackState) playbackStateDao.insertState(playbackState)
queueDao.insertHeap(heap) queueDao.insertHeap(heap)
queueDao.insertMapping(mapping) queueDao.insertShuffledMapping(shuffledMapping)
} catch (e: Exception) { } catch (e: Exception) {
logE("Unable to write new state") logE("Unable to write new state")
logE(e.stackTraceToString()) logE(e.stackTraceToString())

View file

@ -22,7 +22,6 @@ import android.net.Uri
import android.os.SystemClock import android.os.SystemClock
import android.support.v4.media.session.PlaybackStateCompat import android.support.v4.media.session.PlaybackStateCompat
import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.list.adapter.UpdateInstructions
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
@ -33,21 +32,13 @@ interface PlaybackStateHolder {
val parent: MusicParent? val parent: MusicParent?
fun resolveQueue(): List<Song> fun resolveQueue(): RawQueue
fun resolveIndex(): Int
val isShuffled: Boolean val isShuffled: Boolean
val audioSessionId: Int val audioSessionId: Int
fun newPlayback( fun newPlayback(queue: List<Song>, start: Song?, parent: MusicParent?, shuffled: Boolean)
queue: List<Song>,
start: Song?,
parent: MusicParent?,
shuffled: Boolean,
play: Boolean
)
fun playing(playing: Boolean) fun playing(playing: Boolean)
@ -61,32 +52,65 @@ interface PlaybackStateHolder {
fun goto(index: Int) fun goto(index: Int)
fun playNext(songs: List<Song>) fun playNext(songs: List<Song>, ack: StateAck.PlayNext)
fun addToQueue(songs: List<Song>) fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue)
fun move(from: Int, to: Int) fun move(from: Int, to: Int, ack: StateAck.Move)
fun remove(at: Int) fun remove(at: Int, ack: StateAck.Remove)
fun reorder(shuffled: Boolean) fun shuffled(shuffled: Boolean)
fun handleDeferred(action: DeferredPlayback): Boolean fun handleDeferred(action: DeferredPlayback): Boolean
fun applySavedState(parent: MusicParent?, rawQueue: RawQueue)
} }
sealed interface StateEvent { sealed interface StateAck {
data object IndexMoved : StateEvent data object IndexMoved : StateAck
data class QueueChanged(val instructions: UpdateInstructions, val songChanged: Boolean) : data class PlayNext(val at: Int, val size: Int) : StateAck
StateEvent
data object QueueReordered : StateEvent data class AddToQueue(val at: Int, val size: Int) : StateAck
data object NewPlayback : StateEvent data class Move(val from: Int, val to: Int) : StateAck
data object ProgressionChanged : StateEvent data class Remove(val index: Int) : StateAck
data object RepeatModeChanged : StateEvent data object QueueReordered : StateAck
data object NewPlayback : StateAck
data object ProgressionChanged : StateAck
data object RepeatModeChanged : StateAck
}
data class RawQueue(
val heap: List<Song>,
val shuffledMapping: List<Int>,
val heapIndex: Int,
) {
val isShuffled = shuffledMapping.isNotEmpty()
fun resolveSongs() =
if (isShuffled) {
shuffledMapping.map { heap[it] }
} else {
heap
}
fun resolveIndex() =
if (isShuffled) {
shuffledMapping.indexOf(heapIndex)
} else {
heapIndex
}
companion object {
fun nil() = RawQueue(emptyList(), emptyList(), -1)
}
} }
/** /**
@ -129,20 +153,6 @@ sealed interface DeferredPlayback {
data class Open(val uri: Uri) : DeferredPlayback data class Open(val uri: Uri) : DeferredPlayback
} }
data class Queue(val songs: List<Song>, val index: Int) {
companion object {
fun nil() = Queue(emptyList(), -1)
}
}
data class SavedQueue(
val heap: List<Song?>,
val orderedMapping: List<Int>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
)
/** A representation of the current state of audio playback. Use [from] to create an instance. */ /** A representation of the current state of audio playback. Use [from] to create an instance. */
class Progression class Progression
private constructor( private constructor(

View file

@ -20,6 +20,8 @@ package org.oxycblt.auxio.playback.state
import javax.inject.Inject import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.list.adapter.UpdateInstructions
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.util.logD import org.oxycblt.auxio.util.logD
@ -180,7 +182,16 @@ interface PlaybackStateManager {
*/ */
fun shuffled(shuffled: Boolean) fun shuffled(shuffled: Boolean)
fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) /**
* Acknowledges that an event has happened that modified the state held by the current
* [PlaybackStateHolder].
*
* @param stateHolder The [PlaybackStateHolder] to synchronize with. Must be the current
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
* implementation.
* @param ack The [StateAck] to acknowledge.
*/
fun ack(stateHolder: PlaybackStateHolder, ack: StateAck)
/** /**
* Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually. * Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually.
@ -243,30 +254,37 @@ interface PlaybackStateManager {
/** /**
* Called when the position of the currently playing item has changed, changing the current * Called when the position of the currently playing item has changed, changing the current
* [Song], but no other queue attribute has changed. * [Song], but no other queue attribute has changed.
*
* @param index The new index of the currently playing [Song].
*/ */
fun onIndexMoved(index: Int) {} fun onIndexMoved(index: Int) {}
/** /**
* Called when the [Queue] changed in a manner outlined by the given [Queue.Change]. * Called when the queue changed in a manner outlined by the given [Queue.Change].
* *
* @param queue The new [Queue]. * @param queue The songs of the new queue.
* @param change The type of [Queue.Change] that occurred. * @param index The new index of the currently playing [Song].
* @param change The [QueueChange] that occurred.
*/ */
fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {} fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {}
/** /**
* Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but * Called when the queue has changed in a non-trivial manner (such as re-shuffling), but
* the currently playing [Song] has not. * the currently playing [Song] has not.
* *
* @param queue The new [Queue]. * @param queue The songs of the new queue.
* @param index The new index of the currently playing [Song].
* @param isShuffled Whether the queue is shuffled or not.
*/ */
fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {} fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {}
/** /**
* Called when a new playback configuration was created. * Called when a new playback configuration was created.
* *
* @param queue The new [Queue]. * @param parent The [MusicParent] item currently being played from.
* @param parent The new [MusicParent] being played from, or null if playing from all songs. * @param queue The queue of [Song]s to play from.
* @param index The index of the currently playing [Song].
* @param isShuffled Whether the queue is shuffled or not.
*/ */
fun onNewPlayback( fun onNewPlayback(
parent: MusicParent?, parent: MusicParent?,
@ -294,15 +312,17 @@ interface PlaybackStateManager {
* A condensed representation of the playback state that can be persisted. * A condensed representation of the playback state that can be persisted.
* *
* @param parent The [MusicParent] item currently being played from. * @param parent The [MusicParent] item currently being played from.
* @param queueState The [SavedQueue]
* @param positionMs The current position in the currently played song, in ms * @param positionMs The current position in the currently played song, in ms
* @param repeatMode The current [RepeatMode]. * @param repeatMode The current [RepeatMode].
*/ */
data class SavedState( data class SavedState(
val parent: MusicParent?,
val queueState: SavedQueue,
val positionMs: Long, val positionMs: Long,
val repeatMode: RepeatMode, val repeatMode: RepeatMode,
val parent: MusicParent?,
val heap: List<Song?>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
) )
} }
@ -314,6 +334,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
val queue: List<Song>, val queue: List<Song>,
val index: Int, val index: Int,
val isShuffled: Boolean, val isShuffled: Boolean,
val rawQueue: RawQueue
) )
private val listeners = mutableListOf<PlaybackStateManager.Listener>() private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@ -327,7 +348,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
queue = emptyList(), queue = emptyList(),
index = -1, index = -1,
isShuffled = false, isShuffled = false,
) rawQueue = RawQueue.nil())
@Volatile private var stateHolder: PlaybackStateHolder? = null @Volatile private var stateHolder: PlaybackStateHolder? = null
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null @Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
@Volatile private var isInitialized = false @Volatile private var isInitialized = false
@ -408,7 +429,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]") logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]")
// Played something, so we are initialized now // Played something, so we are initialized now
isInitialized = true isInitialized = true
stateHolder.newPlayback(queue, song, parent, shuffled, true) stateHolder.newPlayback(queue, song, parent, shuffled)
} }
// --- QUEUE FUNCTIONS --- // --- QUEUE FUNCTIONS ---
@ -418,6 +439,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Going to next song") logD("Going to next song")
stateHolder.next() stateHolder.next()
stateHolder.playing(true)
} }
@Synchronized @Synchronized
@ -425,6 +447,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Going to previous song") logD("Going to previous song")
stateHolder.prev() stateHolder.prev()
stateHolder.playing(true)
} }
@Synchronized @Synchronized
@ -432,6 +455,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Going to index $index") logD("Going to index $index")
stateHolder.goto(index) stateHolder.goto(index)
stateHolder.playing(true)
} }
@Synchronized @Synchronized
@ -442,7 +466,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} else { } else {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Adding ${songs.size} songs to start of queue") logD("Adding ${songs.size} songs to start of queue")
stateHolder.playNext(songs) stateHolder.playNext(songs, StateAck.PlayNext(stateMirror.index + 1, songs.size))
} }
} }
@ -454,7 +478,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} else { } else {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Adding ${songs.size} songs to end of queue") logD("Adding ${songs.size} songs to end of queue")
stateHolder.addToQueue(songs) stateHolder.addToQueue(songs, StateAck.AddToQueue(stateMirror.index + 1, songs.size))
} }
} }
@ -462,21 +486,21 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
override fun moveQueueItem(src: Int, dst: Int) { override fun moveQueueItem(src: Int, dst: Int) {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Moving item $src to position $dst") logD("Moving item $src to position $dst")
stateHolder.move(src, dst) stateHolder.move(src, dst, StateAck.Move(src, dst))
} }
@Synchronized @Synchronized
override fun removeQueueItem(at: Int) { override fun removeQueueItem(at: Int) {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Removing item at $at") logD("Removing item at $at")
stateHolder.remove(at) stateHolder.remove(at, StateAck.Remove(at))
} }
@Synchronized @Synchronized
override fun shuffled(shuffled: Boolean) { override fun shuffled(shuffled: Boolean) {
val stateHolder = stateHolder ?: return val stateHolder = stateHolder ?: return
logD("Reordering queue [shuffled=$shuffled]") logD("Reordering queue [shuffled=$shuffled]")
stateHolder.reorder(shuffled) stateHolder.shuffled(shuffled)
} }
// --- INTERNAL PLAYER FUNCTIONS --- // --- INTERNAL PLAYER FUNCTIONS ---
@ -525,57 +549,113 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
} }
@Synchronized @Synchronized
override fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) { override fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) {
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) { if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
logW("Given internal player did not match current internal player") logW("Given internal player did not match current internal player")
return return
} }
when (event) { when (ack) {
is StateEvent.IndexMoved -> { is StateAck.IndexMoved -> {
stateMirror = val rawQueue = stateHolder.resolveQueue()
stateMirror.copy( stateMirror = stateMirror.copy(index = rawQueue.resolveIndex(), rawQueue = rawQueue)
index = stateHolder.resolveIndex(),
)
listeners.forEach { it.onIndexMoved(stateMirror.index) } listeners.forEach { it.onIndexMoved(stateMirror.index) }
} }
is StateEvent.QueueChanged -> { is StateAck.PlayNext -> {
val instructions = event.instructions val rawQueue = stateHolder.resolveQueue()
val newIndex = stateHolder.resolveIndex() val change =
val changeType = QueueChange(QueueChange.Type.MAPPING, UpdateInstructions.Add(ack.at, ack.size))
when { stateMirror =
event.songChanged -> { stateMirror.copy(
QueueChange.Type.SONG queue = rawQueue.resolveSongs(),
} rawQueue = rawQueue,
stateMirror.index != newIndex -> QueueChange.Type.INDEX )
else -> QueueChange.Type.MAPPING
}
stateMirror = stateMirror.copy(queue = stateHolder.resolveQueue(), index = newIndex)
val change = QueueChange(changeType, instructions)
listeners.forEach { listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change) it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
} }
} }
is StateEvent.QueueReordered -> { is StateAck.AddToQueue -> {
val rawQueue = stateHolder.resolveQueue()
val change =
QueueChange(QueueChange.Type.MAPPING, UpdateInstructions.Add(ack.at, ack.size))
stateMirror = stateMirror =
stateMirror.copy( stateMirror.copy(
queue = stateHolder.resolveQueue(), queue = rawQueue.resolveSongs(),
index = stateHolder.resolveIndex(), rawQueue = rawQueue,
isShuffled = stateHolder.isShuffled,
) )
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.Move -> {
val rawQueue = stateHolder.resolveQueue()
val newIndex = rawQueue.resolveIndex()
val change =
QueueChange(
if (stateMirror.index != newIndex) QueueChange.Type.INDEX
else QueueChange.Type.MAPPING,
UpdateInstructions.Move(ack.from, ack.to))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = newIndex,
rawQueue = rawQueue,
)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.Remove -> {
val rawQueue = stateHolder.resolveQueue()
val newIndex = rawQueue.resolveIndex()
val change =
QueueChange(
when {
ack.index == stateMirror.index -> QueueChange.Type.SONG
stateMirror.index != newIndex -> QueueChange.Type.INDEX
else -> QueueChange.Type.MAPPING
},
UpdateInstructions.Remove(ack.index, 1))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = newIndex,
rawQueue = rawQueue,
)
if (change.type == QueueChange.Type.SONG) {
playing(true)
}
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.QueueReordered -> {
val rawQueue = stateHolder.resolveQueue()
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = rawQueue.resolveIndex(),
isShuffled = stateHolder.isShuffled,
rawQueue = rawQueue)
listeners.forEach { listeners.forEach {
it.onQueueReordered( it.onQueueReordered(
stateMirror.queue, stateMirror.index, stateMirror.isShuffled) stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
} }
} }
is StateEvent.NewPlayback -> { is StateAck.NewPlayback -> {
val rawQueue = stateHolder.resolveQueue()
stateMirror = stateMirror =
stateMirror.copy( stateMirror.copy(
parent = stateHolder.parent, parent = stateHolder.parent,
queue = stateHolder.resolveQueue(), queue = rawQueue.resolveSongs(),
index = stateHolder.resolveIndex(), index = rawQueue.resolveIndex(),
isShuffled = stateHolder.isShuffled, isShuffled = stateHolder.isShuffled,
) rawQueue = rawQueue)
listeners.forEach { listeners.forEach {
it.onNewPlayback( it.onNewPlayback(
stateMirror.parent, stateMirror.parent,
@ -584,14 +664,14 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
stateMirror.isShuffled) stateMirror.isShuffled)
} }
} }
is StateEvent.ProgressionChanged -> { is StateAck.ProgressionChanged -> {
stateMirror = stateMirror =
stateMirror.copy( stateMirror.copy(
progression = stateHolder.progression, progression = stateHolder.progression,
) )
listeners.forEach { it.onProgressionChanged(stateMirror.progression) } listeners.forEach { it.onProgressionChanged(stateMirror.progression) }
} }
is StateEvent.RepeatModeChanged -> { is StateAck.RepeatModeChanged -> {
stateMirror = stateMirror =
stateMirror.copy( stateMirror.copy(
repeatMode = stateHolder.repeatMode, repeatMode = stateHolder.repeatMode,
@ -603,51 +683,99 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
// --- PERSISTENCE FUNCTIONS --- // --- PERSISTENCE FUNCTIONS ---
@Synchronized override fun toSavedState() = null @Synchronized
// queue.toSavedState()?.let { override fun toSavedState(): PlaybackStateManager.SavedState? {
// PlaybackStateManager.SavedState( val currentSong = currentSong ?: return null
// parent = parent, return PlaybackStateManager.SavedState(
// queueState = it, positionMs = stateMirror.progression.calculateElapsedPositionMs(),
// positionMs = progression.calculateElapsedPositionMs(), repeatMode = stateMirror.repeatMode,
// repeatMode = repeatMode) parent = stateMirror.parent,
// } heap = stateMirror.rawQueue.heap,
shuffledMapping = stateMirror.rawQueue.shuffledMapping,
index = stateMirror.index,
songUid = currentSong.uid,
)
}
@Synchronized @Synchronized
override fun applySavedState( override fun applySavedState(
savedState: PlaybackStateManager.SavedState, savedState: PlaybackStateManager.SavedState,
destructive: Boolean destructive: Boolean
) { ) {
// if (isInitialized && !destructive) { if (isInitialized && !destructive) {
// logW("Already initialized, cannot apply saved state") logW("Already initialized, cannot apply saved state")
// return return
// } }
// val stateHolder = stateHolder ?: return
// logD("Applying state $savedState") // The heap may not be the same if the song composition changed between state saves/reloads.
// // This also means that we must modify the shuffled mapping as well, in what it points to
// val lastSong = queue.currentSong // and it's general composition.
// parent = savedState.parent val heap = mutableListOf<Song>()
// queue.applySavedState(savedState.queueState) val adjustments = mutableListOf<Int?>()
// repeatMode = savedState.repeatMode var currentShift = 0
// notifyNewPlayback() for (song in savedState.heap) {
// if (song != null) {
// // Check if we need to reload the player with a new music file, or if we can just heap.add(song)
// leave adjustments.add(currentShift)
// // it be. Specifically done so we don't pause on music updates that don't really } else {
// change adjustments.add(null)
// // what's playing (ex. playlist editing) currentShift -= 1
// if (lastSong != queue.currentSong) { }
// logD("Song changed, must reload player") }
// // Continuing playback while also possibly doing drastic state updates is
// // a bad idea, so pause. logD("Created adjustment mapping [max shift=$currentShift]")
// stateHolder.loadSong(queue.currentSong, false)
// if (queue.currentSong != null) { val shuffledMapping =
// logD("Seeking to saved position ${savedState.positionMs}ms") savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { index ->
// // Internal player may have reloaded the media item, re-seek to the adjustments[index]?.let { index + it }
// previous }
// // position
// seekTo(savedState.positionMs) // Make sure we re-align the index to point to the previously playing song.
// } fun pointingAtSong(): Boolean {
// } val currentSong =
if (shuffledMapping.isNotEmpty()) {
shuffledMapping.getOrNull(savedState.index)?.let { heap.getOrNull(it) }
} else {
heap.getOrNull(savedState.index)
}
return currentSong?.uid == savedState.songUid
}
var index = savedState.index
while (pointingAtSong() && index > -1) {
index--
}
logD("Corrected index: ${savedState.index} -> $index")
check(shuffledMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
}
val rawQueue =
RawQueue(
heap = heap,
shuffledMapping = savedState.shuffledMapping,
heapIndex =
if (savedState.shuffledMapping.isNotEmpty()) {
savedState.shuffledMapping[savedState.index]
} else {
savedState.index
})
val oldStateMirror = stateMirror
if (oldStateMirror.rawQueue != rawQueue) {
logD("Queue changed, must reload player")
stateHolder?.applySavedState(parent, rawQueue)
}
if (oldStateMirror.progression.calculateElapsedPositionMs() != savedState.positionMs) {
logD("Seeking to saved position ${savedState.positionMs}ms")
stateHolder?.seekTo(savedState.positionMs)
}
isInitialized = true isInitialized = true
} }
} }

View file

@ -29,7 +29,7 @@ import java.util.*
* *
* @author media3 team, Alexander Capehart (OxygenCobalt) * @author media3 team, Alexander Capehart (OxygenCobalt)
*/ */
class BetterShuffleOrder private constructor(private val shuffled: IntArray) : ShuffleOrder { class BetterShuffleOrder constructor(private val shuffled: IntArray) : ShuffleOrder {
private val indexInShuffled: IntArray = IntArray(shuffled.size) private val indexInShuffled: IntArray = IntArray(shuffled.size)
constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex)) constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex))

View file

@ -23,15 +23,19 @@ import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RawQueue
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
val ExoPlayer.song val ExoPlayer.song
get() = currentMediaItem?.song get() = currentMediaItem?.song
fun ExoPlayer.resolveIndex() = unscrambleQueueIndices().indexOf(currentMediaItemIndex) fun ExoPlayer.resolveQueue(): RawQueue {
val heap = (0 until mediaItemCount).map { getMediaItemAt(it).song }
fun ExoPlayer.resolveQueue() = unscrambleQueueIndices().map { getMediaItemAt(it).song } val shuffledMapping = if (shuffleModeEnabled) unscrambleQueueIndices() else emptyList()
logD(shuffledMapping)
return RawQueue(heap, shuffledMapping, currentMediaItemIndex)
}
val ExoPlayer.repeat: RepeatMode val ExoPlayer.repeat: RepeatMode
get() = get() =
@ -57,10 +61,6 @@ fun ExoPlayer.orderedQueue(queue: Collection<Song>, start: Song?) {
} }
fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) { fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
// A fun thing about ShuffleOrder is that ExoPlayer will use cloneAndInsert to both add
// MediaItems AND repopulate MediaItems (?!?!?!?!). As a result, we have to use the default
// shuffle order and it's stupid cloneAndInsert implementation to add the songs, and then
// switch back to our implementation that actually works in normal use.
setMediaItems(queue.map { it.toMediaItem() }) setMediaItems(queue.map { it.toMediaItem() })
shuffleModeEnabled = true shuffleModeEnabled = true
val startIndex = val startIndex =
@ -73,11 +73,22 @@ fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET) seekTo(currentTimeline.getFirstWindowIndex(shuffleModeEnabled), C.TIME_UNSET)
} }
fun ExoPlayer.reorder(shuffled: Boolean) { fun ExoPlayer.applyQueue(rawQueue: RawQueue) {
setMediaItems(rawQueue.heap.map { it.toMediaItem() })
if (rawQueue.isShuffled) {
shuffleModeEnabled = true
setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
} else {
shuffleModeEnabled = false
}
seekTo(rawQueue.heapIndex, C.TIME_UNSET)
}
fun ExoPlayer.shuffled(shuffled: Boolean) {
logD("Reordering queue to $shuffled") logD("Reordering queue to $shuffled")
shuffleModeEnabled = shuffled shuffleModeEnabled = shuffled
if (shuffled) { if (shuffled) {
// Have to manually refresh the shuffle seed. // Have to manually refresh the shuffle seed and anchor it to the new current songs
setShuffleOrder(BetterShuffleOrder(mediaItemCount, currentMediaItemIndex)) setShuffleOrder(BetterShuffleOrder(mediaItemCount, currentMediaItemIndex))
} }
} }

View file

@ -48,7 +48,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.list.ListSettings import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
@ -59,8 +58,9 @@ import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateHolder import org.oxycblt.auxio.playback.state.PlaybackStateHolder
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.RawQueue
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.StateEvent import org.oxycblt.auxio.playback.state.StateAck
import org.oxycblt.auxio.service.ForegroundManager import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE import org.oxycblt.auxio.util.logE
@ -237,8 +237,6 @@ class PlaybackService :
override val isShuffled override val isShuffled
get() = player.shuffleModeEnabled get() = player.shuffleModeEnabled
override fun resolveIndex() = player.resolveIndex()
override fun resolveQueue() = player.resolveQueue() override fun resolveQueue() = player.resolveQueue()
override val audioSessionId: Int override val audioSessionId: Int
@ -248,8 +246,7 @@ class PlaybackService :
queue: List<Song>, queue: List<Song>,
start: Song?, start: Song?,
parent: MusicParent?, parent: MusicParent?,
shuffled: Boolean, shuffled: Boolean
play: Boolean
) { ) {
this.parent = parent this.parent = parent
if (shuffled) { if (shuffled) {
@ -258,8 +255,8 @@ class PlaybackService :
player.orderedQueue(queue, start) player.orderedQueue(queue, start)
} }
player.prepare() player.prepare()
player.playWhenReady = play player.play()
playbackManager.dispatchEvent(this, StateEvent.NewPlayback) playbackManager.ack(this, StateAck.NewPlayback)
} }
override fun playing(playing: Boolean) { override fun playing(playing: Boolean) {
@ -274,7 +271,7 @@ class PlaybackService :
RepeatMode.ALL -> Player.REPEAT_MODE_ALL RepeatMode.ALL -> Player.REPEAT_MODE_ALL
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
} }
playbackManager.dispatchEvent(this, StateEvent.RepeatModeChanged) playbackManager.ack(this, StateAck.RepeatModeChanged)
} }
override fun seekTo(positionMs: Long) { override fun seekTo(positionMs: Long) {
@ -283,137 +280,42 @@ class PlaybackService :
override fun next() { override fun next() {
player.seekToNext() player.seekToNext()
playbackManager.dispatchEvent(this, StateEvent.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
player.play()
} }
override fun prev() { override fun prev() {
player.seekToPrevious() player.seekToPrevious()
playbackManager.dispatchEvent(this, StateEvent.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
player.play()
} }
override fun goto(index: Int) { override fun goto(index: Int) {
player.goto(index) player.goto(index)
playbackManager.dispatchEvent(this, StateEvent.IndexMoved) playbackManager.ack(this, StateAck.IndexMoved)
player.play()
} }
override fun reorder(shuffled: Boolean) { override fun shuffled(shuffled: Boolean) {
player.reorder(shuffled) player.shuffled(shuffled)
playbackManager.dispatchEvent(this, StateEvent.QueueReordered) playbackManager.ack(this, StateAck.QueueReordered)
} }
override fun addToQueue(songs: List<Song>) { override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
val insertAt = playbackManager.index + 1
player.addToQueue(songs)
playbackManager.dispatchEvent(
this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
}
override fun playNext(songs: List<Song>) {
val insertAt = playbackManager.index + 1
player.playNext(songs) player.playNext(songs)
playbackManager.dispatchEvent( playbackManager.ack(this, ack)
this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
} }
override fun move(from: Int, to: Int) { override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
player.addToQueue(songs)
playbackManager.ack(this, ack)
}
override fun move(from: Int, to: Int, ack: StateAck.Move) {
player.move(from, to) player.move(from, to)
playbackManager.dispatchEvent( playbackManager.ack(this, ack)
this, StateEvent.QueueChanged(UpdateInstructions.Move(from, to), false))
} }
override fun remove(at: Int) { override fun remove(at: Int, ack: StateAck.Remove) {
val oldIndex = player.currentMediaItemIndex
player.remove(at) player.remove(at)
val newIndex = player.currentMediaItemIndex playbackManager.ack(this, ack)
playbackManager.dispatchEvent(
this, StateEvent.QueueChanged(UpdateInstructions.Remove(at, 1), oldIndex != newIndex))
}
// --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
hasPlayed = true
logD("Player has started playing")
if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than
// start/stop
logD("Opening audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true
}
} else if (openAudioEffectSession) {
// Make sure to close the audio session when we stop playback.
logD("Closing audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
}
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state")
playbackManager.dispatchEvent(this, StateEvent.ProgressionChanged)
}
}
override fun onPlayerError(error: PlaybackException) {
// TODO: Replace with no skipping and a notification instead
// If there's any issue, just go to the next song.
logE("Player error occured")
logE(error.stackTraceToString())
playbackManager.next()
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
// We now have a library, see if we have anything we need to do.
logD("Library obtained, requesting action")
playbackManager.requestAction(this)
}
}
// --- OTHER FUNCTIONS ---
private fun broadcastAudioEffectAction(event: String) {
logD("Broadcasting AudioEffect event: $event")
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
private fun stopAndSave() {
// This session has ended, so we need to reset this flag for when the next session starts.
hasPlayed = false
if (foregroundManager.tryStopForeground()) {
// Now that we have ended the foreground state (and thus music playback), we'll need
// to save the current state as it's not long until this service (and likely the whole
// app) is killed.
logD("Saving playback state")
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
}
} }
override fun handleDeferred(action: DeferredPlayback): Boolean { override fun handleDeferred(action: DeferredPlayback): Boolean {
@ -456,6 +358,97 @@ class PlaybackService :
return true return true
} }
override fun applySavedState(parent: MusicParent?, rawQueue: RawQueue) {
this.parent = parent
player.applyQueue(rawQueue)
player.prepare()
playbackManager.ack(this, StateAck.NewPlayback)
}
// --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
hasPlayed = true
logD("Player has started playing")
if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than
// start/stop
logD("Opening audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true
}
} else if (openAudioEffectSession) {
// Make sure to close the audio session when we stop playback.
logD("Closing audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
playbackManager.ack(this, StateAck.IndexMoved)
}
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state")
playbackManager.ack(this, StateAck.ProgressionChanged)
}
}
override fun onPlayerError(error: PlaybackException) {
// TODO: Replace with no skipping and a notification instead
// If there's any issue, just go to the next song.
logE("Player error occured")
logE(error.stackTraceToString())
playbackManager.next()
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
// We now have a library, see if we have anything we need to do.
logD("Library obtained, requesting action")
playbackManager.requestAction(this)
}
}
// --- OTHER FUNCTIONS ---
private fun broadcastAudioEffectAction(event: String) {
logD("Broadcasting AudioEffect event: $event")
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
private fun stopAndSave() {
// This session has ended, so we need to reset this flag for when the next session starts.
hasPlayed = false
if (foregroundManager.tryStopForeground()) {
// Now that we have ended the foreground state (and thus music playback), we'll need
// to save the current state as it's not long until this service (and likely the whole
// app) is killed.
logD("Saving playback state")
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
}
}
// --- MEDIASESSIONCOMPONENT OVERRIDES --- // --- MEDIASESSIONCOMPONENT OVERRIDES ---
override fun onPostNotification(notification: NotificationComponent) { override fun onPostNotification(notification: NotificationComponent) {