playback: improve persistence

Improve playback persistence in the following ways:
1. Shift the boundary of PlaybackStateManager and PlaybackStateDatabase
so that the reading and searching phases both occur at the same time,
which is more efficient.
2. Improve music hashing so that conflicts are minimized [this also
helps the future playlists addition]
3. Generally improve code style
This commit is contained in:
OxygenCobalt 2021-11-04 06:58:43 -06:00
parent 10c45f1492
commit 8b8d36cf22
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
16 changed files with 250 additions and 289 deletions

View file

@ -32,11 +32,10 @@ import coil.fetch.Fetcher
import coil.fetch.SourceResult import coil.fetch.SourceResult
import coil.size.OriginalSize import coil.size.OriginalSize
import coil.size.Size import coil.size.Size
import okio.source
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.MusicParent
import java.io.Closeable import java.io.Closeable
import java.lang.Exception import java.lang.Exception
@ -44,10 +43,10 @@ import java.lang.Exception
* A [Fetcher] that takes an [Artist] or [Genre] and returns a mosaic of its albums. * A [Fetcher] that takes an [Artist] or [Genre] and returns a mosaic of its albums.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class MosaicFetcher(private val context: Context) : Fetcher<Parent> { class MosaicFetcher(private val context: Context) : Fetcher<MusicParent> {
override suspend fun fetch( override suspend fun fetch(
pool: BitmapPool, pool: BitmapPool,
data: Parent, data: MusicParent,
size: Size, size: Size,
options: Options options: Options
): FetchResult { ): FetchResult {
@ -147,8 +146,8 @@ class MosaicFetcher(private val context: Context) : Fetcher<Parent> {
forEach { it.use(block) } forEach { it.use(block) }
} }
override fun key(data: Parent): String = data.hashCode().toString() override fun key(data: MusicParent): String = data.hashCode().toString()
override fun handles(data: Parent) = data !is Album // Albums are not used here override fun handles(data: MusicParent) = data !is Album // Albums are not used here
companion object { companion object {
private const val MOSAIC_BITMAP_SIZE = 512 private const val MOSAIC_BITMAP_SIZE = 512

View file

@ -137,7 +137,7 @@ class AlbumDetailFragment : DetailFragment() {
// --- PLAYBACKVIEWMODEL SETUP --- // --- PLAYBACKVIEWMODEL SETUP ---
playbackModel.song.observe(viewLifecycleOwner) { song -> playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_ALBUM && if (playbackModel.playbackMode.value == PlaybackMode.IN_ALBUM &&
playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id playbackModel.parent.value?.id == detailModel.curAlbum.value!!.id
) { ) {
detailAdapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)

View file

@ -131,7 +131,7 @@ class ArtistDetailFragment : DetailFragment() {
// Highlight songs if they are being played // Highlight songs if they are being played
playbackModel.song.observe(viewLifecycleOwner) { song -> playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_ARTIST && if (playbackModel.playbackMode.value == PlaybackMode.IN_ARTIST &&
playbackModel.parent.value?.id == detailModel.curArtist.value?.id playbackModel.parent.value?.id == detailModel.curArtist.value?.id
) { ) {
detailAdapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)

View file

@ -99,7 +99,7 @@ class GenreDetailFragment : DetailFragment() {
// --- PLAYBACKVIEWMODEL SETUP --- // --- PLAYBACKVIEWMODEL SETUP ---
playbackModel.song.observe(viewLifecycleOwner) { song -> playbackModel.song.observe(viewLifecycleOwner) { song ->
if (playbackModel.mode.value == PlaybackMode.IN_GENRE && if (playbackModel.playbackMode.value == PlaybackMode.IN_GENRE &&
playbackModel.parent.value?.id == detailModel.curGenre.value!!.id playbackModel.parent.value?.id == detailModel.curGenre.value!!.id
) { ) {
detailAdapter.highlightSong(song, binding.detailRecycler) detailAdapter.highlightSong(song, binding.detailRecycler)

View file

@ -33,16 +33,23 @@ sealed class BaseModel {
abstract val id: Long abstract val id: Long
} }
/**
* A [BaseModel] variant that represents a music item.
* @property name The raw name of this track
* @property hash A stable, unique-ish hash for this item. Used for database work.
*/
sealed class Music : BaseModel() { sealed class Music : BaseModel() {
abstract val name: String abstract val name: String
abstract val hash: Int abstract val hash: Long
} }
/** /**
* [BaseModel] variant that denotes that this object is a parent of other data objects, such * [BaseModel] variant that denotes that this object is a parent of other data objects, such
* as an [Album] or [Artist] * as an [Album] or [Artist]
* @property resolvedName A name resolved from it's raw form to a form suitable to be shown in
* a ui. Ex. unknown would become Unknown Artist, (124) would become its proper genre name, etc.
*/ */
sealed class Parent : Music() { sealed class MusicParent : Music() {
abstract val resolvedName: String abstract val resolvedName: String
} }
@ -79,8 +86,10 @@ data class Song(
val seconds: Long get() = duration / 1000 val seconds: Long get() = duration / 1000
val formattedDuration: String get() = (duration / 1000).toDuration() val formattedDuration: String get() = (duration / 1000).toDuration()
override val hash: Int get() { override val hash: Long get() {
var result = name.hashCode() var result = name.hashCode().toLong()
result = 31 * result + albumName.hashCode()
result = 31 * result + artistName.hashCode()
result = 31 * result + track result = 31 * result + track
result = 31 * result + duration.hashCode() result = 31 * result + duration.hashCode()
return result return result
@ -96,7 +105,7 @@ data class Song(
} }
/** /**
* The data object for an album. Inherits [Parent]. * The data object for an album. Inherits [MusicParent].
* @property artistName The name of the parent artist. Do not use this outside of creating the artist from albums * @property artistName The name of the parent artist. Do not use this outside of creating the artist from albums
* @property year The year this album was released. 0 if there is none in the metadata. * @property year The year this album was released. 0 if there is none in the metadata.
* @property artist The Album's parent [Artist]. use this instead of [artistName] * @property artist The Album's parent [Artist]. use this instead of [artistName]
@ -109,7 +118,7 @@ data class Album(
val artistName: String, val artistName: String,
val year: Int, val year: Int,
val songs: List<Song> val songs: List<Song>
) : Parent() { ) : MusicParent() {
init { init {
songs.forEach { song -> songs.forEach { song ->
song.linkAlbum(this) song.linkAlbum(this)
@ -126,8 +135,8 @@ data class Album(
mArtist = artist mArtist = artist
} }
override val hash: Int get() { override val hash: Long get() {
var result = name.hashCode() var result = name.hashCode().toLong()
result = 31 * result + artistName.hashCode() result = 31 * result + artistName.hashCode()
result = 31 * result + year result = 31 * result + year
return result return result
@ -138,7 +147,7 @@ data class Album(
} }
/** /**
* The data object for an artist. Inherits [Parent] * The data object for an artist. Inherits [MusicParent]
* @property albums The list of all [Album]s in this artist * @property albums The list of all [Album]s in this artist
* @property genre The most prominent genre for this artist * @property genre The most prominent genre for this artist
* @property songs The list of all [Song]s in this artist * @property songs The list of all [Song]s in this artist
@ -148,7 +157,7 @@ data class Artist(
override val name: String, override val name: String,
override val resolvedName: String, override val resolvedName: String,
val albums: List<Album> val albums: List<Album>
) : Parent() { ) : MusicParent() {
init { init {
albums.forEach { album -> albums.forEach { album ->
album.linkArtist(this) album.linkArtist(this)
@ -165,18 +174,18 @@ data class Artist(
albums.flatMap { it.songs } albums.flatMap { it.songs }
} }
override val hash = name.hashCode() override val hash = name.hashCode().toLong()
} }
/** /**
* The data object for a genre. Inherits [Parent] * The data object for a genre. Inherits [MusicParent]
* @property songs The list of all [Song]s in this genre. * @property songs The list of all [Song]s in this genre.
*/ */
data class Genre( data class Genre(
override val id: Long, override val id: Long,
override val name: String, override val name: String,
override val resolvedName: String override val resolvedName: String
) : Parent() { ) : MusicParent() {
private val mSongs = mutableListOf<Song>() private val mSongs = mutableListOf<Song>()
val songs: List<Song> get() = mSongs val songs: List<Song> get() = mSongs
@ -188,13 +197,14 @@ data class Genre(
song.linkGenre(this) song.linkGenre(this)
} }
override val hash = name.hashCode() override val hash = name.hashCode().toLong()
} }
/** /**
* The string used for a header instance. This class is a bit complex, mostly because it revolves * The string used for a header instance. This class is a bit complex, mostly because it revolves
* around passing string resources that are then resolved by the view instead of passing a context * around passing string resources that are then resolved by the view instead of passing a context
* directly. * directly.
* @author OxygenCobalt
*/ */
sealed class HeaderString { sealed class HeaderString {
/** A single string resource. */ /** A single string resource. */

View file

@ -92,7 +92,7 @@ class MusicStore private constructor() {
/** /**
* Find a song in a faster manner using a hash for its album as well. * Find a song in a faster manner using a hash for its album as well.
*/ */
fun findSongFast(songHash: Int, albumHash: Int): Song? { fun findSongFast(songHash: Long, albumHash: Long): Song? {
return albums.find { it.hash == albumHash }?.songs?.find { it.hash == songHash } return albums.find { it.hash == albumHash }?.songs?.find { it.hash == songHash }
} }

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
/** /**
* A complete array of all the hardcoded genre values for ID3 <v3, contains standard genres and * A complete array of all the hardcoded genre values for ID3(v2), contains standard genres and
* winamp extensions. * winamp extensions.
*/ */
private val ID3_GENRES = arrayOf( private val ID3_GENRES = arrayOf(

View file

@ -38,6 +38,9 @@ import org.oxycblt.auxio.util.systemBarsCompat
* properly. The mechanism is mostly inspired by Material Files' PersistentBarLayout, however * properly. The mechanism is mostly inspired by Material Files' PersistentBarLayout, however
* this class was primarily written by me and I plan to expand this layout to become part of * this class was primarily written by me and I plan to expand this layout to become part of
* the playback navigation process. * the playback navigation process.
*
* TODO: Explain how this thing works so that others can be spared the pain of deciphering
* this custom viewgroup
*/ */
class PlaybackBarLayout @JvmOverloads constructor( class PlaybackBarLayout @JvmOverloads constructor(
context: Context, context: Context,

View file

@ -35,8 +35,8 @@ import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Header import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.HeaderString import org.oxycblt.auxio.music.HeaderString
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.QueueAdapter import org.oxycblt.auxio.playback.queue.QueueAdapter
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
@ -56,7 +56,7 @@ import org.oxycblt.auxio.util.logE
class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback { class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
// Playback // Playback
private val mSong = MutableLiveData<Song?>() private val mSong = MutableLiveData<Song?>()
private val mParent = MutableLiveData<Parent?>() private val mParent = MutableLiveData<MusicParent?>()
private val mPosition = MutableLiveData(0L) private val mPosition = MutableLiveData(0L)
// Queue // Queue
@ -77,16 +77,16 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
/** The current song. */ /** The current song. */
val song: LiveData<Song?> get() = mSong val song: LiveData<Song?> get() = mSong
/** The current model that is being played from, such as an [Album] or [Artist] */ /** The current model that is being played from, such as an [Album] or [Artist] */
val parent: LiveData<Parent?> get() = mParent val parent: LiveData<MusicParent?> get() = mParent
/** The current playback position, in seconds */ /** The current playback position, in seconds */
val position: LiveData<Long> get() = mPosition val position: LiveData<Long> get() = mPosition
/** The current queue determined by [mode] and [parent] */ /** The current queue determined by [playbackMode] and [parent] */
val queue: LiveData<List<Song>> get() = mQueue val queue: LiveData<List<Song>> get() = mQueue
/** The queue created by the user. */ /** The queue created by the user. */
val userQueue: LiveData<List<Song>> get() = mUserQueue val userQueue: LiveData<List<Song>> get() = mUserQueue
/** The current [PlaybackMode] that also determines the queue */ /** The current [PlaybackMode] that also determines the queue */
val mode: LiveData<PlaybackMode> get() = mMode val playbackMode: LiveData<PlaybackMode> get() = mMode
/** Whether playback is originating from the user-generated queue or not */ /** Whether playback is originating from the user-generated queue or not */
val isInUserQueue: LiveData<Boolean> = mIsInUserQueue val isInUserQueue: LiveData<Boolean> = mIsInUserQueue
@ -156,11 +156,6 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
} }
} }
/** The position as SeekBar progress. */
val positionAsProgress = Transformations.map(mPosition) {
if (mSong.value != null) it.toInt() else 0
}
private val playbackManager = PlaybackStateManager.maybeGetInstance() private val playbackManager = PlaybackStateManager.maybeGetInstance()
private val settingsManager = SettingsManager.getInstance() private val settingsManager = SettingsManager.getInstance()
@ -449,7 +444,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mPosition.value = playbackManager.position / 1000 mPosition.value = playbackManager.position / 1000
mParent.value = playbackManager.parent mParent.value = playbackManager.parent
mQueue.value = playbackManager.queue mQueue.value = playbackManager.queue
mMode.value = playbackManager.mode mMode.value = playbackManager.playbackMode
mUserQueue.value = playbackManager.userQueue mUserQueue.value = playbackManager.userQueue
mIndex.value = playbackManager.index mIndex.value = playbackManager.index
mIsPlaying.value = playbackManager.isPlaying mIsPlaying.value = playbackManager.isPlaying
@ -467,7 +462,7 @@ class PlaybackViewModel : ViewModel(), PlaybackStateManager.Callback {
mSong.value = song mSong.value = song
} }
override fun onParentUpdate(parent: Parent?) { override fun onParentUpdate(parent: MusicParent?) {
mParent.value = parent mParent.value = parent
} }

View file

@ -24,27 +24,27 @@ package org.oxycblt.auxio.playback.state
*/ */
enum class PlaybackMode { enum class PlaybackMode {
/** Construct the queue from the genre's songs */ /** Construct the queue from the genre's songs */
IN_GENRE, ALL_SONGS,
/** Construct the queue from the artist's songs */ /** Construct the queue from the artist's songs */
IN_ARTIST,
/** Construct the queue from the album's songs */
IN_ALBUM, IN_ALBUM,
/** Construct the queue from the album's songs */
IN_ARTIST,
/** Construct the queue from all songs */ /** Construct the queue from all songs */
ALL_SONGS; IN_GENRE;
/** /**
* Convert the mode into an int constant, to be saved in PlaybackStateDatabase * Convert the mode into an int constant, to be saved in PlaybackStateDatabase
* @return The constant for this mode, * @return The constant for this mode,
*/ */
fun toInt(): Int { fun toInt(): Int {
return CONST_IN_ARTIST + ordinal return CONST_ALL_SONGS + ordinal
} }
companion object { companion object {
private const val CONST_IN_GENRE = 0xA103 private const val CONST_ALL_SONGS = 0xA103
private const val CONST_IN_ARTIST = 0xA104 private const val CONST_IN_ALBUM = 0xA104
private const val CONST_IN_ALBUM = 0xA105 private const val CONST_IN_ARTIST = 0xA105
private const val CONST_ALL_SONGS = 0xA106 private const val CONST_IN_GENRE = 0xA106
/** /**
* Get a [PlaybackMode] for an int [constant] * Get a [PlaybackMode] for an int [constant]
@ -52,11 +52,10 @@ enum class PlaybackMode {
*/ */
fun fromInt(constant: Int): PlaybackMode? { fun fromInt(constant: Int): PlaybackMode? {
return when (constant) { return when (constant) {
CONST_IN_ARTIST -> IN_ARTIST
CONST_IN_ALBUM -> IN_ALBUM
CONST_IN_GENRE -> IN_GENRE
CONST_ALL_SONGS -> ALL_SONGS CONST_ALL_SONGS -> ALL_SONGS
CONST_IN_ALBUM -> IN_ALBUM
CONST_IN_ARTIST -> IN_ARTIST
CONST_IN_GENRE -> IN_GENRE
else -> null else -> null
} }
} }

View file

@ -22,7 +22,11 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import androidx.core.database.getLongOrNull
import androidx.core.database.sqlite.transaction import androidx.core.database.sqlite.transaction
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.assertBackgroundThread import org.oxycblt.auxio.util.assertBackgroundThread
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.queryAll import org.oxycblt.auxio.util.queryAll
@ -30,8 +34,7 @@ import org.oxycblt.auxio.util.queryAll
/** /**
* A SQLite database for managing the persistent playback state and queue. * A SQLite database for managing the persistent playback state and queue.
* Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs. * Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
* TODO: Improve the boundary between this and [PlaybackStateManager]. This would be more * LEFT-OFF: Improve hashing by making everything a long
* efficient.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackStateDatabase(context: Context) : class PlaybackStateDatabase(context: Context) :
@ -74,30 +77,30 @@ class PlaybackStateDatabase(context: Context) :
} }
/** /**
* Construct a [DatabaseState] table * Construct a [StateColumns] table
*/ */
private fun constructStateTable(command: StringBuilder): StringBuilder { private fun constructStateTable(command: StringBuilder): StringBuilder {
command.append("${DatabaseState.COLUMN_ID} LONG PRIMARY KEY,") command.append("${StateColumns.COLUMN_ID} LONG PRIMARY KEY,")
.append("${DatabaseState.COLUMN_SONG_HASH} INTEGER NOT NULL,") .append("${StateColumns.COLUMN_SONG_HASH} LONG,")
.append("${DatabaseState.COLUMN_POSITION} LONG NOT NULL,") .append("${StateColumns.COLUMN_POSITION} LONG NOT NULL,")
.append("${DatabaseState.COLUMN_PARENT_HASH} INTEGER NOT NULL,") .append("${StateColumns.COLUMN_PARENT_HASH} LONG,")
.append("${DatabaseState.COLUMN_INDEX} INTEGER NOT NULL,") .append("${StateColumns.COLUMN_QUEUE_INDEX} INTEGER NOT NULL,")
.append("${DatabaseState.COLUMN_MODE} INTEGER NOT NULL,") .append("${StateColumns.COLUMN_PLAYBACK_MODE} INTEGER NOT NULL,")
.append("${DatabaseState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,") .append("${StateColumns.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,")
.append("${DatabaseState.COLUMN_LOOP_MODE} INTEGER NOT NULL,") .append("${StateColumns.COLUMN_LOOP_MODE} INTEGER NOT NULL,")
.append("${DatabaseState.COLUMN_IN_USER_QUEUE} BOOLEAN NOT NULL)") .append("${StateColumns.COLUMN_IS_IN_USER_QUEUE} BOOLEAN NOT NULL)")
return command return command
} }
/** /**
* Construct a [DatabaseQueueItem] table * Construct a [QueueColumns] table
*/ */
private fun constructQueueTable(command: StringBuilder): StringBuilder { private fun constructQueueTable(command: StringBuilder): StringBuilder {
command.append("${DatabaseQueueItem.COLUMN_ID} LONG PRIMARY KEY,") command.append("${QueueColumns.ID} LONG PRIMARY KEY,")
.append("${DatabaseQueueItem.COLUMN_SONG_HASH} INTEGER NOT NULL,") .append("${QueueColumns.SONG_HASH} INTEGER NOT NULL,")
.append("${DatabaseQueueItem.COLUMN_ALBUM_HASH} INTEGER NOT NULL,") .append("${QueueColumns.ALBUM_HASH} INTEGER NOT NULL,")
.append("${DatabaseQueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)") .append("${QueueColumns.IS_USER_QUEUE} BOOLEAN NOT NULL)")
return command return command
} }
@ -105,9 +108,9 @@ class PlaybackStateDatabase(context: Context) :
// --- INTERFACE FUNCTIONS --- // --- INTERFACE FUNCTIONS ---
/** /**
* Clear the previously written [DatabaseState] and write a new one. * Clear the previously written [SavedState] and write a new one.
*/ */
fun writeState(state: DatabaseState) { fun writeState(state: SavedState) {
assertBackgroundThread() assertBackgroundThread()
writableDatabase.transaction { writableDatabase.transaction {
@ -116,15 +119,15 @@ class PlaybackStateDatabase(context: Context) :
this@PlaybackStateDatabase.logD("Wiped state db.") this@PlaybackStateDatabase.logD("Wiped state db.")
val stateData = ContentValues(10).apply { val stateData = ContentValues(10).apply {
put(DatabaseState.COLUMN_ID, state.id) put(StateColumns.COLUMN_ID, 0)
put(DatabaseState.COLUMN_SONG_HASH, state.songHash) put(StateColumns.COLUMN_SONG_HASH, state.song?.hash)
put(DatabaseState.COLUMN_POSITION, state.position) put(StateColumns.COLUMN_POSITION, state.position)
put(DatabaseState.COLUMN_PARENT_HASH, state.parentHash) put(StateColumns.COLUMN_PARENT_HASH, state.parent?.hash)
put(DatabaseState.COLUMN_INDEX, state.index) put(StateColumns.COLUMN_QUEUE_INDEX, state.queueIndex)
put(DatabaseState.COLUMN_MODE, state.mode) put(StateColumns.COLUMN_PLAYBACK_MODE, state.playbackMode.toInt())
put(DatabaseState.COLUMN_IS_SHUFFLING, state.isShuffling) put(StateColumns.COLUMN_IS_SHUFFLING, state.isShuffling)
put(DatabaseState.COLUMN_LOOP_MODE, state.loopMode) put(StateColumns.COLUMN_LOOP_MODE, state.loopMode.toInt())
put(DatabaseState.COLUMN_IN_USER_QUEUE, state.inUserQueue) put(StateColumns.COLUMN_IS_IN_USER_QUEUE, state.isInUserQueue)
} }
insert(TABLE_NAME_STATE, null, stateData) insert(TABLE_NAME_STATE, null, stateData)
@ -134,39 +137,55 @@ class PlaybackStateDatabase(context: Context) :
} }
/** /**
* Read the stored [DatabaseState] from the database, if there is one. * Read the stored [SavedState] from the database, if there is one.
* @return The stored [DatabaseState], null if there isn't one. * @param musicStore Required to transform database songs/parents into actual instances
* @return The stored [SavedState], null if there isn't one.
*/ */
fun readState(): DatabaseState? { fun readState(musicStore: MusicStore): SavedState? {
assertBackgroundThread() assertBackgroundThread()
var state: DatabaseState? = null var state: SavedState? = null
readableDatabase.queryAll(TABLE_NAME_STATE) { cursor -> readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
if (cursor.count == 0) return@queryAll if (cursor.count == 0) return@queryAll
val songIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_SONG_HASH) val songIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_SONG_HASH)
val posIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_POSITION) val posIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_POSITION)
val parentIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_PARENT_HASH) val parentIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PARENT_HASH)
val indexIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_INDEX) val indexIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_QUEUE_INDEX)
val modeIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_MODE) val modeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_PLAYBACK_MODE)
val shuffleIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_IS_SHUFFLING) val shuffleIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_IS_SHUFFLING)
val loopModeIndex = cursor.getColumnIndexOrThrow(DatabaseState.COLUMN_LOOP_MODE) val loopModeIndex = cursor.getColumnIndexOrThrow(StateColumns.COLUMN_LOOP_MODE)
val inUserQueueIndex = cursor.getColumnIndexOrThrow( val isInUserQueueIndex = cursor.getColumnIndexOrThrow(
DatabaseState.COLUMN_IN_USER_QUEUE StateColumns.COLUMN_IS_IN_USER_QUEUE
) )
cursor.moveToFirst() cursor.moveToFirst()
state = DatabaseState( val song = cursor.getLongOrNull(songIndex)?.let { hash ->
songHash = cursor.getInt(songIndex), musicStore.songs.find { it.hash == hash }
}
val mode = PlaybackMode.fromInt(cursor.getInt(modeIndex)) ?: PlaybackMode.ALL_SONGS
val parent = cursor.getLongOrNull(parentIndex)?.let { hash ->
when (mode) {
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.hash == hash }
PlaybackMode.ALL_SONGS -> null
}
}
state = SavedState(
song = song,
position = cursor.getLong(posIndex), position = cursor.getLong(posIndex),
parentHash = cursor.getInt(parentIndex), parent = parent,
index = cursor.getInt(indexIndex), queueIndex = cursor.getInt(indexIndex),
mode = cursor.getInt(modeIndex), playbackMode = mode,
isShuffling = cursor.getInt(shuffleIndex) == 1, isShuffling = cursor.getInt(shuffleIndex) == 1,
loopMode = cursor.getInt(loopModeIndex), loopMode = LoopMode.fromInt(cursor.getInt(loopModeIndex)) ?: LoopMode.NONE,
inUserQueue = cursor.getInt(inUserQueueIndex) == 1 isInUserQueue = cursor.getInt(isInUserQueueIndex) == 1
) )
} }
@ -174,9 +193,9 @@ class PlaybackStateDatabase(context: Context) :
} }
/** /**
* Write a list of [queueItems] to the database, clearing the previous queue present. * Write a [SavedQueue] to the database.
*/ */
fun writeQueue(queueItems: List<DatabaseQueueItem>) { fun writeQueue(queue: SavedQueue) {
assertBackgroundThread() assertBackgroundThread()
val database = writableDatabase val database = writableDatabase
@ -187,22 +206,29 @@ class PlaybackStateDatabase(context: Context) :
logD("Wiped queue db.") logD("Wiped queue db.")
writeQueueBatch(queue.user, true, 0)
writeQueueBatch(queue.queue, false, queue.user.size)
}
private fun writeQueueBatch(queue: List<Song>, isUserQueue: Boolean, idStart: Int) {
logD("Beginning queue write [start: $idStart, userQueue: $isUserQueue]")
val database = writableDatabase
var position = 0 var position = 0
// Try to write out the entirety of the queue. while (position < queue.size) {
while (position < queueItems.size) {
var i = position var i = position
database.transaction { database.transaction {
while (i < queueItems.size) { while (i < queue.size) {
val item = queueItems[i] val song = queue[i]
i++ i++
val itemData = ContentValues(4).apply { val itemData = ContentValues(4).apply {
put(DatabaseQueueItem.COLUMN_ID, item.id) put(QueueColumns.ID, idStart + i)
put(DatabaseQueueItem.COLUMN_SONG_HASH, item.songHash) put(QueueColumns.SONG_HASH, song.hash)
put(DatabaseQueueItem.COLUMN_ALBUM_HASH, item.albumHash) put(QueueColumns.ALBUM_HASH, song.album.hash)
put(DatabaseQueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue) put(QueueColumns.IS_USER_QUEUE, isUserQueue)
} }
insert(TABLE_NAME_QUEUE, null, itemData) insert(TABLE_NAME_QUEUE, null, itemData)
@ -218,38 +244,70 @@ class PlaybackStateDatabase(context: Context) :
} }
/** /**
* Read the database for any [DatabaseQueueItem]s. * Read a [SavedQueue] from this database.
* @return A list of any stored [DatabaseQueueItem]s. * @param musicStore Required to transform database songs into actual song instances
*/ */
fun readQueue(): List<DatabaseQueueItem> { fun readQueue(musicStore: MusicStore): SavedQueue {
assertBackgroundThread() assertBackgroundThread()
val queueItems = mutableListOf<DatabaseQueueItem>() val queue = SavedQueue(mutableListOf(), mutableListOf())
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor -> readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
if (cursor.count == 0) return@queryAll if (cursor.count == 0) return@queryAll
val idIndex = cursor.getColumnIndexOrThrow(DatabaseQueueItem.COLUMN_ID) val songIndex = cursor.getColumnIndexOrThrow(QueueColumns.SONG_HASH)
val songIdIndex = cursor.getColumnIndexOrThrow(DatabaseQueueItem.COLUMN_SONG_HASH) val albumIndex = cursor.getColumnIndexOrThrow(QueueColumns.ALBUM_HASH)
val albumIdIndex = cursor.getColumnIndexOrThrow(DatabaseQueueItem.COLUMN_ALBUM_HASH) val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueColumns.IS_USER_QUEUE)
val isUserQueueIndex = cursor.getColumnIndexOrThrow(DatabaseQueueItem.COLUMN_IS_USER_QUEUE)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
queueItems += DatabaseQueueItem( musicStore.findSongFast(cursor.getLong(songIndex), cursor.getLong(albumIndex))?.let { song ->
id = cursor.getLong(idIndex), if (cursor.getInt(isUserQueueIndex) == 1) {
songHash = cursor.getInt(songIdIndex), queue.user.add(song)
albumHash = cursor.getInt(albumIdIndex), } else {
isUserQueue = cursor.getInt(isUserQueueIndex) == 1 queue.queue.add(song)
) }
}
} }
} }
return queueItems return queue
}
data class SavedState(
val song: Song?,
val position: Long,
val parent: MusicParent?,
val queueIndex: Int,
val playbackMode: PlaybackMode,
val isShuffling: Boolean,
val loopMode: LoopMode,
val isInUserQueue: Boolean
)
data class SavedQueue(val user: MutableList<Song>, val queue: MutableList<Song>)
private object StateColumns {
const val COLUMN_ID = "id"
const val COLUMN_SONG_HASH = "song"
const val COLUMN_POSITION = "position"
const val COLUMN_PARENT_HASH = "parent"
const val COLUMN_QUEUE_INDEX = "queue_index"
const val COLUMN_PLAYBACK_MODE = "playback_mode"
const val COLUMN_IS_SHUFFLING = "is_shuffling"
const val COLUMN_LOOP_MODE = "loop_mode"
const val COLUMN_IS_IN_USER_QUEUE = "is_in_user_queue"
}
private object QueueColumns {
const val ID = "id"
const val SONG_HASH = "song"
const val ALBUM_HASH = "album"
const val IS_USER_QUEUE = "is_user_queue"
} }
companion object { companion object {
const val DB_NAME = "auxio_state_database.db" const val DB_NAME = "auxio_state_database.db"
const val DB_VERSION = 4 const val DB_VERSION = 5
const val TABLE_NAME_STATE = "playback_state_table" const val TABLE_NAME_STATE = "playback_state_table"
const val TABLE_NAME_QUEUE = "queue_table" const val TABLE_NAME_QUEUE = "queue_table"
@ -275,61 +333,3 @@ class PlaybackStateDatabase(context: Context) :
} }
} }
} }
/**
* A database entity that stores a simplified representation of a song in a queue.
* @property id The database entity's id
* @property songHash The hash for the song represented
* @property albumHash The hash for the album represented
* @property isUserQueue A bool for if this queue item is a user queue item or not
* @author OxygenCobalt
*/
data class DatabaseQueueItem(
var id: Long = 0L,
val songHash: Int,
val albumHash: Int,
val isUserQueue: Boolean = false
) {
companion object {
const val COLUMN_ID = "id"
const val COLUMN_SONG_HASH = "song"
const val COLUMN_ALBUM_HASH = "album"
const val COLUMN_IS_USER_QUEUE = "is_user_queue"
}
}
/**
* A database entity that stores a compressed variant of the current playback state.
* @property id - The database key for this state
* @property songHash - The hash for the currently playing song
* @property parentHash - The hash for the currently playing parent
* @property index - The current index in the queue.
* @property mode - The integer form of the current [org.oxycblt.auxio.playback.state.PlaybackMode]
* @property isShuffling - A bool for if the queue was shuffled
* @property loopMode - The integer form of the current [org.oxycblt.auxio.playback.state.LoopMode]
* @property inUserQueue - A bool for if the state was currently playing from the user queue.
* @author OxygenCobalt
*/
data class DatabaseState(
val id: Long = 0L,
val songHash: Int,
val position: Long,
val parentHash: Int,
val index: Int,
val mode: Int,
val isShuffling: Boolean,
val loopMode: Int,
val inUserQueue: Boolean
) {
companion object {
const val COLUMN_ID = "state_id"
const val COLUMN_SONG_HASH = "song"
const val COLUMN_POSITION = "position"
const val COLUMN_PARENT_HASH = "parent"
const val COLUMN_INDEX = "_index"
const val COLUMN_MODE = "mode"
const val COLUMN_IS_SHUFFLING = "is_shuffling"
const val COLUMN_LOOP_MODE = "loop_mode"
const val COLUMN_IN_USER_QUEUE = "is_user_queue"
}
}

View file

@ -24,8 +24,8 @@ import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Parent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -53,7 +53,7 @@ class PlaybackStateManager private constructor() {
field = value field = value
callbacks.forEach { it.onPositionUpdate(value) } callbacks.forEach { it.onPositionUpdate(value) }
} }
private var mParent: Parent? = null private var mParent: MusicParent? = null
set(value) { set(value) {
field = value field = value
callbacks.forEach { it.onParentUpdate(value) } callbacks.forEach { it.onParentUpdate(value) }
@ -75,7 +75,7 @@ class PlaybackStateManager private constructor() {
field = value field = value
callbacks.forEach { it.onIndexUpdate(value) } callbacks.forEach { it.onIndexUpdate(value) }
} }
private var mMode = PlaybackMode.ALL_SONGS private var mPlaybackMode = PlaybackMode.ALL_SONGS
set(value) { set(value) {
field = value field = value
callbacks.forEach { it.onModeUpdate(value) } callbacks.forEach { it.onModeUpdate(value) }
@ -109,17 +109,17 @@ class PlaybackStateManager private constructor() {
/** The currently playing song. Null if there isn't one */ /** The currently playing song. Null if there isn't one */
val song: Song? get() = mSong val song: Song? get() = mSong
/** The parent the queue is based on, null if all_songs */ /** The parent the queue is based on, null if all_songs */
val parent: Parent? get() = mParent val parent: MusicParent? get() = mParent
/** The current playback progress */ /** The current playback progress */
val position: Long get() = mPosition val position: Long get() = mPosition
/** The current queue determined by [parent] and [mode] */ /** The current queue determined by [parent] and [playbackMode] */
val queue: List<Song> get() = mQueue val queue: List<Song> get() = mQueue
/** The queue created by the user. */ /** The queue created by the user. */
val userQueue: List<Song> get() = mUserQueue val userQueue: List<Song> get() = mUserQueue
/** The current index of the queue */ /** The current index of the queue */
val index: Int get() = mIndex val index: Int get() = mIndex
/** The current [PlaybackMode] */ /** The current [PlaybackMode] */
val mode: PlaybackMode get() = mMode val playbackMode: PlaybackMode get() = mPlaybackMode
/** Whether playback is paused or not */ /** Whether playback is paused or not */
val isPlaying: Boolean get() = mIsPlaying val isPlaying: Boolean get() = mIsPlaying
/** Whether the queue is shuffled */ /** Whether the queue is shuffled */
@ -194,7 +194,7 @@ class PlaybackStateManager private constructor() {
} }
} }
mMode = mode mPlaybackMode = mode
updatePlayback(song) updatePlayback(song)
// Keep shuffle on, if enabled // Keep shuffle on, if enabled
@ -205,7 +205,7 @@ class PlaybackStateManager private constructor() {
* Play a [parent], such as an artist or album. * Play a [parent], such as an artist or album.
* @param shuffled Whether the queue is shuffled or not * @param shuffled Whether the queue is shuffled or not
*/ */
fun playParent(parent: Parent, shuffled: Boolean) { fun playParent(parent: MusicParent, shuffled: Boolean) {
logD("Playing ${parent.name}") logD("Playing ${parent.name}")
mParent = parent mParent = parent
@ -214,17 +214,17 @@ class PlaybackStateManager private constructor() {
when (parent) { when (parent) {
is Album -> { is Album -> {
mQueue = parent.songs.toMutableList() mQueue = parent.songs.toMutableList()
mMode = PlaybackMode.IN_ALBUM mPlaybackMode = PlaybackMode.IN_ALBUM
} }
is Artist -> { is Artist -> {
mQueue = parent.songs.toMutableList() mQueue = parent.songs.toMutableList()
mMode = PlaybackMode.IN_ARTIST mPlaybackMode = PlaybackMode.IN_ARTIST
} }
is Genre -> { is Genre -> {
mQueue = parent.songs.toMutableList() mQueue = parent.songs.toMutableList()
mMode = PlaybackMode.IN_GENRE mPlaybackMode = PlaybackMode.IN_GENRE
} }
} }
@ -238,7 +238,7 @@ class PlaybackStateManager private constructor() {
fun shuffleAll() { fun shuffleAll() {
val musicStore = MusicStore.maybeGetInstance() ?: return val musicStore = MusicStore.maybeGetInstance() ?: return
mMode = PlaybackMode.ALL_SONGS mPlaybackMode = PlaybackMode.ALL_SONGS
mQueue = musicStore.songs.toMutableList() mQueue = musicStore.songs.toMutableList()
mParent = null mParent = null
@ -471,7 +471,7 @@ class PlaybackStateManager private constructor() {
val musicStore = MusicStore.requireInstance() val musicStore = MusicStore.requireInstance()
mQueue = when (mMode) { mQueue = when (mPlaybackMode) {
PlaybackMode.ALL_SONGS -> PlaybackMode.ALL_SONGS ->
settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList() settingsManager.libSongSort.sortSongs(musicStore.songs).toMutableList()
PlaybackMode.IN_ALBUM -> PlaybackMode.IN_ALBUM ->
@ -537,6 +537,14 @@ class PlaybackStateManager private constructor() {
setPlaying(true) setPlaying(true)
} }
/**
* Loop playback around to the beginning.
*/
fun loop() {
seekTo(0)
setPlaying(!settingsManager.pauseOnLoop)
}
/** /**
* Set the [LoopMode] to [mode]. * Set the [LoopMode] to [mode].
*/ */
@ -573,8 +581,16 @@ class PlaybackStateManager private constructor() {
val database = PlaybackStateDatabase.getInstance(context) val database = PlaybackStateDatabase.getInstance(context)
database.writeState(packToPlaybackState()) logD("$mPlaybackMode")
database.writeQueue(packQueue())
database.writeState(
PlaybackStateDatabase.SavedState(
mSong, mPosition, mParent, mIndex,
mPlaybackMode, mIsShuffling, mLoopMode, mIsInUserQueue
)
)
database.writeQueue(PlaybackStateDatabase.SavedQueue(mUserQueue, mQueue))
this@PlaybackStateManager.logD( this@PlaybackStateManager.logD(
"Save finished in ${System.currentTimeMillis() - start}ms" "Save finished in ${System.currentTimeMillis() - start}ms"
@ -589,28 +605,28 @@ class PlaybackStateManager private constructor() {
suspend fun restoreFromDatabase(context: Context) { suspend fun restoreFromDatabase(context: Context) {
logD("Getting state from DB.") logD("Getting state from DB.")
val musicStore = MusicStore.requireInstance()
val start: Long val start: Long
val playbackState: DatabaseState? val playbackState: PlaybackStateDatabase.SavedState?
val queueItems: List<DatabaseQueueItem> val queue: PlaybackStateDatabase.SavedQueue
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
start = System.currentTimeMillis() start = System.currentTimeMillis()
val database = PlaybackStateDatabase.getInstance(context) val database = PlaybackStateDatabase.getInstance(context)
playbackState = database.readState() playbackState = database.readState(musicStore)
queueItems = database.readQueue() queue = database.readQueue(musicStore)
} }
// Get off the IO coroutine since it will cause LiveData updates to throw an exception // Get off the IO coroutine since it will cause LiveData updates to throw an exception
if (playbackState != null) { if (playbackState != null) {
logD("Found playback state $playbackState with queue size ${queueItems.size}") logD("Found playback state $playbackState with queue size ${queue.user.size + queue.queue.size}")
val musicStore = MusicStore.requireInstance() unpackFromPlaybackState(playbackState)
unpackQueue(queue)
unpackFromPlaybackState(playbackState, musicStore)
unpackQueue(queueItems, musicStore)
doParentSanityCheck() doParentSanityCheck()
} }
@ -619,78 +635,32 @@ class PlaybackStateManager private constructor() {
markRestored() markRestored()
} }
/**
* Pack the current state into a [DatabaseState] to be saved.
* @return A [DatabaseState] reflecting the current state.
*/
private fun packToPlaybackState(): DatabaseState {
return DatabaseState(
songHash = mSong?.hash ?: Int.MIN_VALUE,
position = mPosition,
parentHash = mParent?.hash ?: Int.MIN_VALUE,
index = mIndex,
mode = mMode.toInt(),
isShuffling = mIsShuffling,
loopMode = mLoopMode.toInt(),
inUserQueue = mIsInUserQueue
)
}
/** /**
* Unpack a [playbackState] into this instance. * Unpack a [playbackState] into this instance.
*/ */
private fun unpackFromPlaybackState(playbackState: DatabaseState, musicStore: MusicStore) { private fun unpackFromPlaybackState(playbackState: PlaybackStateDatabase.SavedState) {
// Turn the simplified information from PlaybackState into usable data. // Turn the simplified information from PlaybackState into usable data.
// Do queue setup first // Do queue setup first
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS mPlaybackMode = playbackState.playbackMode
mParent = findParent(playbackState.parentHash, mMode, musicStore) mParent = playbackState.parent
mIndex = playbackState.index mIndex = playbackState.queueIndex
// Then set up the current state // Then set up the current state
mSong = musicStore.songs.find { it.hash == playbackState.songHash } mSong = playbackState.song
mLoopMode = LoopMode.fromInt(playbackState.loopMode) ?: LoopMode.NONE mLoopMode = playbackState.loopMode
mIsShuffling = playbackState.isShuffling mIsShuffling = playbackState.isShuffling
mIsInUserQueue = playbackState.inUserQueue mIsInUserQueue = playbackState.isInUserQueue
seekTo(playbackState.position) seekTo(playbackState.position)
} }
/**
* Pack the queue into a list of [DatabaseQueueItem]s to be saved.
* @return A list of packed queue items.
*/
private fun packQueue(): List<DatabaseQueueItem> {
val unified = mutableListOf<DatabaseQueueItem>()
var queueItemId = 0L
mUserQueue.forEach { song ->
unified.add(DatabaseQueueItem(queueItemId, song.hash, song.album.hash, true))
queueItemId++
}
mQueue.forEach { song ->
unified.add(DatabaseQueueItem(queueItemId, song.hash, song.album.hash, false))
queueItemId++
}
return unified
}
/** /**
* Unpack a list of queue items into a queue & user queue. * Unpack a list of queue items into a queue & user queue.
* @param queueItems The list of [DatabaseQueueItem]s to unpack.
*/ */
private fun unpackQueue(queueItems: List<DatabaseQueueItem>, musicStore: MusicStore) { private fun unpackQueue(queue: PlaybackStateDatabase.SavedQueue) {
for (item in queueItems) { mUserQueue = queue.user
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song -> mQueue = queue.queue
if (item.isUserQueue) {
mUserQueue.add(song)
} else {
mQueue.add(song)
}
}
}
// When done, get a more accurate index to prevent issues with queue songs that were saved // When done, get a more accurate index to prevent issues with queue songs that were saved
// to the db but are now deleted when the restore occurred. // to the db but are now deleted when the restore occurred.
@ -706,27 +676,15 @@ class PlaybackStateManager private constructor() {
forceUserQueueUpdate() forceUserQueueUpdate()
} }
/**
* Get a [Parent] from music store given a [hash] and PlaybackMode [mode].
*/
private fun findParent(hash: Int, mode: PlaybackMode, musicStore: MusicStore): Parent? {
return when (mode) {
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.hash == hash }
PlaybackMode.ALL_SONGS -> null
}
}
/** /**
* Do the sanity check to make sure the parent was not lost in the restore process. * Do the sanity check to make sure the parent was not lost in the restore process.
*/ */
private fun doParentSanityCheck() { private fun doParentSanityCheck() {
// Check if the parent was lost while in the DB. // Check if the parent was lost while in the DB.
if (mSong != null && mParent == null && mMode != PlaybackMode.ALL_SONGS) { if (mSong != null && mParent == null && mPlaybackMode != PlaybackMode.ALL_SONGS) {
logD("Parent lost, attempting restore.") logD("Parent lost, attempting restore.")
mParent = when (mMode) { mParent = when (mPlaybackMode) {
PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album PlaybackMode.IN_ALBUM -> mQueue.firstOrNull()?.album
PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist PlaybackMode.IN_ARTIST -> mQueue.firstOrNull()?.album?.artist
PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre PlaybackMode.IN_GENRE -> mQueue.firstOrNull()?.genre
@ -742,7 +700,7 @@ class PlaybackStateManager private constructor() {
*/ */
interface Callback { interface Callback {
fun onSongUpdate(song: Song?) {} fun onSongUpdate(song: Song?) {}
fun onParentUpdate(parent: Parent?) {} fun onParentUpdate(parent: MusicParent?) {}
fun onPositionUpdate(position: Long) {} fun onPositionUpdate(position: Long) {}
fun onQueueUpdate(queue: List<Song>) {} fun onQueueUpdate(queue: List<Song>) {}
fun onUserQueueUpdate(userQueue: List<Song>) {} fun onUserQueueUpdate(userQueue: List<Song>) {}

View file

@ -29,7 +29,7 @@ import androidx.core.app.NotificationCompat
import androidx.media.app.NotificationCompat.MediaStyle import androidx.media.app.NotificationCompat.MediaStyle
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.coil.loadBitmap import org.oxycblt.auxio.coil.loadBitmap
import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
import org.oxycblt.auxio.util.newBroadcastIntent import org.oxycblt.auxio.util.newBroadcastIntent
@ -118,7 +118,7 @@ class PlaybackNotification private constructor(
/** /**
* Apply the current [parent] to the header of the notification. * Apply the current [parent] to the header of the notification.
*/ */
fun setParent(parent: Parent?) { fun setParent(parent: MusicParent?) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
// A blank parent always means that the mode is ALL_SONGS // A blank parent always means that the mode is ALL_SONGS

View file

@ -52,7 +52,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toURI import org.oxycblt.auxio.music.toURI
import org.oxycblt.auxio.playback.state.LoopMode import org.oxycblt.auxio.playback.state.LoopMode
@ -223,11 +223,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
Player.STATE_ENDED -> { Player.STATE_ENDED -> {
if (playbackManager.loopMode == LoopMode.TRACK) { if (playbackManager.loopMode == LoopMode.TRACK) {
playbackManager.rewind() playbackManager.loop()
if (settingsManager.pauseOnLoop) {
playbackManager.setPlaying(false)
}
} else { } else {
playbackManager.next() playbackManager.next()
} }
@ -270,7 +266,7 @@ class PlaybackService : Service(), Player.Listener, PlaybackStateManager.Callbac
stopForegroundAndNotification() stopForegroundAndNotification()
} }
override fun onParentUpdate(parent: Parent?) { override fun onParentUpdate(parent: MusicParent?) {
notification.setParent(parent) notification.setParent(parent)
startForegroundOrNotify() startForegroundOrNotify()

View file

@ -23,7 +23,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Parent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
/** /**
@ -99,14 +99,14 @@ enum class SortMode(@IdRes val itemId: Int) {
} }
/** /**
* Sort a generic list of [Parent] instances. * Sort a generic list of [MusicParent] instances.
* *
* **Behavior:** * **Behavior:**
* - [ASCENDING]: By name after article, ascending * - [ASCENDING]: By name after article, ascending
* - [DESCENDING]: By name after article, descending * - [DESCENDING]: By name after article, descending
* - Same parent list is returned otherwise. * - Same parent list is returned otherwise.
*/ */
fun <T : Parent> sortParents(parents: Collection<T>): List<T> { fun <T : MusicParent> sortParents(parents: Collection<T>): List<T> {
return when (this) { return when (this) {
ASCENDING -> parents.sortedWith( ASCENDING -> parents.sortedWith(
compareBy(String.CASE_INSENSITIVE_ORDER) { model -> compareBy(String.CASE_INSENSITIVE_ORDER) { model ->

View file

@ -33,6 +33,7 @@
android:background="@android:color/transparent" android:background="@android:color/transparent"
app:tabContentStart="@dimen/spacing_medium" app:tabContentStart="@dimen/spacing_medium"
app:tabMode="scrollable" app:tabMode="scrollable"
app:tabGravity="start"
app:tabTextAppearance="@style/TextAppearance.Auxio.TabLayout.Label" app:tabTextAppearance="@style/TextAppearance.Auxio.TabLayout.Label"
app:tabTextColor="@color/sel_accented_primary"/> app:tabTextColor="@color/sel_accented_primary"/>