Rewrite Database to stop using room

Refactor/Rewrite the database system to be based on SQLiteOpenHelper instead of Room, as Room will keep empty columns around even after trying to explicitly delete them.
This commit is contained in:
OxygenCobalt 2020-11-22 14:30:45 -07:00
parent d8a40fe219
commit 49897c53b4
22 changed files with 436 additions and 208 deletions

View file

@ -72,12 +72,6 @@ dependencies {
// Media // Media
implementation 'androidx.media:media:1.2.0' implementation 'androidx.media:media:1.2.0'
// Database
def room_version = '2.3.0-alpha03'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
// --- THIRD PARTY --- // --- THIRD PARTY ---
// Image loading // Image loading

View file

@ -1,38 +0,0 @@
package org.oxycblt.auxio.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [PlaybackState::class, QueueItem::class], version = 1, exportSchema = false)
abstract class AuxioDatabase : RoomDatabase() {
abstract val playbackStateDAO: PlaybackStateDAO
abstract val queueDAO: QueueDAO
companion object {
@Volatile
private var INSTANCE: AuxioDatabase? = null
/**
* Get/Instantiate the single instance of [AuxioDatabase].
*/
fun getInstance(context: Context): AuxioDatabase {
val currentInstance = INSTANCE
if (currentInstance != null) {
return currentInstance
}
synchronized(this) {
val newInstance = Room.databaseBuilder(
context.applicationContext,
AuxioDatabase::class.java,
"playback_state_database"
).fallbackToDestructiveMigration().build()
INSTANCE = newInstance
return newInstance
}
}
}
}

View file

@ -1,32 +1,27 @@
package org.oxycblt.auxio.database package org.oxycblt.auxio.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "playback_state_table")
data class PlaybackState( data class PlaybackState(
@PrimaryKey(autoGenerate = true) val id: Long = 0L,
var id: Long = Long.MIN_VALUE, val songId: Long = -1L,
@ColumnInfo(name = "song_id")
val songId: Long = Long.MIN_VALUE,
@ColumnInfo(name = "position")
val position: Long, val position: Long,
@ColumnInfo(name = "parent_id")
val parentId: Long = -1L, val parentId: Long = -1L,
val index: Int,
@ColumnInfo(name = "mode")
val mode: Int, val mode: Int,
@ColumnInfo(name = "is_shuffling")
val isShuffling: Boolean, val isShuffling: Boolean,
val shuffleSeed: Long,
@ColumnInfo(name = "loop_mode")
val loopMode: Int, val loopMode: Int,
@ColumnInfo(name = "in_user_queue")
val inUserQueue: Boolean val inUserQueue: Boolean
) ) {
companion object {
const val COLUMN_ID = "state_id"
const val COLUMN_SONG_ID = "song_id"
const val COLUMN_POSITION = "position"
const val COLUMN_PARENT_ID = "parent_id"
const val COLUMN_INDEX = "state_index"
const val COLUMN_MODE = "mode"
const val COLUMN_IS_SHUFFLING = "is_shuffling"
const val COLUMN_SHUFFLE_SEED = "shuffle_seed"
const val COLUMN_LOOP_MODE = "loop_mode"
const val COLUMN_IN_USER_QUEUE = "is_user_queue"
}
}

View file

@ -1,20 +0,0 @@
package org.oxycblt.auxio.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface PlaybackStateDAO {
@Insert
fun insert(playbackState: PlaybackState)
@Query("SELECT * FROM playback_state_table")
fun getAll(): List<PlaybackState>
@Query("DELETE FROM playback_state_table")
fun clear()
@Query("SELECT * FROM playback_state_table ORDER BY id DESC LIMIT 1")
fun getRecent(): PlaybackState?
}

View file

@ -0,0 +1,261 @@
package org.oxycblt.auxio.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import android.util.Log
/**
* A bootstrapped SQLite database for managing the persistent playback state and queue.
*/
class PlaybackStateDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
createTable(db, TABLE_NAME_STATE)
createTable(db, TABLE_NAME_QUEUE)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME_STATE")
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME_QUEUE")
onCreate(db)
}
// --- DATABASE CONSTRUCTION FUNCTIONS ---
private fun createTable(database: SQLiteDatabase, tableName: String) {
val command = StringBuilder()
command.append("CREATE TABLE IF NOT EXISTS $tableName(")
if (tableName == TABLE_NAME_STATE) {
constructStateTable(command)
} else if (tableName == TABLE_NAME_QUEUE) {
constructQueueTable(command)
}
database.execSQL(command.toString())
}
private fun constructStateTable(command: StringBuilder): StringBuilder {
command.append("${PlaybackState.COLUMN_ID} LONG PRIMARY KEY,")
command.append("${PlaybackState.COLUMN_SONG_ID} LONG NOT NULL,")
command.append("${PlaybackState.COLUMN_POSITION} LONG NOT NULL,")
command.append("${PlaybackState.COLUMN_PARENT_ID} LONG NOT NULL,")
command.append("${PlaybackState.COLUMN_INDEX} INTEGER NOT NULL,")
command.append("${PlaybackState.COLUMN_MODE} INTEGER NOT NULL,")
command.append("${PlaybackState.COLUMN_IS_SHUFFLING} BOOLEAN NOT NULL,")
command.append("${PlaybackState.COLUMN_SHUFFLE_SEED} LONG NOT NULL,")
command.append("${PlaybackState.COLUMN_LOOP_MODE} INTEGER NOT NULL,")
command.append("${PlaybackState.COLUMN_IN_USER_QUEUE} BOOLEAN NOT NULL)")
return command
}
private fun constructQueueTable(command: StringBuilder): StringBuilder {
command.append("${QueueItem.COLUMN_ID} LONG PRIMARY KEY,")
command.append("${QueueItem.COLUMN_SONG_ID} LONG NOT NULL,")
command.append("${QueueItem.COLUMN_ALBUM_ID} LONG NOT NULL,")
command.append("${QueueItem.COLUMN_IS_USER_QUEUE} BOOLEAN NOT NULL)")
return command
}
// --- INTERFACE FUNCTIONS ---
fun writeState(state: PlaybackState) {
val database = writableDatabase
database.beginTransaction()
try {
database.delete(TABLE_NAME_STATE, null, null)
database.setTransactionSuccessful()
} finally {
database.endTransaction()
Log.d(this::class.simpleName, "Successfully wiped previous state.")
}
try {
database.beginTransaction()
val stateData = ContentValues(10)
stateData.put(PlaybackState.COLUMN_ID, state.id)
stateData.put(PlaybackState.COLUMN_SONG_ID, state.songId)
stateData.put(PlaybackState.COLUMN_POSITION, state.position)
stateData.put(PlaybackState.COLUMN_PARENT_ID, state.parentId)
stateData.put(PlaybackState.COLUMN_INDEX, state.index)
stateData.put(PlaybackState.COLUMN_MODE, state.mode)
stateData.put(PlaybackState.COLUMN_IS_SHUFFLING, state.isShuffling)
stateData.put(PlaybackState.COLUMN_SHUFFLE_SEED, state.shuffleSeed)
stateData.put(PlaybackState.COLUMN_LOOP_MODE, state.loopMode)
stateData.put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
database.insert(TABLE_NAME_STATE, null, stateData)
database.setTransactionSuccessful()
} finally {
database.endTransaction()
Log.d(this::class.simpleName, "Wrote state to database.")
}
}
fun readState(): PlaybackState? {
val database = writableDatabase
var state: PlaybackState? = null
var stateCursor: Cursor? = null
try {
stateCursor = database.query(TABLE_NAME_STATE, null, null, null, null, null, null)
stateCursor?.use { cursor ->
if (cursor.count == 0) return@use
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_ID)
val positionIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_POSITION)
val parentIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_PARENT_ID)
val indexIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_INDEX)
val modeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_MODE)
val isShufflingIndex =
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
val shuffleSeedIndex =
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SHUFFLE_SEED)
val loopModeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_LOOP_MODE)
val inUserQueueIndex =
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE)
cursor.moveToFirst()
state = PlaybackState(
songId = cursor.getLong(songIndex),
position = cursor.getLong(positionIndex),
parentId = cursor.getLong(parentIndex),
index = cursor.getInt(indexIndex),
mode = cursor.getInt(modeIndex),
isShuffling = cursor.getInt(isShufflingIndex) == 1,
shuffleSeed = cursor.getLong(shuffleSeedIndex),
loopMode = cursor.getInt(loopModeIndex),
inUserQueue = cursor.getInt(inUserQueueIndex) == 1
)
}
} finally {
stateCursor?.close()
return state
}
}
fun writeQueue(queue: List<QueueItem>) {
val database = readableDatabase
database.beginTransaction()
try {
database.delete(TABLE_NAME_QUEUE, null, null)
database.setTransactionSuccessful()
} finally {
database.endTransaction()
Log.d(this::class.simpleName, "Successfully wiped queue.")
}
Log.d(this::class.simpleName, "Writing to queue.")
var position = 0
while (position < queue.size) {
database.beginTransaction()
var i = position
try {
while (i < queue.size) {
val item = queue[i]
val itemData = ContentValues(4)
i++
itemData.put(QueueItem.COLUMN_ID, item.id)
itemData.put(QueueItem.COLUMN_SONG_ID, item.songId)
itemData.put(QueueItem.COLUMN_ALBUM_ID, item.albumId)
itemData.put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
database.insert(TABLE_NAME_QUEUE, null, itemData)
}
database.setTransactionSuccessful()
} finally {
database.endTransaction()
// Update the position at the end, if an insert failed at any point, then
// the next iteration should skip it.
position = i
Log.d(this::class.simpleName, "Wrote batch of $position songs.")
}
}
}
fun readQueue(): List<QueueItem> {
val database = readableDatabase
val queueItems = mutableListOf<QueueItem>()
var queueCursor: Cursor? = null
try {
queueCursor = database.query(TABLE_NAME_QUEUE, null, null, null, null, null, null)
queueCursor?.use { cursor ->
if (cursor.count == 0) return@use
val idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_ID)
val albumIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ALBUM_ID)
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
val songId = cursor.getLong(songIdIndex)
val albumId = cursor.getLong(albumIdIndex)
val isUserQueue = cursor.getInt(isUserQueueIndex) == 1
queueItems.add(
QueueItem(id, songId, albumId, isUserQueue)
)
}
}
} finally {
queueCursor?.close()
return queueItems
}
}
companion object {
const val DB_VERSION = 1
const val DB_NAME = "auxio_state_database"
const val TABLE_NAME_STATE = "playback_state_table"
const val TABLE_NAME_QUEUE = "queue_table"
@Volatile
private var INSTANCE: PlaybackStateDatabase? = null
/**
* Get/Instantiate the single instance of [PlaybackStateDatabase].
*/
fun getInstance(context: Context): PlaybackStateDatabase {
val currentInstance = INSTANCE
if (currentInstance != null) {
return currentInstance
}
synchronized(this) {
val newInstance = PlaybackStateDatabase(context.applicationContext)
INSTANCE = newInstance
return newInstance
}
}
}
}

View file

@ -1,29 +0,0 @@
package org.oxycblt.auxio.database
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
@Dao
interface QueueDAO {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insert(item: QueueItem)
@Transaction
suspend fun insertAll(items: List<QueueItem>) {
items.forEach {
insert(it)
}
}
@Query("SELECT * FROM queue_table WHERE is_user_queue == 1")
fun getUserQueue(): List<QueueItem>
@Query("SELECT * FROM queue_table WHERE is_user_queue == 0")
fun getQueue(): List<QueueItem>
@Query("DELETE FROM queue_table")
fun clear()
}

View file

@ -1,23 +1,15 @@
package org.oxycblt.auxio.database package org.oxycblt.auxio.database
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
/**
* A simplified database entity that represents a given song in the queue.
*/
@Entity(tableName = "queue_table")
data class QueueItem( data class QueueItem(
@PrimaryKey(autoGenerate = true)
var id: Long = 0L, var id: Long = 0L,
@ColumnInfo(name = "song_id")
val songId: Long = Long.MIN_VALUE, val songId: Long = Long.MIN_VALUE,
@ColumnInfo(name = "album_id")
val albumId: Long = Long.MIN_VALUE, val albumId: Long = Long.MIN_VALUE,
@ColumnInfo(name = "is_user_queue")
val isUserQueue: Boolean = false val isUserQueue: Boolean = false
) ) {
companion object {
const val COLUMN_ID = "id"
const val COLUMN_SONG_ID = "song_id"
const val COLUMN_ALBUM_ID = "album_id"
const val COLUMN_IS_USER_QUEUE = "is_user_queue"
}
}

View file

