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")
|
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() {
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue