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.
|
* 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() {
|
||||||
|
|
|
@ -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.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 {
|
||||||
|
|
Loading…
Reference in a new issue