Move playback state system to hashes
Use unique-ish hashes in the playback state system instead of the less efficent and less reliable string system. This cuts save times in ~half and improves restore times by ~1/3. Yeah, this is like the 4th time I've changed this system but unless I have some major loader refactor I think this wont change again.
This commit is contained in:
parent
969f25176a
commit
17e5aed131
12 changed files with 121 additions and 96 deletions
|
@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
|
import org.oxycblt.auxio.ui.assertBackgroundThread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Database for storing blacklisted paths.
|
* Database for storing blacklisted paths.
|
||||||
|
|
|
@ -2,7 +2,6 @@ package org.oxycblt.auxio.database
|
||||||
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.os.Looper
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shortcut for querying all items in a database and running [block] with the cursor returned.
|
* Shortcut for querying all items in a database and running [block] with the cursor returned.
|
||||||
|
@ -10,12 +9,3 @@ import android.os.Looper
|
||||||
*/
|
*/
|
||||||
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
||||||
query(tableName, null, null, null, null, null, null)?.use(block)
|
query(tableName, null, null, null, null, null, null)?.use(block)
|
||||||
|
|
||||||
/**
|
|
||||||
* Assert that we are on a background thread.
|
|
||||||
*/
|
|
||||||
fun assertBackgroundThread() {
|
|
||||||
check(Looper.myLooper() != Looper.getMainLooper()) {
|
|
||||||
"Database operations must be ran on a background thread."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ package org.oxycblt.auxio.database
|
||||||
/**
|
/**
|
||||||
* A database entity that stores a compressed variant of the current playback state.
|
* A database entity that stores a compressed variant of the current playback state.
|
||||||
* @property id - The database key for this state
|
* @property id - The database key for this state
|
||||||
* @property songName - The song that is currently playing
|
* @property songHash - The hash for the currently playing song
|
||||||
* @property parentName - The parent that is being played from [-1 if none]
|
* @property parentHash - The hash for the currently playing parent
|
||||||
* @property index - The current index in the queue.
|
* @property index - The current index in the queue.
|
||||||
* @property mode - The integer form of the current [org.oxycblt.auxio.playback.state.PlaybackMode]
|
* @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 isShuffling - A bool for if the queue was shuffled
|
||||||
|
@ -14,10 +14,9 @@ package org.oxycblt.auxio.database
|
||||||
*/
|
*/
|
||||||
data class PlaybackState(
|
data class PlaybackState(
|
||||||
val id: Long = 0L,
|
val id: Long = 0L,
|
||||||
val songName: String = "",
|
val songHash: Int,
|
||||||
val songAlbumName: String = "",
|
|
||||||
val position: Long,
|
val position: Long,
|
||||||
val parentName: String = "",
|
val parentHash: Int,
|
||||||
val index: Int,
|
val index: Int,
|
||||||
val mode: Int,
|
val mode: Int,
|
||||||
val isShuffling: Boolean,
|
val isShuffling: Boolean,
|
||||||
|
@ -26,11 +25,10 @@ data class PlaybackState(
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val COLUMN_ID = "state_id"
|
const val COLUMN_ID = "state_id"
|
||||||
const val COLUMN_SONG_NAME = "cur_song_name"
|
const val COLUMN_SONG_HASH = "song"
|
||||||
const val COLUMN_SONG_ALBUM_NAME = "cur_song_album"
|
|
||||||
const val COLUMN_POSITION = "position"
|
const val COLUMN_POSITION = "position"
|
||||||
const val COLUMN_PARENT_NAME = "parent_name"
|
const val COLUMN_PARENT_HASH = "parent"
|
||||||
const val COLUMN_INDEX = "state_index"
|
const val COLUMN_INDEX = "_index"
|
||||||
const val COLUMN_MODE = "mode"
|
const val COLUMN_MODE = "mode"
|
||||||
const val COLUMN_IS_SHUFFLING = "is_shuffling"
|
const val COLUMN_IS_SHUFFLING = "is_shuffling"
|
||||||
const val COLUMN_LOOP_MODE = "loop_mode"
|
const val COLUMN_LOOP_MODE = "loop_mode"
|
||||||
|
|
|
@ -4,9 +4,9 @@ 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.getStringOrNull
|
|
||||||
import androidx.core.database.sqlite.transaction
|
import androidx.core.database.sqlite.transaction
|
||||||
import org.oxycblt.auxio.logD
|
import org.oxycblt.auxio.logD
|
||||||
|
import org.oxycblt.auxio.ui.assertBackgroundThread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A SQLite database for managing the persistent playback state and queue.
|
* A SQLite database for managing the persistent playback state and queue.
|
||||||
|
@ -57,10 +57,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
*/
|
*/
|
||||||
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
private fun constructStateTable(command: StringBuilder): StringBuilder {
|
||||||
command.append("${PlaybackState.COLUMN_ID} LONG PRIMARY KEY,")
|
command.append("${PlaybackState.COLUMN_ID} LONG PRIMARY KEY,")
|
||||||
.append("${PlaybackState.COLUMN_SONG_NAME} TEXT NOT NULL,")
|
.append("${PlaybackState.COLUMN_SONG_HASH} INTEGER NOT NULL,")
|
||||||
.append("${PlaybackState.COLUMN_SONG_ALBUM_NAME} TEXT NOT NULL,")
|
|
||||||
.append("${PlaybackState.COLUMN_POSITION} LONG NOT NULL,")
|
.append("${PlaybackState.COLUMN_POSITION} LONG NOT NULL,")
|
||||||
.append("${PlaybackState.COLUMN_PARENT_NAME} TEXT NOT NULL,")
|
.append("${PlaybackState.COLUMN_PARENT_HASH} INTEGER NOT NULL,")
|
||||||
.append("${PlaybackState.COLUMN_INDEX} INTEGER NOT NULL,")
|
.append("${PlaybackState.COLUMN_INDEX} INTEGER NOT NULL,")
|
||||||
.append("${PlaybackState.COLUMN_MODE} INTEGER NOT NULL,")
|
.append("${PlaybackState.COLUMN_MODE} INTEGER NOT NULL,")
|
||||||
.append("${PlaybackState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,")
|
.append("${PlaybackState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,")
|
||||||
|
@ -75,8 +74,8 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
*/
|
*/
|
||||||
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
private fun constructQueueTable(command: StringBuilder): StringBuilder {
|
||||||
command.append("${QueueItem.COLUMN_ID} LONG PRIMARY KEY,")
|
command.append("${QueueItem.COLUMN_ID} LONG PRIMARY KEY,")
|
||||||
.append("${QueueItem.COLUMN_SONG_NAME} TEXT NOT NULL,")
|
.append("${QueueItem.COLUMN_SONG_HASH} INTEGER NOT NULL,")
|
||||||
.append("${QueueItem.COLUMN_ALBUM_NAME} TEXT NOT NULL,")
|
.append("${QueueItem.COLUMN_ALBUM_HASH} INTEGER NOT NULL,")
|
||||||
.append("${QueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)")
|
.append("${QueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)")
|
||||||
|
|
||||||
return command
|
return command
|
||||||
|
@ -97,10 +96,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
val stateData = ContentValues(10).apply {
|
val stateData = ContentValues(10).apply {
|
||||||
put(PlaybackState.COLUMN_ID, state.id)
|
put(PlaybackState.COLUMN_ID, state.id)
|
||||||
put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
put(PlaybackState.COLUMN_SONG_HASH, state.songHash)
|
||||||
put(PlaybackState.COLUMN_SONG_ALBUM_NAME, state.songAlbumName)
|
|
||||||
put(PlaybackState.COLUMN_POSITION, state.position)
|
put(PlaybackState.COLUMN_POSITION, state.position)
|
||||||
put(PlaybackState.COLUMN_PARENT_NAME, state.parentName)
|
put(PlaybackState.COLUMN_PARENT_HASH, state.parentHash)
|
||||||
put(PlaybackState.COLUMN_INDEX, state.index)
|
put(PlaybackState.COLUMN_INDEX, state.index)
|
||||||
put(PlaybackState.COLUMN_MODE, state.mode)
|
put(PlaybackState.COLUMN_MODE, state.mode)
|
||||||
put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
|
||||||
|
@ -126,10 +124,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
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(PlaybackState.COLUMN_SONG_NAME)
|
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_HASH)
|
||||||
val albumIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ALBUM_NAME)
|
|
||||||
val posIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_POSITION)
|
val posIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_POSITION)
|
||||||
val parentIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_PARENT_NAME)
|
val parentIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_PARENT_HASH)
|
||||||
val indexIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_INDEX)
|
val indexIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_INDEX)
|
||||||
val modeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_MODE)
|
val modeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_MODE)
|
||||||
val shuffleIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
|
val shuffleIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
|
||||||
|
@ -141,10 +138,9 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
cursor.moveToFirst()
|
cursor.moveToFirst()
|
||||||
|
|
||||||
state = PlaybackState(
|
state = PlaybackState(
|
||||||
songName = cursor.getStringOrNull(songIndex) ?: "",
|
songHash = cursor.getInt(songIndex),
|
||||||
songAlbumName = cursor.getStringOrNull(albumIndex) ?: "",
|
|
||||||
position = cursor.getLong(posIndex),
|
position = cursor.getLong(posIndex),
|
||||||
parentName = cursor.getStringOrNull(parentIndex) ?: "",
|
parentHash = cursor.getInt(parentIndex),
|
||||||
index = cursor.getInt(indexIndex),
|
index = cursor.getInt(indexIndex),
|
||||||
mode = cursor.getInt(modeIndex),
|
mode = cursor.getInt(modeIndex),
|
||||||
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
||||||
|
@ -183,8 +179,8 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
|
|
||||||
val itemData = ContentValues(4).apply {
|
val itemData = ContentValues(4).apply {
|
||||||
put(QueueItem.COLUMN_ID, item.id)
|
put(QueueItem.COLUMN_ID, item.id)
|
||||||
put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
put(QueueItem.COLUMN_SONG_HASH, item.songHash)
|
||||||
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
put(QueueItem.COLUMN_ALBUM_HASH, item.albumHash)
|
||||||
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,15 +209,15 @@ class PlaybackStateDatabase(context: Context) :
|
||||||
if (cursor.count == 0) return@queryAll
|
if (cursor.count == 0) return@queryAll
|
||||||
|
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
|
val idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
|
||||||
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_NAME)
|
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_HASH)
|
||||||
val albumIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ALBUM_NAME)
|
val albumIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ALBUM_HASH)
|
||||||
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
|
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
queueItems += QueueItem(
|
queueItems += QueueItem(
|
||||||
id = cursor.getLong(idIndex),
|
id = cursor.getLong(idIndex),
|
||||||
songName = cursor.getStringOrNull(songIdIndex) ?: "",
|
songHash = cursor.getInt(songIdIndex),
|
||||||
albumName = cursor.getStringOrNull(albumIdIndex) ?: "",
|
albumHash = cursor.getInt(albumIdIndex),
|
||||||
isUserQueue = cursor.getInt(isUserQueueIndex) == 1
|
isUserQueue = cursor.getInt(isUserQueueIndex) == 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,21 +3,21 @@ package org.oxycblt.auxio.database
|
||||||
/**
|
/**
|
||||||
* A database entity that stores a simplified representation of a song in a queue.
|
* A database entity that stores a simplified representation of a song in a queue.
|
||||||
* @property id The database entity's id
|
* @property id The database entity's id
|
||||||
* @property songName The song name for this queue item
|
* @property songHash The hash for the song represented
|
||||||
* @property albumName The album name for this queue item, used to make searching quicker
|
* @property albumHash The hash for the album represented
|
||||||
* @property isUserQueue A bool for if this queue item is a user queue item or not
|
* @property isUserQueue A bool for if this queue item is a user queue item or not
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
data class QueueItem(
|
data class QueueItem(
|
||||||
var id: Long = 0L,
|
var id: Long = 0L,
|
||||||
val songName: String = "",
|
val songHash: Int,
|
||||||
val albumName: String = "",
|
val albumHash: Int,
|
||||||
val isUserQueue: Boolean = false
|
val isUserQueue: Boolean = false
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
const val COLUMN_ID = "id"
|
const val COLUMN_ID = "id"
|
||||||
const val COLUMN_SONG_NAME = "song_name"
|
const val COLUMN_SONG_HASH = "song"
|
||||||
const val COLUMN_ALBUM_NAME = "album_name"
|
const val COLUMN_ALBUM_HASH = "album"
|
||||||
const val COLUMN_IS_USER_QUEUE = "is_user_queue"
|
const val COLUMN_IS_USER_QUEUE = "is_user_queue"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,6 @@ import android.net.Uri
|
||||||
|
|
||||||
// --- MUSIC MODELS ---
|
// --- MUSIC MODELS ---
|
||||||
|
|
||||||
// TODO: Implement some kind of hash system, removing the need to redundant names but also without the volatility of id
|
|
||||||
// They need to be completely unique, however, and from whatever information I have about them on creation
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The base data object for all music.
|
* The base data object for all music.
|
||||||
* @property id The ID that is assigned to this object
|
* @property id The ID that is assigned to this object
|
||||||
|
@ -20,10 +17,18 @@ sealed class BaseModel {
|
||||||
/**
|
/**
|
||||||
* [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 displayName Name that handles the usage of [Genre.resolvedName] and the normal [BaseModel.name]
|
* @property hash A versatile, unique(ish) hash used for databases
|
||||||
|
* @property displayName Name that handles the usage of [Genre.resolvedName]
|
||||||
|
* and the normal [BaseModel.name]
|
||||||
*/
|
*/
|
||||||
sealed class Parent : BaseModel() {
|
sealed class Parent : BaseModel() {
|
||||||
val displayName: String get() = if (this is Genre) resolvedName else name
|
abstract val hash: Int
|
||||||
|
|
||||||
|
val displayName: String get() = if (this is Genre) {
|
||||||
|
resolvedName
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,6 +43,7 @@ sealed class Parent : BaseModel() {
|
||||||
* These are not ensured to be linked due to possible quirks in the genre loading system.
|
* These are not ensured to be linked due to possible quirks in the genre loading system.
|
||||||
* @property seconds The Song's duration in seconds
|
* @property seconds The Song's duration in seconds
|
||||||
* @property formattedDuration The Song's duration as a duration string.
|
* @property formattedDuration The Song's duration as a duration string.
|
||||||
|
* @property hash A versatile, unique(ish) hash used for databases
|
||||||
*/
|
*/
|
||||||
data class Song(
|
data class Song(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
|
@ -45,7 +51,7 @@ data class Song(
|
||||||
val fileName: String,
|
val fileName: String,
|
||||||
val albumId: Long = -1,
|
val albumId: Long = -1,
|
||||||
val track: Int = -1,
|
val track: Int = -1,
|
||||||
val duration: Long = 0,
|
val duration: Long = 0
|
||||||
) : BaseModel() {
|
) : BaseModel() {
|
||||||
private var mAlbum: Album? = null
|
private var mAlbum: Album? = null
|
||||||
private var mGenre: Genre? = null
|
private var mGenre: Genre? = null
|
||||||
|
@ -56,6 +62,8 @@ data class Song(
|
||||||
val seconds = duration / 1000
|
val seconds = duration / 1000
|
||||||
val formattedDuration = seconds.toDuration()
|
val formattedDuration = seconds.toDuration()
|
||||||
|
|
||||||
|
val hash = songHash()
|
||||||
|
|
||||||
fun linkAlbum(album: Album) {
|
fun linkAlbum(album: Album) {
|
||||||
if (mAlbum == null) {
|
if (mAlbum == null) {
|
||||||
mAlbum = album
|
mAlbum = album
|
||||||
|
@ -67,6 +75,13 @@ data class Song(
|
||||||
mGenre = genre
|
mGenre = genre
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun songHash(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + track
|
||||||
|
result = 31 * result + duration.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,6 +109,8 @@ data class Album(
|
||||||
val totalDuration: String get() =
|
val totalDuration: String get() =
|
||||||
songs.sumOf { it.seconds }.toDuration()
|
songs.sumOf { it.seconds }.toDuration()
|
||||||
|
|
||||||
|
override val hash = albumHash()
|
||||||
|
|
||||||
fun linkArtist(artist: Artist) {
|
fun linkArtist(artist: Artist) {
|
||||||
mArtist = artist
|
mArtist = artist
|
||||||
}
|
}
|
||||||
|
@ -104,6 +121,13 @@ data class Album(
|
||||||
mSongs.add(song)
|
mSongs.add(song)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun albumHash(): Int {
|
||||||
|
var result = name.hashCode()
|
||||||
|
result = 31 * result + artistName.hashCode()
|
||||||
|
result = 31 * result + year
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -117,12 +141,6 @@ data class Artist(
|
||||||
override val name: String,
|
override val name: String,
|
||||||
val albums: List<Album>
|
val albums: List<Album>
|
||||||
) : Parent() {
|
) : Parent() {
|
||||||
init {
|
|
||||||
albums.forEach { album ->
|
|
||||||
album.linkArtist(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val genre: Genre? by lazy {
|
val genre: Genre? by lazy {
|
||||||
// Get the genre that corresponds to the most songs in this artist, which would be
|
// Get the genre that corresponds to the most songs in this artist, which would be
|
||||||
// the most "Prominent" genre.
|
// the most "Prominent" genre.
|
||||||
|
@ -132,6 +150,14 @@ data class Artist(
|
||||||
val songs: List<Song> by lazy {
|
val songs: List<Song> by lazy {
|
||||||
albums.flatMap { it.songs }
|
albums.flatMap { it.songs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val hash = name.hashCode()
|
||||||
|
|
||||||
|
init {
|
||||||
|
albums.forEach { album ->
|
||||||
|
album.linkArtist(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -153,6 +179,8 @@ data class Genre(
|
||||||
val totalDuration: String get() =
|
val totalDuration: String get() =
|
||||||
songs.sumOf { it.seconds }.toDuration()
|
songs.sumOf { it.seconds }.toDuration()
|
||||||
|
|
||||||
|
override val hash = name.hashCode()
|
||||||
|
|
||||||
fun linkSong(song: Song) {
|
fun linkSong(song: Song) {
|
||||||
mSongs.add(song)
|
mSongs.add(song)
|
||||||
song.linkGenre(this)
|
song.linkGenre(this)
|
||||||
|
|
|
@ -13,8 +13,12 @@ import org.oxycblt.auxio.logD
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
|
* Class that loads/constructs [Genre]s, [Artist]s, [Album]s, and [Song] objects from the filesystem
|
||||||
* TODO: Use album artist instead of artist tag.
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
|
*
|
||||||
|
* FIXME: Here's a catalog of problems that I already know about with this abomination
|
||||||
|
* - Does not support the album artist tag
|
||||||
|
* - All loading is done at startup [Not efficent for large libraries, would require massive arch retooling to fix]
|
||||||
|
* - Genre system is a bottleneck [Nothing I can do about it, MediaStore is garbage]
|
||||||
*/
|
*/
|
||||||
class MusicLoader(private val context: Context) {
|
class MusicLoader(private val context: Context) {
|
||||||
var genres = mutableListOf<Genre>()
|
var genres = mutableListOf<Genre>()
|
||||||
|
@ -94,9 +98,9 @@ class MusicLoader(private val context: Context) {
|
||||||
Albums._ID, // 0
|
Albums._ID, // 0
|
||||||
Albums.ALBUM, // 1
|
Albums.ALBUM, // 1
|
||||||
Albums.ARTIST, // 2
|
Albums.ARTIST, // 2
|
||||||
Albums.FIRST_YEAR, // 3
|
Albums.FIRST_YEAR, // 4
|
||||||
),
|
),
|
||||||
null, null,
|
"", null,
|
||||||
Albums.DEFAULT_SORT_ORDER
|
Albums.DEFAULT_SORT_ORDER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,13 +110,13 @@ class MusicLoader(private val context: Context) {
|
||||||
albumCursor?.use { cursor ->
|
albumCursor?.use { cursor ->
|
||||||
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
val idIndex = cursor.getColumnIndexOrThrow(Albums._ID)
|
||||||
val nameIndex = cursor.getColumnIndexOrThrow(Albums.ALBUM)
|
val nameIndex = cursor.getColumnIndexOrThrow(Albums.ALBUM)
|
||||||
val artistIdIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST)
|
val artistNameIndex = cursor.getColumnIndexOrThrow(Albums.ARTIST)
|
||||||
val yearIndex = cursor.getColumnIndexOrThrow(Albums.FIRST_YEAR)
|
val yearIndex = cursor.getColumnIndexOrThrow(Albums.FIRST_YEAR)
|
||||||
|
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
val id = cursor.getLong(idIndex)
|
val id = cursor.getLong(idIndex)
|
||||||
val name = cursor.getString(nameIndex) ?: albumPlaceholder
|
val name = cursor.getString(nameIndex) ?: albumPlaceholder
|
||||||
var artistName = cursor.getString(artistIdIndex) ?: artistPlaceholder
|
var artistName = cursor.getString(artistNameIndex) ?: artistPlaceholder
|
||||||
val year = cursor.getInt(yearIndex)
|
val year = cursor.getInt(yearIndex)
|
||||||
val coverUri = id.toAlbumArtURI()
|
val coverUri = id.toAlbumArtURI()
|
||||||
|
|
||||||
|
@ -144,7 +148,7 @@ class MusicLoader(private val context: Context) {
|
||||||
Media.TITLE, // 2
|
Media.TITLE, // 2
|
||||||
Media.ALBUM_ID, // 3
|
Media.ALBUM_ID, // 3
|
||||||
Media.TRACK, // 4
|
Media.TRACK, // 4
|
||||||
Media.DURATION // 5
|
Media.DURATION, // 5
|
||||||
),
|
),
|
||||||
selector, args,
|
selector, args,
|
||||||
Media.DEFAULT_SORT_ORDER
|
Media.DEFAULT_SORT_ORDER
|
||||||
|
|
|
@ -75,20 +75,14 @@ class MusicStore private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a song from this instance in a safe manner.
|
* Find a song in a faster manner using a hash for its album as well.
|
||||||
* Using a normal search of the songs list runs the risk of getting the *wrong* song with
|
|
||||||
* the same name, so the album name is also used to fix the above problem.
|
|
||||||
* FIXME: Artist names are more unique than album names, use those
|
|
||||||
* @param name The name of the song
|
|
||||||
* @param albumName The name of the song's album.
|
|
||||||
* @return The song requested, null if there isnt one.
|
|
||||||
*/
|
*/
|
||||||
fun findSong(name: String, albumName: String): Song? {
|
fun findSongFast(songHash: Int, albumHash: Int): Song? {
|
||||||
return albums.find { it.name == albumName }?.songs?.find { it.name == name }
|
return albums.find { it.hash == albumHash }?.songs?.find { it.hash == songHash }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a song for a [uri], this is similar to [findSong], but with some kind of content uri.
|
* Find a song for a [uri], this is similar to [findSongFast], but with some kind of content uri.
|
||||||
* @return The corresponding [Song] for this [uri], null if there isnt one.
|
* @return The corresponding [Song] for this [uri], null if there isnt one.
|
||||||
*/
|
*/
|
||||||
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
fun findSongForUri(uri: Uri, resolver: ContentResolver): Song? {
|
||||||
|
|
|
@ -27,7 +27,6 @@ import org.oxycblt.auxio.settings.SettingsManager
|
||||||
* All access should be done with [PlaybackStateManager.getInstance].
|
* All access should be done with [PlaybackStateManager.getInstance].
|
||||||
*
|
*
|
||||||
* TODO: Queues should reflect sort mode
|
* TODO: Queues should reflect sort mode
|
||||||
* TODO: Update loop mode to actually make sense [#13]
|
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class PlaybackStateManager private constructor() {
|
class PlaybackStateManager private constructor() {
|
||||||
|
@ -605,10 +604,9 @@ class PlaybackStateManager private constructor() {
|
||||||
*/
|
*/
|
||||||
private fun packToPlaybackState(): PlaybackState {
|
private fun packToPlaybackState(): PlaybackState {
|
||||||
return PlaybackState(
|
return PlaybackState(
|
||||||
songName = mSong?.name ?: "",
|
songHash = mSong?.hash ?: Int.MIN_VALUE,
|
||||||
songAlbumName = mSong?.album?.name ?: "",
|
|
||||||
position = mPosition,
|
position = mPosition,
|
||||||
parentName = mParent?.name ?: "",
|
parentHash = mParent?.hash ?: Int.MIN_VALUE,
|
||||||
index = mIndex,
|
index = mIndex,
|
||||||
mode = mMode.toInt(),
|
mode = mMode.toInt(),
|
||||||
isShuffling = mIsShuffling,
|
isShuffling = mIsShuffling,
|
||||||
|
@ -625,11 +623,11 @@ class PlaybackStateManager private constructor() {
|
||||||
|
|
||||||
// Do queue setup first
|
// Do queue setup first
|
||||||
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
mMode = PlaybackMode.fromInt(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||||
mParent = findParent(playbackState.parentName, mMode)
|
mParent = findParent(playbackState.parentHash, mMode)
|
||||||
mIndex = playbackState.index
|
mIndex = playbackState.index
|
||||||
|
|
||||||
// Then set up the current state
|
// Then set up the current state
|
||||||
mSong = musicStore.findSong(playbackState.songName, playbackState.songAlbumName)
|
mSong = musicStore.songs.find { it.hash == playbackState.songHash }
|
||||||
mLoopMode = LoopMode.fromInt(playbackState.loopMode) ?: LoopMode.NONE
|
mLoopMode = LoopMode.fromInt(playbackState.loopMode) ?: LoopMode.NONE
|
||||||
mIsShuffling = playbackState.isShuffling
|
mIsShuffling = playbackState.isShuffling
|
||||||
mIsInUserQueue = playbackState.inUserQueue
|
mIsInUserQueue = playbackState.inUserQueue
|
||||||
|
@ -647,12 +645,12 @@ class PlaybackStateManager private constructor() {
|
||||||
var queueItemId = 0L
|
var queueItemId = 0L
|
||||||
|
|
||||||
mUserQueue.forEach { song ->
|
mUserQueue.forEach { song ->
|
||||||
unified.add(QueueItem(queueItemId, song.name, song.album.name, true))
|
unified.add(QueueItem(queueItemId, song.hash, song.album.hash, true))
|
||||||
queueItemId++
|
queueItemId++
|
||||||
}
|
}
|
||||||
|
|
||||||
mQueue.forEach { song ->
|
mQueue.forEach { song ->
|
||||||
unified.add(QueueItem(queueItemId, song.name, song.album.name, false))
|
unified.add(QueueItem(queueItemId, song.hash, song.album.hash, false))
|
||||||
queueItemId++
|
queueItemId++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -664,8 +662,8 @@ class PlaybackStateManager private constructor() {
|
||||||
* @param queueItems The list of [QueueItem]s to unpack.
|
* @param queueItems The list of [QueueItem]s to unpack.
|
||||||
*/
|
*/
|
||||||
private fun unpackQueue(queueItems: List<QueueItem>) {
|
private fun unpackQueue(queueItems: List<QueueItem>) {
|
||||||
queueItems.forEach { item ->
|
for (item in queueItems) {
|
||||||
musicStore.findSong(item.songName, item.albumName)?.let { song ->
|
musicStore.findSongFast(item.songHash, item.albumHash)?.let { song ->
|
||||||
if (item.isUserQueue) {
|
if (item.isUserQueue) {
|
||||||
mUserQueue.add(song)
|
mUserQueue.add(song)
|
||||||
} else {
|
} else {
|
||||||
|
@ -689,15 +687,14 @@ class PlaybackStateManager private constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a [Parent] from music store given a [name] and playback [mode].
|
* Get a [Parent] from music store given a [hash] and PlaybackMode [mode].
|
||||||
*/
|
*/
|
||||||
private fun findParent(name: String, mode: PlaybackMode): Parent? {
|
private fun findParent(hash: Int, mode: PlaybackMode): Parent? {
|
||||||
return when (mode) {
|
return when (mode) {
|
||||||
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.name == name }
|
PlaybackMode.IN_GENRE -> musicStore.genres.find { it.hash == hash }
|
||||||
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.name == name }
|
PlaybackMode.IN_ARTIST -> musicStore.artists.find { it.hash == hash }
|
||||||
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.name == name }
|
PlaybackMode.IN_ALBUM -> musicStore.albums.find { it.hash == hash }
|
||||||
|
PlaybackMode.ALL_SONGS -> null
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import org.oxycblt.auxio.settings.blacklist.BlacklistDialog
|
||||||
import org.oxycblt.auxio.settings.ui.IntListPrefDialog
|
import org.oxycblt.auxio.settings.ui.IntListPrefDialog
|
||||||
import org.oxycblt.auxio.settings.ui.IntListPreference
|
import org.oxycblt.auxio.settings.ui.IntListPreference
|
||||||
import org.oxycblt.auxio.ui.Accent
|
import org.oxycblt.auxio.ui.Accent
|
||||||
|
import org.oxycblt.auxio.ui.showToast
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
* The actual fragment containing the settings menu. Inherits [PreferenceFragmentCompat].
|
||||||
|
@ -129,7 +130,7 @@ class SettingsListFragment : PreferenceFragmentCompat() {
|
||||||
SettingsManager.KEY_SAVE_STATE -> {
|
SettingsManager.KEY_SAVE_STATE -> {
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
playbackModel.savePlaybackState(requireContext()) {
|
playbackModel.savePlaybackState(requireContext()) {
|
||||||
requireContext().getString(R.string.label_state_saved)
|
requireContext().showToast(R.string.label_state_saved)
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.graphics.Point
|
||||||
import android.graphics.drawable.AnimatedVectorDrawable
|
import android.graphics.drawable.AnimatedVectorDrawable
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Looper
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -138,6 +139,24 @@ fun Context.showToast(@StringRes str: Int) {
|
||||||
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
|
Toast.makeText(applicationContext, getString(str), Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that we are on a background thread.
|
||||||
|
*/
|
||||||
|
fun assertBackgroundThread() {
|
||||||
|
check(Looper.myLooper() != Looper.getMainLooper()) {
|
||||||
|
"This operation must be ran on a background thread."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assert that we are on a foreground thread.
|
||||||
|
*/
|
||||||
|
fun assertMainThread() {
|
||||||
|
check(Looper.myLooper() == Looper.getMainLooper()) {
|
||||||
|
"This operation must be ran on the main thread"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- CONFIGURATION ---
|
// --- CONFIGURATION ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package org.oxycblt.auxio.ui
|
package org.oxycblt.auxio.ui
|
||||||
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
@ -42,9 +41,7 @@ class MemberBinder<T : ViewDataBinding>(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
|
||||||
check(Looper.myLooper() == Looper.getMainLooper()) {
|
assertMainThread()
|
||||||
"View can only be accessed on the main thread."
|
|
||||||
}
|
|
||||||
|
|
||||||
val binding = fragmentBinding
|
val binding = fragmentBinding
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue