playback: reimplement state saving
This commit is contained in:
parent
1d63ad5b7b
commit
bd240f967e
8 changed files with 440 additions and 320 deletions
|
@ -138,23 +138,18 @@ class IndexerService :
|
|||
logD("Music changed, updating shared objects")
|
||||
// Wipe possibly-invalidated outdated covers
|
||||
imageLoader.memoryCache?.clear()
|
||||
// // Clear invalid models from PlaybackStateManager. This is not connected
|
||||
// // to a listener as it is bad practice for a shared object to attach to
|
||||
// // the listener system of another.
|
||||
// playbackManager.toSavedState()?.let { savedState ->
|
||||
// playbackManager.applySavedState(
|
||||
// PlaybackStateManager.SavedState(
|
||||
// parent =
|
||||
// savedState.parent?.let { musicRepository.find(it.uid) as?
|
||||
// MusicParent },
|
||||
// queueState =
|
||||
// savedState.queueState.remap { song ->
|
||||
// deviceLibrary.findSong(requireNotNull(song).uid)
|
||||
// },
|
||||
// positionMs = savedState.positionMs,
|
||||
// repeatMode = savedState.repeatMode),
|
||||
// true)
|
||||
// }
|
||||
// Clear invalid models from PlaybackStateManager. This is not connected
|
||||
// to a listener as it is bad practice for a shared object to attach to
|
||||
// the listener system of another.
|
||||
playbackManager.toSavedState()?.let { savedState ->
|
||||
playbackManager.applySavedState(
|
||||
savedState.copy(
|
||||
heap =
|
||||
savedState.heap.map { song ->
|
||||
song?.let { deviceLibrary.findSong(it.uid) }
|
||||
}),
|
||||
true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIndexingStateChanged() {
|
||||
|
|
|
@ -37,8 +37,8 @@ import org.oxycblt.auxio.playback.state.RepeatMode
|
|||
* @author Alexander Capehart
|
||||
*/
|
||||
@Database(
|
||||
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
|
||||
version = 32,
|
||||
entities = [PlaybackState::class, QueueHeapItem::class, QueueShuffledMappingItem::class],
|
||||
version = 38,
|
||||
exportSchema = false)
|
||||
@TypeConverters(Music.UID.TypeConverters::class)
|
||||
abstract class PersistenceDatabase : RoomDatabase() {
|
||||
|
@ -109,15 +109,16 @@ interface QueueDao {
|
|||
/**
|
||||
* 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. */
|
||||
@Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap()
|
||||
|
||||
/** 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.
|
||||
|
@ -129,10 +130,10 @@ interface QueueDao {
|
|||
/**
|
||||
* 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)
|
||||
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
|
||||
|
@ -148,5 +149,4 @@ data class PlaybackState(
|
|||
|
||||
@Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID)
|
||||
|
||||
@Entity
|
||||
data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int)
|
||||
@Entity data class QueueShuffledMappingItem(@PrimaryKey val id: Int, val index: Int)
|
||||
|
|
|
@ -53,48 +53,37 @@ constructor(
|
|||
override suspend fun readState(): PlaybackStateManager.SavedState? {
|
||||
val deviceLibrary = musicRepository.deviceLibrary ?: return null
|
||||
val playbackState: PlaybackState
|
||||
val heap: List<QueueHeapItem>
|
||||
val mapping: List<QueueMappingItem>
|
||||
val heapItems: List<QueueHeapItem>
|
||||
val mappingItems: List<QueueShuffledMappingItem>
|
||||
try {
|
||||
playbackState = playbackStateDao.getState() ?: return null
|
||||
heap = queueDao.getHeap()
|
||||
mapping = queueDao.getMapping()
|
||||
heapItems = queueDao.getHeap()
|
||||
mappingItems = queueDao.getShuffledMapping()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable read playback state")
|
||||
logE(e.stackTraceToString())
|
||||
return null
|
||||
}
|
||||
|
||||
val orderedMapping = mutableListOf<Int>()
|
||||
val shuffledMapping = mutableListOf<Int>()
|
||||
for (entry in mapping) {
|
||||
orderedMapping.add(entry.orderedIndex)
|
||||
shuffledMapping.add(entry.shuffledIndex)
|
||||
}
|
||||
|
||||
val heap = heapItems.map { deviceLibrary.findSong(it.uid) }
|
||||
val shuffledMapping = mappingItems.map { it.index }
|
||||
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
|
||||
logD("Successfully read playback state")
|
||||
|
||||
// return PlaybackStateManager.SavedState(
|
||||
// parent = parent,
|
||||
// queueState =
|
||||
// Queue.SavedState(
|
||||
// heap.map { deviceLibrary.findSong(it.uid) },
|
||||
// orderedMapping,
|
||||
// shuffledMapping,
|
||||
// playbackState.index,
|
||||
// playbackState.songUid),
|
||||
// positionMs = playbackState.positionMs,
|
||||
// repeatMode = playbackState.repeatMode)
|
||||
|
||||
return null
|
||||
return PlaybackStateManager.SavedState(
|
||||
positionMs = playbackState.positionMs,
|
||||
repeatMode = playbackState.repeatMode,
|
||||
parent = parent,
|
||||
heap = heap,
|
||||
shuffledMapping = shuffledMapping,
|
||||
index = playbackState.index,
|
||||
songUid = playbackState.songUid)
|
||||
}
|
||||
|
||||
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
|
||||
try {
|
||||
playbackStateDao.nukeState()
|
||||
queueDao.nukeHeap()
|
||||
queueDao.nukeMapping()
|
||||
queueDao.nukeShuffledMapping()
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to clear previous state")
|
||||
logE(e.stackTraceToString())
|
||||
|
@ -107,29 +96,23 @@ constructor(
|
|||
val playbackState =
|
||||
PlaybackState(
|
||||
id = 0,
|
||||
index = state.queueState.index,
|
||||
index = state.index,
|
||||
positionMs = state.positionMs,
|
||||
repeatMode = state.repeatMode,
|
||||
songUid = state.queueState.songUid,
|
||||
songUid = state.songUid,
|
||||
parentUid = state.parent?.uid)
|
||||
|
||||
// Convert the remaining queue information do their database-specific counterparts.
|
||||
val heap =
|
||||
state.queueState.heap.mapIndexed { i, song ->
|
||||
QueueHeapItem(i, requireNotNull(song).uid)
|
||||
}
|
||||
state.heap.mapIndexed { i, song -> QueueHeapItem(i, requireNotNull(song).uid) }
|
||||
|
||||
val mapping =
|
||||
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
|
||||
i,
|
||||
pair ->
|
||||
QueueMappingItem(i, pair.first, pair.second)
|
||||
}
|
||||
val shuffledMapping =
|
||||
state.shuffledMapping.mapIndexed { i, index -> QueueShuffledMappingItem(i, index) }
|
||||
|
||||
try {
|
||||
playbackStateDao.insertState(playbackState)
|
||||
queueDao.insertHeap(heap)
|
||||
queueDao.insertMapping(mapping)
|
||||
queueDao.insertShuffledMapping(shuffledMapping)
|
||||
} catch (e: Exception) {
|
||||
logE("Unable to write new state")
|
||||
logE(e.stackTraceToString())
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.net.Uri
|
|||
import android.os.SystemClock
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
|
@ -33,21 +32,13 @@ interface PlaybackStateHolder {
|
|||
|
||||
val parent: MusicParent?
|
||||
|
||||
fun resolveQueue(): List<Song>
|
||||
|
||||
fun resolveIndex(): Int
|
||||
fun resolveQueue(): RawQueue
|
||||
|
||||
val isShuffled: Boolean
|
||||
|
||||
val audioSessionId: Int
|
||||
|
||||
fun newPlayback(
|
||||
queue: List<Song>,
|
||||
start: Song?,
|
||||
parent: MusicParent?,
|
||||
shuffled: Boolean,
|
||||
play: Boolean
|
||||
)
|
||||
fun newPlayback(queue: List<Song>, start: Song?, parent: MusicParent?, shuffled: Boolean)
|
||||
|
||||
fun playing(playing: Boolean)
|
||||
|
||||
|
@ -61,32 +52,65 @@ interface PlaybackStateHolder {
|
|||
|
||||
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 applySavedState(parent: MusicParent?, rawQueue: RawQueue)
|
||||
}
|
||||
|
||||
sealed interface StateEvent {
|
||||
data object IndexMoved : StateEvent
|
||||
sealed interface StateAck {
|
||||
data object IndexMoved : StateAck
|
||||
|
||||
data class QueueChanged(val instructions: UpdateInstructions, val songChanged: Boolean) :
|
||||
StateEvent
|
||||
data class PlayNext(val at: Int, val size: Int) : StateAck
|
||||
|
||||
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 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. */
|
||||
class Progression
|
||||
private constructor(
|
||||
|
|
|
@ -20,6 +20,8 @@ package org.oxycblt.auxio.playback.state
|
|||
|
||||
import javax.inject.Inject
|
||||
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.Song
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -180,7 +182,16 @@ interface PlaybackStateManager {
|
|||
*/
|
||||
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.
|
||||
|
@ -243,30 +254,37 @@ interface PlaybackStateManager {
|
|||
/**
|
||||
* Called when the position of the currently playing item has changed, changing the current
|
||||
* [Song], but no other queue attribute has changed.
|
||||
*
|
||||
* @param index The new index of the currently playing [Song].
|
||||
*/
|
||||
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 change The type of [Queue.Change] that occurred.
|
||||
* @param queue The songs of the new queue.
|
||||
* @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) {}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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) {}
|
||||
|
||||
/**
|
||||
* Called when a new playback configuration was created.
|
||||
*
|
||||
* @param queue The new [Queue].
|
||||
* @param parent The new [MusicParent] being played from, or null if playing from all songs.
|
||||
* @param parent The [MusicParent] item currently being played from.
|
||||
* @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(
|
||||
parent: MusicParent?,
|
||||
|
@ -294,15 +312,17 @@ interface PlaybackStateManager {
|
|||
* A condensed representation of the playback state that can be persisted.
|
||||
*
|
||||
* @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 repeatMode The current [RepeatMode].
|
||||
*/
|
||||
data class SavedState(
|
||||
val parent: MusicParent?,
|
||||
val queueState: SavedQueue,
|
||||
val positionMs: Long,
|
||||
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 index: Int,
|
||||
val isShuffled: Boolean,
|
||||
val rawQueue: RawQueue
|
||||
)
|
||||
|
||||
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
|
||||
|
@ -327,7 +348,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
queue = emptyList(),
|
||||
index = -1,
|
||||
isShuffled = false,
|
||||
)
|
||||
rawQueue = RawQueue.nil())
|
||||
@Volatile private var stateHolder: PlaybackStateHolder? = null
|
||||
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
|
||||
@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]")
|
||||
// Played something, so we are initialized now
|
||||
isInitialized = true
|
||||
stateHolder.newPlayback(queue, song, parent, shuffled, true)
|
||||
stateHolder.newPlayback(queue, song, parent, shuffled)
|
||||
}
|
||||
|
||||
// --- QUEUE FUNCTIONS ---
|
||||
|
@ -418,6 +439,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to next song")
|
||||
stateHolder.next()
|
||||
stateHolder.playing(true)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -425,6 +447,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to previous song")
|
||||
stateHolder.prev()
|
||||
stateHolder.playing(true)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -432,6 +455,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
val stateHolder = stateHolder ?: return
|
||||
logD("Going to index $index")
|
||||
stateHolder.goto(index)
|
||||
stateHolder.playing(true)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
|
@ -442,7 +466,7 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
} else {
|
||||
val stateHolder = stateHolder ?: return
|
||||
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 {
|
||||
val stateHolder = stateHolder ?: return
|
||||
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) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Moving item $src to position $dst")
|
||||
stateHolder.move(src, dst)
|
||||
stateHolder.move(src, dst, StateAck.Move(src, dst))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun removeQueueItem(at: Int) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Removing item at $at")
|
||||
stateHolder.remove(at)
|
||||
stateHolder.remove(at, StateAck.Remove(at))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
val stateHolder = stateHolder ?: return
|
||||
logD("Reordering queue [shuffled=$shuffled]")
|
||||
stateHolder.reorder(shuffled)
|
||||
stateHolder.shuffled(shuffled)
|
||||
}
|
||||
|
||||
// --- INTERNAL PLAYER FUNCTIONS ---
|
||||
|
@ -525,57 +549,113 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
}
|
||||
|
||||
@Synchronized
|
||||
override fun dispatchEvent(stateHolder: PlaybackStateHolder, event: StateEvent) {
|
||||
override fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) {
|
||||
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
|
||||
logW("Given internal player did not match current internal player")
|
||||
return
|
||||
}
|
||||
|
||||
when (event) {
|
||||
is StateEvent.IndexMoved -> {
|
||||
stateMirror =
|
||||
stateMirror.copy(
|
||||
index = stateHolder.resolveIndex(),
|
||||
)
|
||||
when (ack) {
|
||||
is StateAck.IndexMoved -> {
|
||||
val rawQueue = stateHolder.resolveQueue()
|
||||
stateMirror = stateMirror.copy(index = rawQueue.resolveIndex(), rawQueue = rawQueue)
|
||||
listeners.forEach { it.onIndexMoved(stateMirror.index) }
|
||||
}
|
||||
is StateEvent.QueueChanged -> {
|
||||
val instructions = event.instructions
|
||||
val newIndex = stateHolder.resolveIndex()
|
||||
val changeType =
|
||||
when {
|
||||
event.songChanged -> {
|
||||
QueueChange.Type.SONG
|
||||
}
|
||||
stateMirror.index != newIndex -> QueueChange.Type.INDEX
|
||||
else -> QueueChange.Type.MAPPING
|
||||
}
|
||||
stateMirror = stateMirror.copy(queue = stateHolder.resolveQueue(), index = newIndex)
|
||||
val change = QueueChange(changeType, instructions)
|
||||
is StateAck.PlayNext -> {
|
||||
val rawQueue = stateHolder.resolveQueue()
|
||||
val change =
|
||||
QueueChange(QueueChange.Type.MAPPING, UpdateInstructions.Add(ack.at, ack.size))
|
||||
stateMirror =
|
||||
stateMirror.copy(
|
||||
queue = rawQueue.resolveSongs(),
|
||||
rawQueue = rawQueue,
|
||||
)
|
||||
listeners.forEach {
|
||||
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.copy(
|
||||
queue = stateHolder.resolveQueue(),
|
||||
index = stateHolder.resolveIndex(),
|
||||
isShuffled = stateHolder.isShuffled,
|
||||
queue = rawQueue.resolveSongs(),
|
||||
rawQueue = rawQueue,
|
||||
)
|
||||
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 {
|
||||
it.onQueueReordered(
|
||||
stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
|
||||
}
|
||||
}
|
||||
is StateEvent.NewPlayback -> {
|
||||
is StateAck.NewPlayback -> {
|
||||
val rawQueue = stateHolder.resolveQueue()
|
||||
stateMirror =
|
||||
stateMirror.copy(
|
||||
parent = stateHolder.parent,
|
||||
queue = stateHolder.resolveQueue(),
|
||||
index = stateHolder.resolveIndex(),
|
||||
queue = rawQueue.resolveSongs(),
|
||||
index = rawQueue.resolveIndex(),
|
||||
isShuffled = stateHolder.isShuffled,
|
||||
)
|
||||
rawQueue = rawQueue)
|
||||
listeners.forEach {
|
||||
it.onNewPlayback(
|
||||
stateMirror.parent,
|
||||
|
@ -584,14 +664,14 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
stateMirror.isShuffled)
|
||||
}
|
||||
}
|
||||
is StateEvent.ProgressionChanged -> {
|
||||
is StateAck.ProgressionChanged -> {
|
||||
stateMirror =
|
||||
stateMirror.copy(
|
||||
progression = stateHolder.progression,
|
||||
)
|
||||
listeners.forEach { it.onProgressionChanged(stateMirror.progression) }
|
||||
}
|
||||
is StateEvent.RepeatModeChanged -> {
|
||||
is StateAck.RepeatModeChanged -> {
|
||||
stateMirror =
|
||||
stateMirror.copy(
|
||||
repeatMode = stateHolder.repeatMode,
|
||||
|
@ -603,51 +683,99 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
|||
|
||||
// --- PERSISTENCE FUNCTIONS ---
|
||||
|
||||
@Synchronized override fun toSavedState() = null
|
||||
// queue.toSavedState()?.let {
|
||||
// PlaybackStateManager.SavedState(
|
||||
// parent = parent,
|
||||
// queueState = it,
|
||||
// positionMs = progression.calculateElapsedPositionMs(),
|
||||
// repeatMode = repeatMode)
|
||||
// }
|
||||
@Synchronized
|
||||
override fun toSavedState(): PlaybackStateManager.SavedState? {
|
||||
val currentSong = currentSong ?: return null
|
||||
return PlaybackStateManager.SavedState(
|
||||
positionMs = stateMirror.progression.calculateElapsedPositionMs(),
|
||||
repeatMode = stateMirror.repeatMode,
|
||||
parent = stateMirror.parent,
|
||||
heap = stateMirror.rawQueue.heap,
|
||||
shuffledMapping = stateMirror.rawQueue.shuffledMapping,
|
||||
index = stateMirror.index,
|
||||
songUid = currentSong.uid,
|
||||
)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun applySavedState(
|
||||
savedState: PlaybackStateManager.SavedState,
|
||||
destructive: Boolean
|
||||
) {
|
||||
// if (isInitialized && !destructive) {
|
||||
// logW("Already initialized, cannot apply saved state")
|
||||
// return
|
||||
// }
|
||||
// val stateHolder = stateHolder ?: return
|
||||
// logD("Applying state $savedState")
|
||||
//
|
||||
// val lastSong = queue.currentSong
|
||||
// parent = savedState.parent
|
||||
// queue.applySavedState(savedState.queueState)
|
||||
// repeatMode = savedState.repeatMode
|
||||
// notifyNewPlayback()
|
||||
//
|
||||
// // Check if we need to reload the player with a new music file, or if we can just
|
||||
// leave
|
||||
// // it be. Specifically done so we don't pause on music updates that don't really
|
||||
// change
|
||||
// // what's playing (ex. playlist editing)
|
||||
// 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.
|
||||
// stateHolder.loadSong(queue.currentSong, false)
|
||||
// if (queue.currentSong != null) {
|
||||
// logD("Seeking to saved position ${savedState.positionMs}ms")
|
||||
// // Internal player may have reloaded the media item, re-seek to the
|
||||
// previous
|
||||
// // position
|
||||
// seekTo(savedState.positionMs)
|
||||
// }
|
||||
// }
|
||||
if (isInitialized && !destructive) {
|
||||
logW("Already initialized, cannot apply saved state")
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// and it's general composition.
|
||||
val heap = mutableListOf<Song>()
|
||||
val adjustments = mutableListOf<Int?>()
|
||||
var currentShift = 0
|
||||
for (song in savedState.heap) {
|
||||
if (song != null) {
|
||||
heap.add(song)
|
||||
adjustments.add(currentShift)
|
||||
} else {
|
||||
adjustments.add(null)
|
||||
currentShift -= 1
|
||||
}
|
||||
}
|
||||
|
||||
logD("Created adjustment mapping [max shift=$currentShift]")
|
||||
|
||||
val shuffledMapping =
|
||||
savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { index ->
|
||||
adjustments[index]?.let { index + it }
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ import java.util.*
|
|||
*
|
||||
* @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)
|
||||
|
||||
constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex))
|
||||
|
|
|
@ -23,15 +23,19 @@ import androidx.media3.common.MediaItem
|
|||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RawQueue
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
val ExoPlayer.song
|
||||
get() = currentMediaItem?.song
|
||||
|
||||
fun ExoPlayer.resolveIndex() = unscrambleQueueIndices().indexOf(currentMediaItemIndex)
|
||||
|
||||
fun ExoPlayer.resolveQueue() = unscrambleQueueIndices().map { getMediaItemAt(it).song }
|
||||
fun ExoPlayer.resolveQueue(): RawQueue {
|
||||
val heap = (0 until mediaItemCount).map { getMediaItemAt(it).song }
|
||||
val shuffledMapping = if (shuffleModeEnabled) unscrambleQueueIndices() else emptyList()
|
||||
logD(shuffledMapping)
|
||||
return RawQueue(heap, shuffledMapping, currentMediaItemIndex)
|
||||
}
|
||||
|
||||
val ExoPlayer.repeat: RepeatMode
|
||||
get() =
|
||||
|
@ -57,10 +61,6 @@ fun ExoPlayer.orderedQueue(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() })
|
||||
shuffleModeEnabled = true
|
||||
val startIndex =
|
||||
|
@ -73,11 +73,22 @@ fun ExoPlayer.shuffledQueue(queue: Collection<Song>, start: Song?) {
|
|||
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")
|
||||
shuffleModeEnabled = 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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.list.ListSettings
|
||||
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
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.PlaybackStateManager
|
||||
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.StateEvent
|
||||
import org.oxycblt.auxio.playback.state.StateAck
|
||||
import org.oxycblt.auxio.service.ForegroundManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logE
|
||||
|
@ -237,8 +237,6 @@ class PlaybackService :
|
|||
override val isShuffled
|
||||
get() = player.shuffleModeEnabled
|
||||
|
||||
override fun resolveIndex() = player.resolveIndex()
|
||||
|
||||
override fun resolveQueue() = player.resolveQueue()
|
||||
|
||||
override val audioSessionId: Int
|
||||
|
@ -248,8 +246,7 @@ class PlaybackService :
|
|||
queue: List<Song>,
|
||||
start: Song?,
|
||||
parent: MusicParent?,
|
||||
shuffled: Boolean,
|
||||
play: Boolean
|
||||
shuffled: Boolean
|
||||
) {
|
||||
this.parent = parent
|
||||
if (shuffled) {
|
||||
|
@ -258,8 +255,8 @@ class PlaybackService :
|
|||
player.orderedQueue(queue, start)
|
||||
}
|
||||
player.prepare()
|
||||
player.playWhenReady = play
|
||||
playbackManager.dispatchEvent(this, StateEvent.NewPlayback)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.NewPlayback)
|
||||
}
|
||||
|
||||
override fun playing(playing: Boolean) {
|
||||
|
@ -274,7 +271,7 @@ class PlaybackService :
|
|||
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
|
||||
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
|
||||
}
|
||||
playbackManager.dispatchEvent(this, StateEvent.RepeatModeChanged)
|
||||
playbackManager.ack(this, StateAck.RepeatModeChanged)
|
||||
}
|
||||
|
||||
override fun seekTo(positionMs: Long) {
|
||||
|
@ -283,137 +280,42 @@ class PlaybackService :
|
|||
|
||||
override fun next() {
|
||||
player.seekToNext()
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
}
|
||||
|
||||
override fun prev() {
|
||||
player.seekToPrevious()
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
}
|
||||
|
||||
override fun goto(index: Int) {
|
||||
player.goto(index)
|
||||
playbackManager.dispatchEvent(this, StateEvent.IndexMoved)
|
||||
player.play()
|
||||
playbackManager.ack(this, StateAck.IndexMoved)
|
||||
}
|
||||
|
||||
override fun reorder(shuffled: Boolean) {
|
||||
player.reorder(shuffled)
|
||||
playbackManager.dispatchEvent(this, StateEvent.QueueReordered)
|
||||
override fun shuffled(shuffled: Boolean) {
|
||||
player.shuffled(shuffled)
|
||||
playbackManager.ack(this, StateAck.QueueReordered)
|
||||
}
|
||||
|
||||
override fun addToQueue(songs: List<Song>) {
|
||||
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
|
||||
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
|
||||
player.playNext(songs)
|
||||
playbackManager.dispatchEvent(
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Add(insertAt, songs.size), false))
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
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)
|
||||
playbackManager.dispatchEvent(
|
||||
this, StateEvent.QueueChanged(UpdateInstructions.Move(from, to), false))
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
override fun remove(at: Int) {
|
||||
val oldIndex = player.currentMediaItemIndex
|
||||
override fun remove(at: Int, ack: StateAck.Remove) {
|
||||
player.remove(at)
|
||||
val newIndex = player.currentMediaItemIndex
|
||||
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()) }
|
||||
}
|
||||
playbackManager.ack(this, ack)
|
||||
}
|
||||
|
||||
override fun handleDeferred(action: DeferredPlayback): Boolean {
|
||||
|
@ -456,6 +358,97 @@ class PlaybackService :
|
|||
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 ---
|
||||
|
||||
override fun onPostNotification(notification: NotificationComponent) {
|
||||
|
|
Loading…
Reference in a new issue