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. * Database for storing blacklisted paths.
* Note that the paths stored here will not work with MediaStore unless you append a "%" at the * Note that the paths stored here will not work with MediaStore unless you append a "%" at the end.
* 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) { class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) { override fun onCreate(db: SQLiteDatabase) {
@ -43,43 +44,29 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
return false return false
} }
val database = writableDatabase writableDatabase.execute {
database.beginTransaction()
try {
val values = ContentValues(1) val values = ContentValues(1)
values.put(COLUMN_PATH, path) values.put(COLUMN_PATH, path)
database.insert(TABLE_NAME, null, values) insert(TABLE_NAME, null, values)
database.setTransactionSuccessful() }
} finally {
database.endTransaction()
return true return true
} }
}
/** /**
* Remove a [File] from this blacklist. * Remove a [File] from this blacklist.
*/ */
fun removePath(file: File) { fun removePath(file: File) {
val database = writableDatabase writableDatabase.execute {
val path = file.mediaStorePath delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(file.mediaStorePath))
}
database.beginTransaction()
database.delete(TABLE_NAME, "$COLUMN_PATH=?", arrayOf(path))
database.setTransactionSuccessful()
database.endTransaction()
} }
fun getPaths(): List<String> { fun getPaths(): List<String> {
val paths = mutableListOf<String>() val paths = mutableListOf<String>()
val pathsCursor = readableDatabase.query( readableDatabase.queryAll(TABLE_NAME) { cursor ->
TABLE_NAME, arrayOf(COLUMN_PATH), null, null, null, null, null
)
pathsCursor?.use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
paths.add(cursor.getString(0)) paths.add(cursor.getString(0))
} }
@ -89,19 +76,11 @@ class BlacklistDatabase(context: Context) : SQLiteOpenHelper(context, DB_NAME, n
} }
private fun hasFile(path: String): Boolean { private fun hasFile(path: String): Boolean {
val pathsCursor = readableDatabase.query( val exists = readableDatabase.queryUse(TABLE_NAME, null, "$COLUMN_PATH=?", path) { cursor ->
TABLE_NAME, cursor.moveToFirst()
arrayOf(COLUMN_PATH),
"$COLUMN_PATH=?",
arrayOf(path),
null, null, null, null
)
pathsCursor?.use { cursor ->
return cursor.moveToFirst()
} }
return false return exists ?: false
} }
private val File.mediaStorePath: String get() { 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.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper import android.database.sqlite.SQLiteOpenHelper
import android.os.Looper
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import org.oxycblt.auxio.logD import org.oxycblt.auxio.logD
/** /**
* A SQLite database for managing the persistent playback state and queue. * 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 * Yes. I know Room exists. But that would needlessly bloat my app and has crippling bugs.
* has a crippling bug where it will endlessly allocate rows even if you clear the entire db, so...
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class PlaybackStateDatabase(context: Context) : class PlaybackStateDatabase(context: Context) :
@ -86,24 +84,12 @@ class PlaybackStateDatabase(context: Context) :
fun writeState(state: PlaybackState) { fun writeState(state: PlaybackState) {
assertBackgroundThread() assertBackgroundThread()
val database = writableDatabase writableDatabase.execute {
database.beginTransaction() delete(TABLE_NAME_STATE, null, null)
try { this@PlaybackStateDatabase.logD("Wiped state db.")
database.delete(TABLE_NAME_STATE, null, null)
database.setTransactionSuccessful()
} finally {
database.endTransaction()
logD("Successfully wiped previous state.") val stateData = ContentValues(9).apply {
}
try {
database.beginTransaction()
val stateData = ContentValues(9)
stateData.apply {
put(PlaybackState.COLUMN_ID, state.id) put(PlaybackState.COLUMN_ID, state.id)
put(PlaybackState.COLUMN_SONG_NAME, state.songName) put(PlaybackState.COLUMN_SONG_NAME, state.songName)
put(PlaybackState.COLUMN_POSITION, state.position) put(PlaybackState.COLUMN_POSITION, state.position)
@ -115,14 +101,11 @@ class PlaybackStateDatabase(context: Context) :
put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue) put(PlaybackState.COLUMN_IN_USER_QUEUE, state.inUserQueue)
} }
database.insert(TABLE_NAME_STATE, null, stateData) insert(TABLE_NAME_STATE, null, stateData)
database.setTransactionSuccessful() }
} finally {
database.endTransaction()
logD("Wrote state to database.") logD("Wrote state to database.")
} }
}
/** /**
* Read the stored [PlaybackState] from the database, if there is one. * Read the stored [PlaybackState] from the database, if there is one.
@ -131,49 +114,38 @@ class PlaybackStateDatabase(context: Context) :
fun readState(): PlaybackState? { fun readState(): PlaybackState? {
assertBackgroundThread() assertBackgroundThread()
val database = writableDatabase
var state: PlaybackState? = null var state: PlaybackState? = null
try { readableDatabase.queryAll(TABLE_NAME_STATE) { cursor ->
val stateCursor = database.query( if (cursor.count == 0) return@queryAll
TABLE_NAME_STATE,
null, null, null,
null, null, null
)
stateCursor?.use { cursor ->
// Don't bother if the cursor [and therefore database] has nothing in it.
if (cursor.count == 0) return@use
val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_NAME) val songIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_SONG_NAME)
val positionIndex = 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_NAME)
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 isShufflingIndex = val shuffleIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IS_SHUFFLING)
val loopModeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_LOOP_MODE) val loopModeIndex = cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_LOOP_MODE)
val inUserQueueIndex = val inUserQueueIndex = cursor.getColumnIndexOrThrow(
cursor.getColumnIndexOrThrow(PlaybackState.COLUMN_IN_USER_QUEUE) PlaybackState.COLUMN_IN_USER_QUEUE
)
// If there is something in it, get the first item from it, ignoring anything else.
cursor.moveToFirst() cursor.moveToFirst()
state = PlaybackState( state = PlaybackState(
songName = cursor.getStringOrNull(songIndex) ?: "", songName = cursor.getStringOrNull(songIndex) ?: "",
position = cursor.getLong(positionIndex), position = cursor.getLong(posIndex),
parentName = cursor.getStringOrNull(parentIndex) ?: "", parentName = cursor.getStringOrNull(parentIndex) ?: "",
index = cursor.getInt(indexIndex), index = cursor.getInt(indexIndex),
mode = cursor.getInt(modeIndex), mode = cursor.getInt(modeIndex),
isShuffling = cursor.getInt(isShufflingIndex) == 1, isShuffling = cursor.getInt(shuffleIndex) == 1,
loopMode = cursor.getInt(loopModeIndex), loopMode = cursor.getInt(loopModeIndex),
inUserQueue = cursor.getInt(inUserQueueIndex) == 1 inUserQueue = cursor.getInt(inUserQueueIndex) == 1
) )
} }
} finally {
return state return state
} }
}
/** /**
* Write a list of [queueItems] to the database, clearing the previous queue present. * Write a list of [queueItems] to the database, clearing the previous queue present.
@ -181,54 +153,41 @@ class PlaybackStateDatabase(context: Context) :
fun writeQueue(queueItems: List<QueueItem>) { fun writeQueue(queueItems: List<QueueItem>) {
assertBackgroundThread() assertBackgroundThread()
val database = readableDatabase val database = writableDatabase
database.beginTransaction()
try { database.execute {
database.delete(TABLE_NAME_QUEUE, null, null) delete(TABLE_NAME_QUEUE, null, null)
database.setTransactionSuccessful()
} finally {
database.endTransaction()
logD("Successfully wiped queue.")
} }
logD("Writing to queue.") logD("Wiped queue db.")
var position = 0 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) { while (position < queueItems.size) {
database.beginTransaction()
var i = position var i = position
try { database.execute {
while (i < queueItems.size) { while (i < queueItems.size) {
val item = queueItems[i] val item = queueItems[i]
val itemData = ContentValues(4)
i++ i++
itemData.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_NAME, item.songName)
put(QueueItem.COLUMN_ALBUM_NAME, item.albumName) put(QueueItem.COLUMN_ALBUM_NAME, item.albumName)
put(QueueItem.COLUMN_IS_USER_QUEUE, item.isUserQueue) 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 // Update the position at the end, if an insert failed at any point, then
// the next iteration should skip it. // the next iteration should skip it.
position = i position = i
logD("Wrote batch of $position songs.") logD("Wrote batch of songs. Position is now at $position")
}
} }
} }
@ -239,17 +198,10 @@ class PlaybackStateDatabase(context: Context) :
fun readQueue(): List<QueueItem> { fun readQueue(): List<QueueItem> {
assertBackgroundThread() assertBackgroundThread()
val database = readableDatabase
val queueItems = mutableListOf<QueueItem>() val queueItems = mutableListOf<QueueItem>()
try { readableDatabase.queryAll(TABLE_NAME_QUEUE) { cursor ->
val queueCursor = database.query( if (cursor.count == 0) return@queryAll
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 idIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_ID)
val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_NAME) val songIdIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_SONG_NAME)
@ -257,25 +209,16 @@ class PlaybackStateDatabase(context: Context) :
val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE) val isUserQueueIndex = cursor.getColumnIndexOrThrow(QueueItem.COLUMN_IS_USER_QUEUE)
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex) queueItems += QueueItem(
val songName = cursor.getStringOrNull(songIdIndex) ?: "" id = cursor.getLong(idIndex),
val albumName = cursor.getStringOrNull(albumIdIndex) ?: "" songName = cursor.getStringOrNull(songIdIndex) ?: "",
val isUserQueue = cursor.getInt(isUserQueueIndex) == 1 albumName = cursor.getStringOrNull(albumIdIndex) ?: "",
isUserQueue = cursor.getInt(isUserQueueIndex) == 1
queueItems.add(
QueueItem(id, songName, albumName, isUserQueue)
) )
} }
} }
} finally {
return queueItems
}
}
private fun assertBackgroundThread() { return queueItems
if (Looper.myLooper() == Looper.getMainLooper()) {
error("Not on a background thread.")
}
} }
companion object { companion object {