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:
parent
d8a40fe219
commit
49897c53b4
22 changed files with 436 additions and 208 deletions
|
@ -72,12 +72,6 @@ dependencies {
|
|||
// Media
|
||||
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 ---
|
||||
|
||||
// Image loading
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +1,27 @@
|
|||
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(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = Long.MIN_VALUE,
|
||||
|
||||
@ColumnInfo(name = "song_id")
|
||||
val songId: Long = Long.MIN_VALUE,
|
||||
|
||||
@ColumnInfo(name = "position")
|
||||
val id: Long = 0L,
|
||||
val songId: Long = -1L,
|
||||
val position: Long,
|
||||
|
||||
@ColumnInfo(name = "parent_id")
|
||||
val parentId: Long = -1L,
|
||||
|
||||
@ColumnInfo(name = "mode")
|
||||
val index: Int,
|
||||
val mode: Int,
|
||||
|
||||
@ColumnInfo(name = "is_shuffling")
|
||||
val isShuffling: Boolean,
|
||||
|
||||
@ColumnInfo(name = "loop_mode")
|
||||
val shuffleSeed: Long,
|
||||
val loopMode: Int,
|
||||
|
||||
@ColumnInfo(name = "in_user_queue")
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -1,23 +1,15 @@
|
|||
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(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0L,
|
||||
|
||||
@ColumnInfo(name = "song_id")
|
||||
val songId: Long = Long.MIN_VALUE,
|
||||
|
||||
@ColumnInfo(name = "album_id")
|
||||
val albumId: Long = Long.MIN_VALUE,
|
||||
|
||||
@ColumnInfo(name = "is_user_queue")
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -121,4 +121,5 @@ data class Genre(
|
|||
data class Header(
|
||||
override val id: Long = -1,
|
||||
override var name: String = "",
|
||||
val isAction: Boolean = false
|
||||
) : BaseModel()
|
||||
|
|
|
@ -126,8 +126,6 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
} else {
|
||||
binding.playbackShuffle.imageTintList = controlColor
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, "Shuffle swap")
|
||||
}
|
||||
|
||||
playbackModel.loopMode.observe(viewLifecycleOwner) {
|
||||
|
|
|
@ -44,7 +44,7 @@ class QueueAdapter(
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context)
|
||||
QUEUE_ITEM_TYPE -> ViewHolder(
|
||||
QUEUE_ITEM_TYPE -> QueueSongViewHolder(
|
||||
ItemQueueSongBinding.inflate(LayoutInflater.from(parent.context))
|
||||
)
|
||||
else -> error("Someone messed with the ViewHolder item types.")
|
||||
|
@ -53,7 +53,7 @@ class QueueAdapter(
|
|||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
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)
|
||||
|
||||
else -> {
|
||||
|
@ -99,7 +99,7 @@ class QueueAdapter(
|
|||
}
|
||||
|
||||
// Generic ViewHolder for a queue item
|
||||
inner class ViewHolder(
|
||||
inner class QueueSongViewHolder(
|
||||
private val binding: ItemQueueSongBinding,
|
||||
) : BaseViewHolder<Song>(binding, null, null) {
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ class QueueDragCallback(private val playbackModel: PlaybackViewModel) : ItemTouc
|
|||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
// 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(
|
||||
ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START)
|
||||
|
|
|
@ -75,7 +75,12 @@ class QueueFragment : Fragment() {
|
|||
val queue = mutableListOf<BaseModel>()
|
||||
|
||||
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!!)
|
||||
}
|
||||
|
||||
|
@ -88,7 +93,8 @@ class QueueFragment : Fragment() {
|
|||
getString(R.string.label_all_songs)
|
||||
else
|
||||
playbackModel.parent.value!!.name
|
||||
)
|
||||
),
|
||||
isAction = false
|
||||
)
|
||||
)
|
||||
queue.addAll(playbackModel.nextItemsInQueue.value!!)
|
||||
|
|
|
@ -4,8 +4,8 @@ import android.content.Context
|
|||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.database.AuxioDatabase
|
||||
import org.oxycblt.auxio.database.PlaybackState
|
||||
import org.oxycblt.auxio.database.PlaybackStateDatabase
|
||||
import org.oxycblt.auxio.database.QueueItem
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
|
@ -436,12 +436,9 @@ class PlaybackStateManager private constructor() {
|
|||
val playbackState = packToPlaybackState()
|
||||
val queueItems = packQueue()
|
||||
|
||||
val database = AuxioDatabase.getInstance(context)
|
||||
database.playbackStateDAO.clear()
|
||||
database.queueDAO.clear()
|
||||
|
||||
database.playbackStateDAO.insert(playbackState)
|
||||
database.queueDAO.insertAll(queueItems)
|
||||
val database = PlaybackStateDatabase.getInstance(context)
|
||||
database.writeState(playbackState)
|
||||
database.writeQueue(queueItems)
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - start
|
||||
|
@ -455,42 +452,24 @@ class PlaybackStateManager private constructor() {
|
|||
val start = System.currentTimeMillis()
|
||||
|
||||
val state: PlaybackState?
|
||||
val queueItems: List<QueueItem>
|
||||
val userQueueItems: List<QueueItem>
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val database = AuxioDatabase.getInstance(context)
|
||||
val queueItems = withContext(Dispatchers.IO) {
|
||||
val database = PlaybackStateDatabase.getInstance(context)
|
||||
|
||||
state = database.playbackStateDAO.getRecent()
|
||||
queueItems = database.queueDAO.getQueue()
|
||||
userQueueItems = database.queueDAO.getUserQueue()
|
||||
|
||||
database.playbackStateDAO.clear()
|
||||
database.queueDAO.clear()
|
||||
state = database.readState()
|
||||
database.readQueue()
|
||||
}
|
||||
|
||||
val loadTime = System.currentTimeMillis() - start
|
||||
|
||||
Log.d(this::class.simpleName, "Load finished in ${loadTime}ms")
|
||||
|
||||
if (state == null) {
|
||||
Log.d(this::class.simpleName, "Nothing here. Not restoring.")
|
||||
state?.let {
|
||||
Log.d(this::class.simpleName, "Valid playback state $it")
|
||||
Log.d(this::class.simpleName, "Valid queue size ${queueItems.size}")
|
||||
|
||||
mIsRestored = true
|
||||
|
||||
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)
|
||||
unpackFromPlaybackState(it)
|
||||
unpackQueue(queueItems)
|
||||
}
|
||||
|
||||
val time = System.currentTimeMillis() - start
|
||||
|
@ -510,8 +489,10 @@ class PlaybackStateManager private constructor() {
|
|||
songId = songId,
|
||||
position = mPosition,
|
||||
parentId = parentId,
|
||||
index = mIndex,
|
||||
mode = intMode,
|
||||
isShuffling = mIsShuffling,
|
||||
shuffleSeed = mShuffleSeed,
|
||||
loopMode = intLoopMode,
|
||||
inUserQueue = mIsInUserQueue
|
||||
)
|
||||
|
@ -520,12 +501,16 @@ class PlaybackStateManager private constructor() {
|
|||
private fun packQueue(): List<QueueItem> {
|
||||
val unified = mutableListOf<QueueItem>()
|
||||
|
||||
var queueItemId = 0L
|
||||
|
||||
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 {
|
||||
unified.add(QueueItem(songId = it.id, albumId = it.albumId, isUserQueue = false))
|
||||
unified.add(QueueItem(queueItemId, it.id, it.albumId, false))
|
||||
queueItemId++
|
||||
}
|
||||
|
||||
return unified
|
||||
|
@ -541,7 +526,9 @@ class PlaybackStateManager private constructor() {
|
|||
mMode = PlaybackMode.fromConstant(playbackState.mode) ?: PlaybackMode.ALL_SONGS
|
||||
mLoopMode = LoopMode.fromConstant(playbackState.loopMode) ?: LoopMode.NONE
|
||||
mIsShuffling = playbackState.isShuffling
|
||||
mShuffleSeed = playbackState.shuffleSeed
|
||||
mIsInUserQueue = playbackState.inUserQueue
|
||||
mIndex = playbackState.index
|
||||
|
||||
callbacks.forEach {
|
||||
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()
|
||||
|
||||
queueItems.forEach { item ->
|
||||
// Traverse albums and then album songs instead of just the songs, as its faster.
|
||||
musicStore.albums.find { it.id == item.albumId }
|
||||
?.songs?.find { it.id == item.songId }?.let {
|
||||
mQueue.add(it)
|
||||
if (item.isUserQueue) {
|
||||
mUserQueue.add(it)
|
||||
} else {
|
||||
mQueue.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userQueueItems.forEach { item ->
|
||||
musicStore.albums.find { it.id == item.albumId }
|
||||
?.songs?.find { it.id == item.songId }?.let {
|
||||
mUserQueue.add(it)
|
||||
// Get a more accurate index [At least if were not in the user queue]
|
||||
if (!mIsInUserQueue) {
|
||||
mSong?.let {
|
||||
val index = mQueue.indexOf(it)
|
||||
mIndex = if (index != -1) index else mIndex
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -129,7 +129,7 @@ class SongViewHolder private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder(
|
||||
open class HeaderViewHolder(
|
||||
private val binding: ItemHeaderBinding
|
||||
) : BaseViewHolder<Header>(binding, null, null) {
|
||||
|
||||
|
|
11
app/src/main/res/drawable/ic_clear.xml
Normal file
11
app/src/main/res/drawable/ic_clear.xml
Normal 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>
|
19
app/src/main/res/drawable/ui_background_ripple.xml
Normal file
19
app/src/main/res/drawable/ui_background_ripple.xml
Normal 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>
|
|
@ -5,7 +5,8 @@ https://stackoverflow.com/a/61157571/14143986
|
|||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:left="-2dp"
|
||||
android:right="-2dp">
|
||||
android:right="-2dp"
|
||||
android:top="-2dp">
|
||||
<shape android:shape="rectangle">
|
||||
<stroke
|
||||
android:width="0.5dp"
|
||||
|
|
|
@ -15,11 +15,9 @@
|
|||
type="org.oxycblt.auxio.playback.PlaybackViewModel" />
|
||||
</data>
|
||||
|
||||
<!-- TODO: Fix elevation not showing -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:animateLayoutChanges="true"
|
||||
android:background="@drawable/ui_ripple"
|
||||
android:background="@drawable/ui_background_ripple"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".MainFragment">
|
||||
|
||||
<!-- TODO: Fix elevation -->
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/main_layout"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -16,7 +18,6 @@
|
|||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:layout_constraintBottom_toTopOf="@+id/compact_playback"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/nav_explore"
|
||||
|
@ -28,6 +29,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:outlineProvider="bounds"
|
||||
app:layout_constraintBottom_toTopOf="@+id/nav_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -38,7 +40,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:itemRippleColor="@color/selection_color"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
android:layout_margin="@dimen/margin_mid_large"
|
||||
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||
android:outlineProvider="bounds"
|
||||
android:elevation="2dp"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:coverArt="@{song}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/playback_song"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
|
|
52
app/src/main/res/layout/item_action_header.xml
Normal file
52
app/src/main/res/layout/item_action_header.xml
Normal 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>
|
|
@ -10,23 +10,17 @@
|
|||
type="org.oxycblt.auxio.music.Header" />
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
<TextView
|
||||
android:id="@+id/header_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/header_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
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:textSize="19sp"
|
||||
android:text="@{header.name}"
|
||||
tools:text="Songs" />
|
||||
|
||||
</FrameLayout>
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
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:textSize="19sp"
|
||||
android:text="@{header.name}"
|
||||
tools:text="Songs" />
|
||||
</layout>
|
Loading…
Reference in a new issue