@ -121,4 +121,5 @@ data class Genre(
data class Header( data class Header(
override val id: Long = -1, override val id: Long = -1,
override var name: String = "", override var name: String = "",
val isAction: Boolean = false
) : BaseModel() ) : BaseModel()

View file

@ -126,8 +126,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
} else { } else {
binding.playbackShuffle.imageTintList = controlColor binding.playbackShuffle.imageTintList = controlColor
} }
Log.d(this::class.simpleName, "Shuffle swap")
} }
playbackModel.loopMode.observe(viewLifecycleOwner) { playbackModel.loopMode.observe(viewLifecycleOwner) {

View file

@ -44,7 +44,7 @@ class QueueAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) { return when (viewType) {
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
QUEUE_ITEM_TYPE -> ViewHolder( QUEUE_ITEM_TYPE -> QueueSongViewHolder(
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context)) ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
) )
else -> error("Someone messed with the ViewHolder item types.") else -> error("Someone messed with the ViewHolder item types.")
@ -53,7 +53,7 @@ class QueueAdapter(
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = data[position]) { when (val item = data[position]) {
is Song -> (holder as ViewHolder).bind(item) is Song -> (holder as QueueSongViewHolder).bind(item)
is Header -> (holder as HeaderViewHolder).bind(item) is Header -> (holder as HeaderViewHolder).bind(item)
else -> { else -> {
@ -99,7 +99,7 @@ class QueueAdapter(
} }
// Generic ViewHolder for a queue item // Generic ViewHolder for a queue item
inner class ViewHolder( inner class QueueSongViewHolder(
private val binding: ItemQueueSongBinding, private val binding: ItemQueueSongBinding,
) : BaseViewHolder<Song>(binding, null, null) { ) : BaseViewHolder<Song>(binding, null, null) {

View file

@ -17,7 +17,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
viewHolder: RecyclerView.ViewHolder viewHolder: RecyclerView.ViewHolder
): Int { ): Int {
// Only allow dragging/swiping with the queue item ViewHolder, not the headers. // Only allow dragging/swiping with the queue item ViewHolder, not the headers.
return if (viewHolder is QueueAdapter.ViewHolder) { return if (viewHolder is QueueAdapter.QueueSongViewHolder) {
makeFlag( makeFlag(
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) ) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)

View file

@ -75,7 +75,12 @@ class QueueFragment : Fragment() {
val queue = mutableListOf<BaseModel>() val queue = mutableListOf<BaseModel>()
if (playbackModel.userQueue.value!!.isNotEmpty()) { if (playbackModel.userQueue.value!!.isNotEmpty()) {
queue.add(Header(name = getString(R.string.label_next_user_queue))) queue.add(
Header(
name = getString(R.string.label_next_user_queue),
isAction = true
)
)
queue.addAll(playbackModel.userQueue.value!!) queue.addAll(playbackModel.userQueue.value!!)
} }
@ -88,7 +93,8 @@ class QueueFragment : Fragment() {
getString(R.string.label_all_songs) getString(R.string.label_all_songs)
else else
playbackModel.parent.value!!.name playbackModel.parent.value!!.name
) ),
isAction = false
) )
) )
queue.addAll(playbackModel.nextItemsInQueue.value!!) queue.addAll(playbackModel.nextItemsInQueue.value!!)

View file

@ -4,8 +4,8 @@ import android.content.Context
import android.util.Log import android.util.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.oxycblt.auxio.database.AuxioDatabase
import org.oxycblt.auxio.database.PlaybackState import org.oxycblt.auxio.database.PlaybackState
import org.oxycblt.auxio.database.PlaybackStateDatabase
import org.oxycblt.auxio.database.QueueItem import org.oxycblt.auxio.database.QueueItem
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
@ -436,12 +436,9 @@ class PlaybackStateManager private constructor() {
val playbackState = packToPlaybackState() val playbackState = packToPlaybackState()
val queueItems = packQueue() val queueItems = packQueue()
val database = AuxioDatabase.getInstance(context) val database = PlaybackStateDatabase.getInstance(context)
database.playbackStateDAO.clear() database.writeState(playbackState)
database.queueDAO.clear() database.writeQueue(queueItems)
database.playbackStateDAO.insert(playbackState)
database.queueDAO.insertAll(queueItems)
} }
val time = System.currentTimeMillis() - start val time = System.currentTimeMillis() - start
@ -455,42 +452,24 @@ class PlaybackStateManager private constructor() {
val start = System.currentTimeMillis() val start = System.currentTimeMillis()
val state: PlaybackState? val state: PlaybackState?
val queueItems: List<QueueItem>
val userQueueItems: List<QueueItem>
withContext(Dispatchers.IO) { val queueItems = withContext(Dispatchers.IO) {
val database = AuxioDatabase.getInstance(context) val database = PlaybackStateDatabase.getInstance(context)
state = database.playbackStateDAO.getRecent() state = database.readState()
queueItems = database.queueDAO.getQueue() database.readQueue()
userQueueItems = database.queueDAO.getUserQueue()
database.playbackStateDAO.clear()
database.queueDAO.clear()
} }
val loadTime = System.currentTimeMillis() - start val loadTime = System.currentTimeMillis() - start
Log.d(this::class.simpleName, "Load finished in ${loadTime}ms") Log.d(this::class.simpleName, "Load finished in ${loadTime}ms")
if (state == null) { state?.let {
Log.d(this::class.simpleName, "Nothing here. Not restoring.") Log.d(this::class.simpleName, "Valid playback state $it")
Log.d(this::class.simpleName, "Valid queue size ${queueItems.size}")
mIsRestored = true unpackFromPlaybackState(it)
unpackQueue(queueItems)
return
}
Log.d(this::class.simpleName, "Old state found, $state")
unpackFromPlaybackState(state)
Log.d(this::class.simpleName, "Found queue of size ${queueItems.size}")
unpackQueues(queueItems, userQueueItems)
mSong?.let {
mIndex = mQueue.indexOf(mSong)
} }
val time = System.currentTimeMillis() - start val time = System.currentTimeMillis() - start
@ -510,8 +489,10 @@ class PlaybackStateManager private constructor() {
songId = songId, songId = songId,
position = mPosition, position = mPosition,
parentId = parentId, parentId = parentId,
index = mIndex,
mode = intMode, mode = intMode,
isShuffling = mIsShuffling, isShuffling = mIsShuffling,
shuffleSeed = mShuffleSeed,
loopMode = intLoopMode, loopMode = intLoopMode,
inUserQueue = mIsInUserQueue inUserQueue = mIsInUserQueue
) )
@ -520,12 +501,16 @@ class PlaybackStateManager private constructor() {
private fun packQueue(): List<QueueItem> { private fun packQueue(): List<QueueItem> {
val unified = mutableListOf<QueueItem>() val unified = mutableListOf<QueueItem>()
var queueItemId = 0L
mUserQueue.forEach { mUserQueue.forEach {
unified.add(QueueItem(songId = it.id, albumId = it.albumId, isUserQueue = true)) unified.add(QueueItem(queueItemId, it.id, it.albumId, true))
queueItemId++
} }
mQueue.forEach { mQueue.forEach {
unified.add(QueueItem(songId = it.id, albumId = it.albumId, isUserQueue = false)) unified.add(QueueItem(queueItemId, it.id, it.albumId, false))
queueItemId++
} }
return unified return unified
@ -541,7 +526,9 @@ class PlaybackStateManager private constructor() {
mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS
mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE
mIsShuffling = playbackState.isShuffling mIsShuffling = playbackState.isShuffling
mShuffleSeed = playbackState.shuffleSeed
mIsInUserQueue = playbackState.inUserQueue mIsInUserQueue = playbackState.inUserQueue
mIndex = playbackState.index
callbacks.forEach { callbacks.forEach {
it.onSeekConfirm(mPosition) it.onSeekConfirm(mPosition)
@ -550,21 +537,26 @@ class PlaybackStateManager private constructor() {
} }
} }
private fun unpackQueues(queueItems: List<QueueItem>, userQueueItems: List<QueueItem>) { private fun unpackQueue(queueItems: List<QueueItem>) {
val musicStore = MusicStore.getInstance() val musicStore = MusicStore.getInstance()
queueItems.forEach { item -> queueItems.forEach { item ->
// Traverse albums and then album songs instead of just the songs, as its faster. // Traverse albums and then album songs instead of just the songs, as its faster.
musicStore.albums.find { it.id == item.albumId } musicStore.albums.find { it.id == item.albumId }
?.songs?.find { it.id == item.songId }?.let { ?.songs?.find { it.id == item.songId }?.let {
mQueue.add(it) if (item.isUserQueue) {
mUserQueue.add(it)
} else {
mQueue.add(it)
}
} }
} }
userQueueItems.forEach { item -> // Get a more accurate index [At least if were not in the user queue]
musicStore.albums.find { it.id == item.albumId } if (!mIsInUserQueue) {
?.songs?.find { it.id == item.songId }?.let { mSong?.let {
mUserQueue.add(it) val index = mQueue.indexOf(it)
mIndex = if (index != -1) index else mIndex
} }
} }

View file

@ -129,7 +129,7 @@ class SongViewHolder private constructor(
} }
} }
class HeaderViewHolder( open class HeaderViewHolder(
private val binding: ItemHeaderBinding private val binding: ItemHeaderBinding
) : BaseViewHolder<Header>(binding, null, null) { ) : BaseViewHolder<Header>(binding, null, null) {

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorPrimary">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z" />
</vector>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/background"/>
</shape>
</item>
<item>
<ripple
android:color="@color/selection_color">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="@color/selection_color" />
</shape>
</item>
</ripple>
</item>
</layer-list>

View file

@ -5,7 +5,8 @@ https://stackoverflow.com/a/61157571/14143986
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:left="-2dp" android:left="-2dp"
android:right="-2dp"> android:right="-2dp"
android:top="-2dp">
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<stroke <stroke
android:width="0.5dp" android:width="0.5dp"

View file

@ -15,11 +15,9 @@
type="org.oxycblt.auxio.playback.PlaybackViewModel" /> type="org.oxycblt.auxio.playback.PlaybackViewModel" />
</data> </data>
<!-- TODO: Fix elevation not showing -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:background="@drawable/ui_ripple" android:background="@drawable/ui_background_ripple"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -4,6 +4,8 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainFragment"> tools:context=".MainFragment">
<!-- TODO: Fix elevation -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_layout" android:id="@+id/main_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -16,7 +18,6 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:elevation="@dimen/elevation_normal"
app:layout_constraintBottom_toTopOf="@+id/compact_playback" app:layout_constraintBottom_toTopOf="@+id/compact_playback"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_explore" app:navGraph="@navigation/nav_explore"
@ -28,6 +29,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
android:outlineProvider="bounds"
app:layout_constraintBottom_toTopOf="@+id/nav_bar" app:layout_constraintBottom_toTopOf="@+id/nav_bar"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -38,7 +40,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/background" android:background="@color/background"
android:elevation="@dimen/elevation_normal"
app:itemRippleColor="@color/selection_color" app:itemRippleColor="@color/selection_color"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -46,7 +46,7 @@
android:layout_margin="@dimen/margin_mid_large" android:layout_margin="@dimen/margin_mid_large"
android:contentDescription="@{@string/description_album_cover(song.name)}" android:contentDescription="@{@string/description_album_cover(song.name)}"
android:outlineProvider="bounds" android:outlineProvider="bounds"
android:elevation="2dp" android:elevation="@dimen/elevation_normal"
app:coverArt="@{song}" app:coverArt="@{song}"
app:layout_constraintBottom_toTopOf="@+id/playback_song" app:layout_constraintBottom_toTopOf="@+id/playback_song"
app:layout_constraintDimensionRatio="1:1" app:layout_constraintDimensionRatio="1:1"

View file

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".recycler.viewholders.HeaderViewHolder">
<data>
<variable
name="header"
type="org.oxycblt.auxio.music.Header" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/header_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold"
android:paddingStart="@dimen/padding_medium"
android:paddingTop="@dimen/padding_small"
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:text="@{header.name}"
android:textColor="?android:attr/textColorPrimary"
android:textSize="19sp"
app:layout_constraintEnd_toStartOf="@+id/header_action_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Songs" />
<ImageButton
android:id="@+id/header_action_button"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
android:src="@drawable/ic_clear"
tools:visibility="visible"
android:background="@drawable/ui_header_dividers"
app:layout_constraintBottom_toBottomOf="@+id/header_text"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="@+id/header_text"
tools:ignore="contentDescription" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -10,23 +10,17 @@
type="org.oxycblt.auxio.music.Header" /> type="org.oxycblt.auxio.music.Header" />
</data> </data>
<FrameLayout <TextView
android:id="@+id/header_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
<TextView android:fontFamily="@font/inter_semibold"
android:id="@+id/header_text" android:paddingStart="@dimen/padding_medium"
android:layout_width="match_parent" android:paddingTop="@dimen/padding_small"
android:layout_height="wrap_content" android:paddingEnd="@dimen/padding_small"
android:textColor="?android:attr/textColorPrimary" android:paddingBottom="@dimen/padding_small"
android:fontFamily="@font/inter_semibold" android:textSize="19sp"
android:paddingStart="@dimen/padding_medium" android:text="@{header.name}"
android:paddingTop="@dimen/padding_small" tools:text="Songs" />
android:paddingEnd="@dimen/padding_small"
android:paddingBottom="@dimen/padding_small"
android:textSize="19sp"
android:text="@{header.name}"
tools:text="Songs" />
</FrameLayout>
</layout> </layout>