Refactor database code
Simplify and refactor alot of the database code, along with extracting some pieces of code into actual utilities.
This commit is contained in:
parent
72877f77ee
commit
9a02eadc95
3 changed files with 133 additions and 153 deletions
|
@ -10,8 +10,9 @@ import java.io.IOException
|
|||
|
||||
/**
|
||||
* Database for storing blacklisted paths.
|
||||
* Note that the paths stored here will not work with MediaStore unless you append a "%" at the
|
||||
* end.
|
||||
* Note that the paths stored here will not work with MediaStore unless you append a "%" at the end.
|
||||
* Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
||||
override fun onCreate(db: SQLiteDatabase) {
|
||||
|
@ -43,43 +44,29 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
|
|||
return false
|
||||
}
|
||||
|
||||
val database = writableDatabase
|
||||
database.beginTransaction()
|
||||
|
||||
try {
|
||||
writableDatabase.execute {
|
||||
val values = ContentValues(1)
|
||||
values.put(COLUMN_PATH, path)
|
||||
|
||||
database.insert(TABLE_NAME, null, values)
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
|
||||
return true
|
||||
insert(TABLE_NAME, null, values)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a [File] from this blacklist.
|
||||
*/
|
||||
fun removePath(file: File) {
|
||||
val database = writableDatabase
|
||||
val path = file.mediaStorePath
|
||||
|
||||
database.beginTransaction()
|
||||
database.delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(path))
|
||||
database.setTransactionSuccessful()
|
||||
database.endTransaction()
|
||||
writableDatabase.execute {
|
||||
delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(file.mediaStorePath))
|
||||
}
|
||||
}
|
||||
|
||||
fun getPaths(): List<String> {
|
||||
val paths = mutableListOf<String>()
|
||||
|
||||
val pathsCursor = readableDatabase.query(
|
||||
TABLE_NAME, arrayOf(COLUMN_PATH), null, null, null, null, null
|
||||
)
|
||||
|
||||
pathsCursor?.use { cursor ->
|
||||
readableDatabase.queryAll(TABLE_NAME) { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
paths.add(cursor.getString(0))
|
||||
}
|
||||
|
@ -89,19 +76,11 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
|
|||
}
|
||||
|
||||
private fun hasFile(path: String): Boolean {
|
||||
val pathsCursor = readableDatabase.query(
|
||||
TABLE_NAME,
|
||||
arrayOf(COLUMN_PATH),
|
||||
"$COLUMN_PATH=?",
|
||||
arrayOf(path),
|
||||
null, null, null, null
|
||||
)
|
||||
|
||||
pathsCursor?.use { cursor ->
|
||||
return cursor.moveToFirst()
|
||||
val exists = readableDatabase.queryUse(TABLE_NAME, null, "$COLUMN_PATH=?", path) { cursor ->
|
||||
cursor.moveToFirst()
|
||||
}
|
||||
|
||||
return false
|
||||
return exists ?: false
|
||||
}
|
||||
|
||||
private val File.mediaStorePath: String get() {
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
package org.oxycblt.auxio.database
|
||||
|
||||
import android.database.Cursor
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.os.Looper
|
||||
import org.oxycblt.auxio.logE
|
||||
import java.lang.Exception
|
||||
|
||||
/**
|
||||
* Shortcut for running a series of [commands] on an [SQLiteDatabase].
|
||||
* @return true if the transaction was successful, false if not.
|
||||
*/
|
||||
fun SQLiteDatabase.execute(commands: SQLiteDatabase.() -> Unit): Boolean {
|
||||
beginTransaction()
|
||||
|
||||
val success = try {
|
||||
commands()
|
||||
setTransactionSuccessful()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logE("An error occurred when trying to execute commands.")
|
||||
logE(e.stackTraceToString())
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
endTransaction()
|
||||
|
||||
return success
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for running a query on this database and then running [block] with the cursor returned.
|
||||
* Will not run if the cursor is null.
|
||||
*/
|
||||
fun <R> SQLiteDatabase.queryUse(
|
||||
tableName: String,
|
||||
columns: Array<String>?,
|
||||
selection: String?,
|
||||
vararg args: String,
|
||||
block: (Cursor) -> R
|
||||
) = query(tableName, columns, selection, args, null, null, null, null)?.use(block)
|
||||
|
||||
/**
|
||||
* Shortcut for querying all items in a database and running [block] with the cursor returned.
|
||||
* Will not run if the cursor is null.
|
||||
*/
|
||||
fun <R> SQLiteDatabase.queryAll(tableName: String, block: (Cursor) -> R) =
|
||||
query(tableName, null, null, null, null, null, null)?.use(block)
|
||||
|
||||
/**
|
||||
* Assert that we are on a background thread.
|
||||
*/
|
||||
fun assertBackgroundThread() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
error("Not on a background thread.")
|
||||
}
|
||||
}
|
|
@ -4,14 +4,12 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.database.sqlite.SQLiteOpenHelper
|
||||
import android.os.Looper
|
||||
import androidx.core.database.getStringOrNull
|
||||
import org.oxycblt.auxio.logD
|
||||
|
||||
/**
|
||||
* A SQLite database for managing the persistent playback state and queue.
|
||||
* Yes, I know androidx has Room which supposedly makes database creation easier, but it also
|
||||
* has a crippling bug where it will endlessly allocate rows even if you clear the entire db, so...
|
||||
* Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class PlaybackStateDatabase(context: Context) :
|
||||
|
@ -86,24 +84,12 @@ class PlaybackStateDatabase(context: Context) :
|
|||
fun writeState(state: PlaybackState) {
|
||||
assertBackgroundThread()
|
||||
|
||||
val database = writableDatabase
|
||||
database.beginTransaction()
|
||||
writableDatabase.execute {
|
||||
delete(TABLE_NAME_STATE, null, null)
|
||||
|
||||
try {
|
||||
database.delete(TABLE_NAME_STATE, null, null)
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
this@PlaybackStateDatabase.logD("Wiped state db.")
|
||||
|
||||
logD("Successfully wiped previous state.")
|
||||
}
|
||||
|
||||
try {
|
||||
database.beginTransaction()
|
||||
|
||||
val stateData = ContentValues(9)
|
||||
|
||||
stateData.apply {
|
||||
val stateData = ContentValues(9).apply {
|
||||
put(PlaybackState.COLUMN_ID, state.id)
|
||||
put(PlaybackState.COLUMN_SONG_NAME, state.songName)
|
||||
put(PlaybackState.COLUMN_POSITION, state.position)
|
||||
|
@ -115,13 +101,10 @@ class PlaybackStateDatabase(context: Context) :
|
|||
put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
|
||||
}
|
||||
|
||||
database.insert(TABLE_NAME_STATE, null, stateData)
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
|
||||
logD("Wrote state to database.")
|
||||
insert(TABLE_NAME_STATE, null, stateData)
|
||||
}
|
||||
|
||||
logD("Wrote state to database.")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,48 +114,37 @@ class PlaybackStateDatabase(context: Context) :
|
|||
fun readState(): PlaybackState? {
|
||||
assertBackgroundThread()
|
||||
|
||||
val database = writableDatabase
|
||||
var state: PlaybackState? = null
|
||||
|
||||
try {
|
||||
val stateCursor = database.query(
|
||||
TABLE_NAME_STATE,
|
||||
null, null, null,
|
||||
null, null, null
|
||||
readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
|
||||
if (cursor.count == 0) return@queryAll
|
||||
|
||||
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_NAME)
|
||||
val posIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_POSITION)
|
||||
val parentIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_PARENT_NAME)
|
||||
val indexIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_INDEX)
|
||||
val modeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_MODE)
|
||||
val shuffleIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
|
||||
val loopModeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_LOOP_MODE)
|
||||
val inUserQueueIndex = cursor.getColumnIndexOrThrow(
|
||||
PlaybackState.COLUMN_IN_USER_QUEUE
|
||||
)
|
||||
|
||||
stateCursor?.use { cursor ->
|
||||
// Don't bother if the cursor [and therefore database] has nothing in it.
|
||||
if (cursor.count == 0) return@use
|
||||
cursor.moveToFirst()
|
||||
|
||||
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_NAME)
|
||||
val positionIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_POSITION)
|
||||
val parentIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_PARENT_NAME)
|
||||
val indexIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_INDEX)
|
||||
val modeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_MODE)
|
||||
val isShufflingIndex =
|
||||
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
|
||||
val loopModeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_LOOP_MODE)
|
||||
val inUserQueueIndex =
|
||||
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE)
|
||||
|
||||
// If there is something in it, get the first item from it, ignoring anything else.
|
||||
cursor.moveToFirst()
|
||||
|
||||
state = PlaybackState(
|
||||
songName = cursor.getStringOrNull(songIndex) ?: "",
|
||||
position = cursor.getLong(positionIndex),
|
||||
parentName = cursor.getStringOrNull(parentIndex) ?: "",
|
||||
index = cursor.getInt(indexIndex),
|
||||
mode = cursor.getInt(modeIndex),
|
||||
isShuffling = cursor.getInt(isShufflingIndex) == 1,
|
||||
loopMode = cursor.getInt(loopModeIndex),
|
||||
inUserQueue = cursor.getInt(inUserQueueIndex) == 1
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
return state
|
||||
state = PlaybackState(
|
||||
songName = cursor.getStringOrNull(songIndex) ?: "",
|
||||
position = cursor.getLong(posIndex),
|
||||
parentName = cursor.getStringOrNull(parentIndex) ?: "",
|
||||
index = cursor.getInt(indexIndex),
|
||||
mode = cursor.getInt(modeIndex),
|
||||
isShuffling = cursor.getInt(shuffleIndex) == 1,
|
||||
loopMode = cursor.getInt(loopModeIndex),
|
||||
inUserQueue = cursor.getInt(inUserQueueIndex) == 1
|
||||
)
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,54 +153,41 @@ class PlaybackStateDatabase(context: Context) :
|
|||
fun writeQueue(queueItems: List<QueueItem>) {
|
||||
assertBackgroundThread()
|
||||
|
||||
val database = readableDatabase
|
||||
database.beginTransaction()
|
||||
val database = writableDatabase
|
||||
|
||||
try {
|
||||
database.delete(TABLE_NAME_QUEUE, null, null)
|
||||
database.setTransactionSuccessful()
|
||||
} finally {
|
||||
database.endTransaction()
|
||||
|
||||
logD("Successfully wiped queue.")
|
||||
database.execute {
|
||||
delete(TABLE_NAME_QUEUE, null, null)
|
||||
}
|
||||
|
||||
logD("Writing to queue.")
|
||||
logD("Wiped queue db.")
|
||||
|
||||
var position = 0
|
||||
|
||||
// Try to write out the entirety of the queue, any failed inserts will be skipped.
|
||||
// Try to write out the entirety of the queue. Failed inserts will be skipped.
|
||||
while (position < queueItems.size) {
|
||||
database.beginTransaction()
|
||||
var i = position
|
||||
|
||||
try {
|
||||
database.execute {
|
||||
while (i < queueItems.size) {
|
||||
val item = queueItems[i]
|
||||
val itemData = ContentValues(4)
|
||||
|
||||
i++
|
||||
|
||||
itemData.apply {
|
||||
val itemData = ContentValues(4).apply {
|
||||
put(QueueItem.COLUMN_ID, item.id)
|
||||
put(QueueItem.COLUMN_SONG_NAME, item.songName)
|
||||
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
|
||||
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue)
|
||||
}
|
||||
|
||||
database.insert(TABLE_NAME_QUEUE, null, itemData)
|
||||
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
|
||||
|
||||
logD("Wrote batch of $position songs.")
|
||||
}
|
||||
|
||||
// Update the position at the end, if an insert failed at any point, then
|
||||
// the next iteration should skip it.
|
||||
position = i
|
||||
|
||||
logD("Wrote batch of songs. Position is now at $position")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,43 +198,27 @@ class PlaybackStateDatabase(context: Context) :
|
|||
fun readQueue(): List<QueueItem> {
|
||||
assertBackgroundThread()
|
||||
|
||||
val database = readableDatabase
|
||||
val queueItems = mutableListOf<QueueItem>()
|
||||
|
||||
try {
|
||||
val queueCursor = database.query(
|
||||
TABLE_NAME_QUEUE, null, null,
|
||||
null, null, null, null
|
||||
)
|
||||
readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
|
||||
if (cursor.count == 0) return@queryAll
|
||||
|
||||
queueCursor?.use { cursor ->
|
||||
if (cursor.count == 0) return@use
|
||||
val idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
|
||||
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_NAME)
|
||||
val albumIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ALBUM_NAME)
|
||||
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
|
||||
|
||||
val idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
|
||||
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_NAME)
|
||||
val albumIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ALBUM_NAME)
|
||||
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idIndex)
|
||||
val songName = cursor.getStringOrNull(songIdIndex) ?: ""
|
||||
val albumName = cursor.getStringOrNull(albumIdIndex) ?: ""
|
||||
val isUserQueue = cursor.getInt(isUserQueueIndex) == 1
|
||||
|
||||
queueItems.add(
|
||||
QueueItem(id, songName, albumName, isUserQueue)
|
||||
)
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
queueItems += QueueItem(
|
||||
id = cursor.getLong(idIndex),
|
||||
songName = cursor.getStringOrNull(songIdIndex) ?: "",
|
||||
albumName = cursor.getStringOrNull(albumIdIndex) ?: "",
|
||||
isUserQueue = cursor.getInt(isUserQueueIndex) == 1
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
return queueItems
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertBackgroundThread() {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
error("Not on a background thread.")
|
||||
}
|
||||
return queueItems
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
Loading…
Reference in a new issue