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:
parent
10c45f1492
commit
8b8d36cf22
16 changed files with 250 additions and 289 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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>) {}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue