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:
OxygenCobalt 2021-03-09 10:14:47 -07:00
parent 72877f77ee
commit 9a02eadc95
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
3 changed files with 133 additions and 153 deletions

View file

@ -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() {

View file

@ -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.")
}
}

View file

@ -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 {