Persist Queue
Implement some very unoptimized queue persistence, Ill have to make it better in the future but for now it works.
This commit is contained in:
parent
da224ffda0
commit
d09ce20e02
12 changed files with 270 additions and 231 deletions
|
@ -14,6 +14,7 @@
|
|||
android:theme="@style/Theme.Base">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:windowSoftInputMode="adjustPan">
|
||||
|
|
|
@ -5,18 +5,19 @@ import androidx.room.Database
|
|||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [PlaybackState::class], version = 1, exportSchema = false)
|
||||
abstract class PlaybackStateDatabase : RoomDatabase() {
|
||||
@Database(entities = [PlaybackState::class, QueueItem::class], version = 1, exportSchema = false)
|
||||
abstract class AuxioDatabase : RoomDatabase() {
|
||||
abstract val playbackStateDAO: PlaybackStateDAO
|
||||
abstract val queueDAO: QueueDAO
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: PlaybackStateDatabase? = null
|
||||
private var INSTANCE: AuxioDatabase? = null
|
||||
|
||||
/**
|
||||
* Get/Instantiate the single instance of [PlaybackStateDatabase].
|
||||
* Get/Instantiate the single instance of [AuxioDatabase].
|
||||
*/
|
||||
fun getInstance(context: Context): PlaybackStateDatabase {
|
||||
fun getInstance(context: Context): AuxioDatabase {
|
||||
val currentInstance = INSTANCE
|
||||
|
||||
if (currentInstance != null) {
|
||||
|
@ -26,7 +27,7 @@ abstract class PlaybackStateDatabase : RoomDatabase() {
|
|||
synchronized(this) {
|
||||
val newInstance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
PlaybackStateDatabase::class.java,
|
||||
AuxioDatabase::class.java,
|
||||
"playback_state_database"
|
||||
).fallbackToDestructiveMigration().build()
|
||||
INSTANCE = newInstance
|
|
@ -18,21 +18,12 @@ data class PlaybackState(
|
|||
@ColumnInfo(name = "parent_id")
|
||||
val parentId: Long = -1L,
|
||||
|
||||
@ColumnInfo(name = "user_queue")
|
||||
val userQueueIds: String,
|
||||
|
||||
@ColumnInfo(name = "index")
|
||||
val index: Int,
|
||||
|
||||
@ColumnInfo(name = "mode")
|
||||
val mode: Int,
|
||||
|
||||
@ColumnInfo(name = "is_shuffling")
|
||||
val isShuffling: Boolean,
|
||||
|
||||
@ColumnInfo(name = "shuffle_seed")
|
||||
val shuffleSeed: Long,
|
||||
|
||||
@ColumnInfo(name = "loop_mode")
|
||||
val loopMode: Int,
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package org.oxycblt.auxio.database
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
||||
object QueueConverter {
|
||||
fun fromString(arrayString: String): MutableList<Song> {
|
||||
val jsonArray = JSONArray(arrayString)
|
||||
val queue = mutableListOf<Song>()
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val id = jsonArray.getLong(i)
|
||||
musicStore.songs.find { it.id == id }?.let {
|
||||
queue.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
return queue
|
||||
}
|
||||
|
||||
fun fromQueue(queueIds: List<Long>): String {
|
||||
val jsonArray = JSONArray()
|
||||
queueIds.forEach {
|
||||
jsonArray.put(it)
|
||||
}
|
||||
return jsonArray.toString(0)
|
||||
}
|
||||
}
|
26
app/src/main/java/org/oxycblt/auxio/database/QueueDAO.kt
Normal file
26
app/src/main/java/org/oxycblt/auxio/database/QueueDAO.kt
Normal file
|
@ -0,0 +1,26 @@
|
|||
package org.oxycblt.auxio.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
|
||||
@Dao
|
||||
interface QueueDAO {
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insert(item: QueueItem)
|
||||
|
||||
@Transaction
|
||||
suspend fun insertAll(items: List<QueueItem>) {
|
||||
items.forEach {
|
||||
insert(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM queue_table")
|
||||
fun getAll(): List<QueueItem>
|
||||
|
||||
@Query("DELETE FROM queue_table")
|
||||
fun clear()
|
||||
}
|
17
app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt
Normal file
17
app/src/main/java/org/oxycblt/auxio/database/QueueItem.kt
Normal file
|
@ -0,0 +1,17 @@
|
|||
package org.oxycblt.auxio.database
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "queue_table")
|
||||
data class QueueItem(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0L,
|
||||
|
||||
@ColumnInfo(name = "song_id")
|
||||
val songId: Long = Long.MIN_VALUE,
|
||||
|
||||
@ColumnInfo(name = "is_user_queue")
|
||||
val isUserQueue: Boolean = false
|
||||
)
|
|
@ -44,19 +44,17 @@ class SearchAdapter(
|
|||
)
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||
|
||||
else -> HeaderViewHolder.from(parent.context)
|
||||
else -> error("Someone messed with the ViewHolder item types.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is GenreViewHolder -> holder.bind(getItem(position) as Genre)
|
||||
is ArtistViewHolder -> holder.bind(getItem(position) as Artist)
|
||||
is AlbumViewHolder -> holder.bind(getItem(position) as Album)
|
||||
is SongViewHolder -> holder.bind(getItem(position) as Song)
|
||||
is HeaderViewHolder -> holder.bind(getItem(position) as Header)
|
||||
|
||||
else -> return
|
||||
when (val item = getItem(position)) {
|
||||
is Genre -> (holder as GenreViewHolder).bind(item)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item)
|
||||
is Album -> (holder as AlbumViewHolder).bind(item)
|
||||
is Song -> (holder as SongViewHolder).bind(item)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,11 @@ class CompactPlaybackFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
binding.root.setOnLongClickListener {
|
||||
playbackModel.save(requireContext())
|
||||
true
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
playbackModel.song.observe(viewLifecycleOwner) {
|
||||
|
|
|
@ -145,19 +145,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
|
||||
if (playbackManager.song != null) {
|
||||
restorePlayer()
|
||||
notification.updateLoop(this)
|
||||
notification.updateMode(this)
|
||||
notification.updatePlaying(this)
|
||||
|
||||
playbackManager.song?.let {
|
||||
notification.setMetadata(it, this) {
|
||||
if (playbackManager.isPlaying) {
|
||||
startForegroundOrNotify("Restore")
|
||||
} else {
|
||||
stopForegroundAndNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
restoreNotification()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -275,6 +263,8 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Do testing where service is destroyed after restore [Possible edge case]
|
||||
|
||||
override fun onLoopUpdate(mode: LoopMode) {
|
||||
changeIsFromAudioFocus = false
|
||||
|
||||
|
@ -308,6 +298,22 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
}
|
||||
}
|
||||
|
||||
private fun restoreNotification() {
|
||||
notification.updateLoop(this)
|
||||
notification.updateMode(this)
|
||||
notification.updatePlaying(this)
|
||||
|
||||
playbackManager.song?.let {
|
||||
notification.setMetadata(it, this) {
|
||||
if (playbackManager.isPlaying) {
|
||||
startForegroundOrNotify("Restore")
|
||||
} else {
|
||||
stopForegroundAndNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreFinish() {
|
||||
Log.d(this::class.simpleName, "Restore done")
|
||||
|
||||
|
@ -349,7 +355,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca
|
|||
private fun startForegroundOrNotify(reason: String) {
|
||||
// Start the service in the foreground if haven't already.
|
||||
if (playbackManager.isRestored) {
|
||||
Log.d(this::class.simpleName, "Starting foreground because of $reason")
|
||||
Log.d(this::class.simpleName, "Starting foreground/notifying because of $reason")
|
||||
|
||||
if (!isForeground) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
|
|
|
@ -256,6 +256,12 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
|
|||
}
|
||||
}
|
||||
|
||||
fun save(context: Context) {
|
||||
viewModelScope.launch {
|
||||
playbackManager.saveStateToDatabase(context)
|
||||
}
|
||||
}
|
||||
|
||||
// --- OVERRIDES ---
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -47,7 +47,7 @@ class QueueAdapter(
|
|||
QUEUE_ITEM_TYPE -> ViewHolder(
|
||||
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
|
||||
)
|
||||
else -> error("Someone messed with the ViewHolder item types. Tell OxygenCobalt.")
|
||||
else -> error("Someone messed with the ViewHolder item types.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,7 +57,7 @@ class QueueAdapter(
|
|||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
|
||||
else -> {
|
||||
Log.d(this::class.simpleName, "Bad data fed to QueueAdapter.")
|
||||
Log.e(this::class.simpleName, "Bad data fed to QueueAdapter.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ class QueueAdapter(
|
|||
if (data[data.lastIndex] is Header) {
|
||||
val lastIndex = data.lastIndex
|
||||
|
||||
// TODO: Do notifyItemRangeRemoved instead of notifyItemRemoved
|
||||
data.removeAt(lastIndex)
|
||||
notifyItemRemoved(lastIndex)
|
||||
} else if (data.lastIndex >= 1 && data[0] is Header && data[1] is Header) {
|
||||
|
|
|
@ -2,11 +2,12 @@ package org.oxycblt.auxio.playback.state
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlin.random.Random
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.database.AuxioDatabase
|
||||
import org.oxycblt.auxio.database.PlaybackState
|
||||
import org.oxycblt.auxio.database.PlaybackStateDatabase
|
||||
import org.oxycblt.auxio.database.QueueConverter
|
||||
import org.oxycblt.auxio.database.QueueItem
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
|
@ -14,7 +15,6 @@ import org.oxycblt.auxio.music.Genre
|
|||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import kotlin.random.Random
|
||||
|
||||
/**
|
||||
* Master class for the playback state. This should ***not*** be used outside of the playback module.
|
||||
|
@ -77,7 +77,6 @@ class PlaybackStateManager private constructor() {
|
|||
field = value
|
||||
callbacks.forEach { it.onShuffleUpdate(value) }
|
||||
}
|
||||
private var mShuffleSeed = -1L
|
||||
private var mLoopMode = LoopMode.NONE
|
||||
set(value) {
|
||||
field = value
|
||||
|
@ -85,6 +84,7 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
private var mIsInUserQueue = false
|
||||
private var mIsRestored = false
|
||||
private var mShuffleSeed = -1L
|
||||
|
||||
val song: Song? get() = mSong
|
||||
val parent: BaseModel? get() = mParent
|
||||
|
@ -125,22 +125,27 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
mParent = when (mode) {
|
||||
PlaybackMode.ALL_SONGS -> null
|
||||
PlaybackMode.IN_ARTIST -> song.album.artist
|
||||
PlaybackMode.IN_ALBUM -> song.album
|
||||
PlaybackMode.IN_GENRE -> song.album.artist.genres[0]
|
||||
when (mode) {
|
||||
PlaybackMode.ALL_SONGS -> {
|
||||
mParent = null
|
||||
mQueue = musicStore.songs.toMutableList()
|
||||
}
|
||||
|
||||
PlaybackMode.IN_ARTIST -> {
|
||||
mParent = song.album.artist
|
||||
mQueue = song.album.artist.songs
|
||||
}
|
||||
|
||||
PlaybackMode.IN_ALBUM -> {
|
||||
mParent = song.album
|
||||
mQueue = song.album.songs
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
mMode = mode
|
||||
|
||||
mQueue = when (mode) {
|
||||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||
PlaybackMode.IN_ARTIST -> song.album.artist.songs
|
||||
PlaybackMode.IN_ALBUM -> song.album.songs
|
||||
PlaybackMode.IN_GENRE -> song.album.artist.genres[0].songs
|
||||
}
|
||||
|
||||
resetLoopMode()
|
||||
updatePlayback(song)
|
||||
|
||||
|
@ -351,15 +356,13 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
// Generate a new shuffled queue.
|
||||
private fun genShuffle(keepSong: Boolean) {
|
||||
// Take a random seed and then shuffle the current queue based off of that.
|
||||
// This seed will be saved in a database, so that the shuffle mode
|
||||
// can be restored when its started again.
|
||||
val newSeed = Random.Default.nextLong()
|
||||
|
||||
Log.d(this::class.simpleName, "Shuffling queue with seed $newSeed")
|
||||
|
||||
mShuffleSeed = newSeed
|
||||
|
||||
Log.d(this::class.simpleName, "Shuffling queue with a seed of $mShuffleSeed.")
|
||||
mQueue.shuffle(Random(mShuffleSeed))
|
||||
mQueue.shuffle(Random(newSeed))
|
||||
mIndex = 0
|
||||
|
||||
// If specified, make the current song the first member of the queue.
|
||||
|
@ -377,14 +380,9 @@ class PlaybackStateManager private constructor() {
|
|||
|
||||
// Stop the queue and attempt to restore to the previous state
|
||||
private fun resetShuffle() {
|
||||
mShuffleSeed = -1
|
||||
mShuffleSeed = -1L
|
||||
|
||||
mQueue = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||
PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList()
|
||||
}
|
||||
setupOrderedQueue()
|
||||
|
||||
mIndex = mQueue.indexOf(mSong)
|
||||
|
||||
|
@ -414,14 +412,165 @@ class PlaybackStateManager private constructor() {
|
|||
}
|
||||
|
||||
private fun resetLoopMode() {
|
||||
// Reset the loop mode froM ONCE if needed.
|
||||
// Reset the loop mode from ONCE if needed.
|
||||
if (mLoopMode == LoopMode.ONCE) {
|
||||
mLoopMode = LoopMode.NONE
|
||||
}
|
||||
}
|
||||
|
||||
// --- PERSISTENCE FUNCTIONS ---
|
||||
// TODO: Optimize queue persistence [Storing seed + edits instead of entire queue.
|
||||
|
||||
suspend fun saveStateToDatabase(context: Context) {
|
||||
Log.d(this::class.simpleName, "Saving state to DB.")
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
Log.d(this::class.simpleName, packQueue().size.toString())
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val playbackState = packToPlaybackState()
|
||||
val queueItems = packQueue()
|
||||
|
||||
val database = AuxioDatabase.getInstance(context)
|
||||
database.playbackStateDAO.clear()
|
||||
database.queueDAO.clear()
|
||||
|
||||
database.playbackStateDAO.insert(playbackState)
|
||||
database.queueDAO.insertAll(queueItems)
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - start
|
||||
|
||||
Log.d(this::class.simpleName, "Save finished in ${time}ms")
|
||||
}
|
||||
|
||||
suspend fun getStateFromDatabase(context: Context) {
|
||||
Log.d(this::class.simpleName, "Getting state from DB.")
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
val states: List<PlaybackState>
|
||||
val queueItems: List<QueueItem>
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val database = AuxioDatabase.getInstance(context)
|
||||
states = database.playbackStateDAO.getAll()
|
||||
queueItems = database.queueDAO.getAll()
|
||||
|
||||
database.playbackStateDAO.clear()
|
||||
database.queueDAO.clear()
|
||||
}
|
||||
|
||||
if (states.isEmpty()) {
|
||||
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
|
||||
|
||||
mIsRestored = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, "Old state found, ${states[0]}")
|
||||
|
||||
unpackFromPlaybackState(states[0])
|
||||
|
||||
Log.d(this::class.simpleName, "Found queue of size ${queueItems.size}")
|
||||
|
||||
unpackQueue(queueItems)
|
||||
|
||||
mSong?.let {
|
||||
mIndex = mQueue.indexOf(mSong)
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - start
|
||||
|
||||
Log.d(this::class.simpleName, "Restore finished in ${time}ms")
|
||||
|
||||
mIsRestored = true
|
||||
}
|
||||
|
||||
private fun packToPlaybackState(): PlaybackState {
|
||||
val songId = mSong?.id ?: -1L
|
||||
val parentId = mParent?.id ?: -1L
|
||||
val intMode = mMode.toConstant()
|
||||
val intLoopMode = mLoopMode.toConstant()
|
||||
|
||||
return PlaybackState(
|
||||
songId = songId,
|
||||
position = mPosition,
|
||||
parentId = parentId,
|
||||
mode = intMode,
|
||||
isShuffling = mIsShuffling,
|
||||
loopMode = intLoopMode,
|
||||
inUserQueue = mIsInUserQueue
|
||||
)
|
||||
}
|
||||
|
||||
private fun packQueue(): List<QueueItem> {
|
||||
val unified = mutableListOf<QueueItem>()
|
||||
|
||||
mUserQueue.forEach {
|
||||
unified.add(QueueItem(songId = it.id, isUserQueue = true))
|
||||
}
|
||||
|
||||
mQueue.forEach {
|
||||
unified.add(QueueItem(songId = it.id, isUserQueue = false))
|
||||
}
|
||||
|
||||
return unified
|
||||
}
|
||||
|
||||
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
// Turn the simplified information from PlaybackState into values that can be used
|
||||
mSong = musicStore.songs.find { it.id == playbackState.songId }
|
||||
mPosition = playbackState.position
|
||||
mParent = musicStore.parents.find { it.id == playbackState.parentId }
|
||||
mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||
mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE
|
||||
mIsShuffling = playbackState.isShuffling
|
||||
mIsInUserQueue = playbackState.inUserQueue
|
||||
|
||||
callbacks.forEach {
|
||||
it.onSeekConfirm(mPosition)
|
||||
it.onModeUpdate(mMode)
|
||||
it.onRestoreFinish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun unpackQueue(queueItems: List<QueueItem>) {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
Log.d(this::class.simpleName, queueItems.size.toString())
|
||||
|
||||
for (item in queueItems) {
|
||||
musicStore.songs.find { it.id == item.songId }?.let {
|
||||
Log.d(this::class.simpleName, it.id.toString())
|
||||
|
||||
if (item.isUserQueue) {
|
||||
mUserQueue.add(it)
|
||||
} else {
|
||||
mQueue.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forceQueueUpdate()
|
||||
forceUserQueueUpdate()
|
||||
}
|
||||
|
||||
// --- ORDERING FUNCTIONS ---
|
||||
|
||||
private fun setupOrderedQueue() {
|
||||
mQueue = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||
PlaybackMode.ALL_SONGS -> MusicStore.getInstance().songs.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun orderSongsInAlbum(album: Album): MutableList<Song> {
|
||||
return album.songs.sortedBy { it.track }.toMutableList()
|
||||
}
|
||||
|
@ -450,138 +599,6 @@ class PlaybackStateManager private constructor() {
|
|||
return final
|
||||
}
|
||||
|
||||
// --- PERSISTENCE FUNCTIONS ---
|
||||
// TODO: Persist queue edits?
|
||||
// FIXME: Shuffling w/o knowing the original queue edit from keepSong will cause issues
|
||||
|
||||
suspend fun saveStateToDatabase(context: Context) {
|
||||
Log.d(this::class.simpleName, "Saving state to DB.")
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val playbackState = packToPlaybackState()
|
||||
|
||||
val database = PlaybackStateDatabase.getInstance(context)
|
||||
database.playbackStateDAO.clear()
|
||||
database.playbackStateDAO.insert(playbackState)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getStateFromDatabase(context: Context) {
|
||||
Log.d(this::class.simpleName, "Getting state from DB.")
|
||||
|
||||
val states = withContext(Dispatchers.IO) {
|
||||
val database = PlaybackStateDatabase.getInstance(context)
|
||||
val states = database.playbackStateDAO.getAll()
|
||||
|
||||
database.playbackStateDAO.clear()
|
||||
|
||||
return@withContext states
|
||||
}
|
||||
|
||||
if (states.isEmpty()) {
|
||||
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
|
||||
|
||||
mIsRestored = true
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, "Old state found, ${states[0]}")
|
||||
|
||||
unpackFromPlaybackState(states[0])
|
||||
|
||||
mIsRestored = true
|
||||
}
|
||||
|
||||
private fun packToPlaybackState(): PlaybackState {
|
||||
val songId = mSong?.id ?: -1L
|
||||
val parentId = mParent?.id ?: -1L
|
||||
val userQueueString = QueueConverter.fromQueue(
|
||||
mutableListOf<Long>().apply {
|
||||
mUserQueue.forEach {
|
||||
this.add(it.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
val intMode = mMode.toConstant()
|
||||
val intLoopMode = mLoopMode.toConstant()
|
||||
|
||||
return PlaybackState(
|
||||
songId = songId,
|
||||
position = mPosition,
|
||||
parentId = parentId,
|
||||
userQueueIds = userQueueString,
|
||||
index = mIndex,
|
||||
mode = intMode,
|
||||
isShuffling = mIsShuffling,
|
||||
shuffleSeed = mShuffleSeed,
|
||||
loopMode = intLoopMode,
|
||||
inUserQueue = mIsInUserQueue
|
||||
)
|
||||
}
|
||||
|
||||
private fun unpackFromPlaybackState(playbackState: PlaybackState) {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
// Turn the simplified information from PlaybackState into values that can be used
|
||||
mSong = musicStore.songs.find { it.id == playbackState.songId }
|
||||
mPosition = playbackState.position
|
||||
mParent = musicStore.parents.find { it.id == playbackState.parentId }
|
||||
mUserQueue = QueueConverter.fromString(playbackState.userQueueIds)
|
||||
mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||
mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE
|
||||
mIsShuffling = playbackState.isShuffling
|
||||
mShuffleSeed = playbackState.shuffleSeed
|
||||
mIsInUserQueue = playbackState.inUserQueue
|
||||
|
||||
// If the parent was somehow dropped during saving, attempt to restore it.
|
||||
mSong?.let {
|
||||
if (mParent == null && mMode != PlaybackMode.ALL_SONGS) {
|
||||
Log.d(
|
||||
this::class.simpleName,
|
||||
"Parent was corrupted while in mode $mMode. Attempting to restore."
|
||||
)
|
||||
mParent = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> it.album.artist
|
||||
PlaybackMode.IN_ALBUM -> it.album
|
||||
else -> {
|
||||
// If that fails, then just put the mode into all songs.
|
||||
mMode = PlaybackMode.ALL_SONGS
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mIsShuffling) {
|
||||
mQueue = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> (mParent as Artist).songs
|
||||
PlaybackMode.IN_ALBUM -> (mParent as Album).songs
|
||||
PlaybackMode.IN_GENRE -> (mParent as Genre).songs
|
||||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||
}
|
||||
|
||||
mQueue.shuffle(Random(mShuffleSeed))
|
||||
|
||||
forceQueueUpdate()
|
||||
} else {
|
||||
mQueue = when (mMode) {
|
||||
PlaybackMode.IN_ARTIST -> orderSongsInArtist(mParent as Artist)
|
||||
PlaybackMode.IN_ALBUM -> orderSongsInAlbum(mParent as Album)
|
||||
PlaybackMode.IN_GENRE -> orderSongsInGenre(mParent as Genre)
|
||||
PlaybackMode.ALL_SONGS -> musicStore.songs.toMutableList()
|
||||
}
|
||||
}
|
||||
|
||||
mIndex = playbackState.index
|
||||
|
||||
callbacks.forEach {
|
||||
it.onSeekConfirm(mPosition)
|
||||
it.onModeUpdate(mMode)
|
||||
it.onRestoreFinish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for receiving updates from [PlaybackStateManager].
|
||||
* Add the callback to [PlaybackStateManager] using [addCallback],
|
||||
|
|
Loading…
Reference in a new issue