Clean up (Database.kt)

This commit is contained in:
machiav3lli 2021-11-01 01:07:21 +01:00
parent dc65c060e7
commit cc8e7b6ea3

View File

@ -1,24 +1,14 @@
package com.looker.droidify.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.os.CancellationSignal
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.entity.Product
import com.looker.droidify.entity.ProductItem
import com.looker.droidify.entity.Repository
import com.looker.droidify.utility.extension.android.asSequence
import com.looker.droidify.utility.extension.android.firstOrNull
import com.looker.droidify.utility.extension.json.Json
import com.looker.droidify.utility.extension.json.parseDictionary
import com.looker.droidify.utility.extension.json.writeDictionary
import io.reactivex.rxjava3.core.Observable
import java.io.ByteArrayOutputStream
object Database {
fun init(context: Context): Boolean {
@ -26,7 +16,7 @@ object Database {
db = helper.writableDatabase
if (helper.created) {
for (repository in Repository.defaultRepositories) {
RepositoryAdapter.put(repository)
//RepositoryAdapter.put(repository)
}
}
return helper.created || helper.updated
@ -160,12 +150,6 @@ object Database {
$ROW_VERSION_CODE INTEGER NOT NULL
"""
}
// TODO find a class to include them as constants
object Synthetic {
const val ROW_CAN_UPDATE = "can_update"
const val ROW_MATCH_RANK = "match_rank"
}
}
// not needed remove after migration
@ -313,26 +297,6 @@ object Database {
}
}
// TODO not needed remove after migration (replaced by LiveData)
private fun notifyChanged(vararg subjects: Subject) {
synchronized(observers) {
subjects.asSequence().mapNotNull { observers[it] }.flatten().forEach { it() }
}
}
// TODO Done through inserts/replace of DAOs, only temporary still not finished
private fun SQLiteDatabase.insertOrReplace(
replace: Boolean,
table: String,
contentValues: ContentValues
): Long {
return if (replace) replace(table, null, contentValues) else insert(
table,
null,
contentValues
)
}
private fun SQLiteDatabase.query(
table: String, columns: Array<String>? = null,
selection: Pair<String, Array<String>>? = null, orderBy: String? = null,
@ -351,501 +315,4 @@ object Database {
signal
)
}
private fun Cursor.observable(subject: Subject): ObservableCursor {
return ObservableCursor(this, dataObservable(subject))
}
fun <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
}
fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray {
val outputStream = ByteArrayOutputStream()
Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) }
return outputStream.toByteArray()
}
// Partially done, only
object RepositoryAdapter {
// Done in put
internal fun putWithoutNotification(repository: Repository, shouldReplace: Boolean): Long {
return db.insertOrReplace(shouldReplace, Schema.Repository.name, ContentValues().apply {
if (shouldReplace) {
put(Schema.Repository.ROW_ID, repository.id)
}
put(Schema.Repository.ROW_ENABLED, if (repository.enabled) 1 else 0)
put(Schema.Repository.ROW_DELETED, 0)
put(Schema.Repository.ROW_DATA, jsonGenerate(repository::serialize))
})
}
// Done
fun put(repository: Repository): Repository {
val shouldReplace = repository.id >= 0L
val newId = putWithoutNotification(repository, shouldReplace)
val id = if (shouldReplace) repository.id else newId
notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products)
return if (newId != repository.id) repository.copy(id = newId) else repository
}
// Done
fun get(id: Long): Repository? {
return db.query(
Schema.Repository.name,
selection = Pair(
"${Schema.Repository.ROW_ID} = ? AND ${Schema.Repository.ROW_DELETED} == 0",
arrayOf(id.toString())
)
)
.use { it.firstOrNull()?.let(::transform) }
}
// Done
// MAYBE signal has to be considered
fun getAll(signal: CancellationSignal?): List<Repository> {
return db.query(
Schema.Repository.name,
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
signal = signal
).use { it.asSequence().map(::transform).toList() }
}
// Done Pair<Long,Int> instead
// MAYBE signal has to be considered
fun getAllDisabledDeleted(signal: CancellationSignal?): Set<Pair<Long, Boolean>> {
return db.query(
Schema.Repository.name,
columns = arrayOf(Schema.Repository.ROW_ID, Schema.Repository.ROW_DELETED),
selection = Pair(
"${Schema.Repository.ROW_ENABLED} == 0 OR ${Schema.Repository.ROW_DELETED} != 0",
emptyArray()
),
signal = signal
).use {
it.asSequence().map {
Pair(
it.getLong(it.getColumnIndex(Schema.Repository.ROW_ID)),
it.getInt(it.getColumnIndex(Schema.Repository.ROW_DELETED)) != 0
)
}.toSet()
}
}
// Done
fun markAsDeleted(id: Long) {
db.update(Schema.Repository.name, ContentValues().apply {
put(Schema.Repository.ROW_DELETED, 1)
}, "${Schema.Repository.ROW_ID} = ?", arrayOf(id.toString()))
notifyChanged(Subject.Repositories, Subject.Repository(id), Subject.Products)
}
// Done
fun cleanup(pairs: Set<Pair<Long, Boolean>>) {
val result = pairs.windowed(10, 10, true).map {
val idsString = it.joinToString(separator = ", ") { it.first.toString() }
val productsCount = db.delete(
Schema.Product.name,
"${Schema.Product.ROW_REPOSITORY_ID} IN ($idsString)", null
)
val categoriesCount = db.delete(
Schema.Category.name,
"${Schema.Category.ROW_REPOSITORY_ID} IN ($idsString)", null
)
val deleteIdsString = it.asSequence().filter { it.second }
.joinToString(separator = ", ") { it.first.toString() }
if (deleteIdsString.isNotEmpty()) {
db.delete(
Schema.Repository.name,
"${Schema.Repository.ROW_ID} IN ($deleteIdsString)",
null
)
}
productsCount != 0 || categoriesCount != 0
}
if (result.any { it }) {
notifyChanged(Subject.Products)
}
}
// get the cursor in the specific table. Unnecessary with Room
fun query(signal: CancellationSignal?): Cursor {
return db.query(
Schema.Repository.name,
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
signal = signal
).observable(Subject.Repositories)
}
// Done
fun transform(cursor: Cursor): Repository {
return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA))
.jsonParse {
Repository.deserialize(it).apply {
this.id = cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID))
}
}
}
}
object ProductAdapter {
// Done
fun get(packageName: String, signal: CancellationSignal?): List<Product> {
return db.query(
Schema.Product.name,
columns = arrayOf(
Schema.Product.ROW_REPOSITORY_ID,
Schema.Product.ROW_DESCRIPTION,
Schema.Product.ROW_DATA
),
selection = Pair("${Schema.Product.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
signal = signal
).use { it.asSequence().map(::transform).toList() }
}
// Done
fun getCount(repositoryId: Long): Int {
return db.query(
Schema.Product.name, columns = arrayOf("COUNT (*)"),
selection = Pair(
"${Schema.Product.ROW_REPOSITORY_ID} = ?",
arrayOf(repositoryId.toString())
)
)
.use { it.firstOrNull()?.getInt(0) ?: 0 }
}
// Done
fun query(
installed: Boolean, updates: Boolean, searchQuery: String,
section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?
): Cursor {
val builder = QueryBuilder()
val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND
product.${Schema.Product.ROW_SIGNATURES} LIKE ('%.' || installed.${Schema.Installed.ROW_SIGNATURE} || '.%') AND
product.${Schema.Product.ROW_SIGNATURES} != ''"""
builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID},
product.${Schema.Product.ROW_PACKAGE_NAME}, product.${Schema.Product.ROW_NAME},
product.${Schema.Product.ROW_SUMMARY}, installed.${Schema.Installed.ROW_VERSION},
(COALESCE(lock.${Schema.Lock.ROW_VERSION_CODE}, -1) NOT IN (0, product.${Schema.Product.ROW_VERSION_CODE}) AND
product.${Schema.Product.ROW_COMPATIBLE} != 0 AND product.${Schema.Product.ROW_VERSION_CODE} >
COALESCE(installed.${Schema.Installed.ROW_VERSION_CODE}, 0xffffffff) AND $signatureMatches)
AS ${Schema.Synthetic.ROW_CAN_UPDATE}, product.${Schema.Product.ROW_COMPATIBLE},
product.${Schema.Product.ROW_DATA_ITEM},"""
if (searchQuery.isNotEmpty()) {
builder += """(((product.${Schema.Product.ROW_NAME} LIKE ? OR
product.${Schema.Product.ROW_SUMMARY} LIKE ?) * 7) |
((product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ?) * 3) |
(product.${Schema.Product.ROW_DESCRIPTION} LIKE ?)) AS ${Schema.Synthetic.ROW_MATCH_RANK},"""
builder %= List(4) { "%$searchQuery%" }
} else {
builder += "0 AS ${Schema.Synthetic.ROW_MATCH_RANK},"
}
builder += """MAX((product.${Schema.Product.ROW_COMPATIBLE} AND
(installed.${Schema.Installed.ROW_SIGNATURE} IS NULL OR $signatureMatches)) ||
PRINTF('%016X', product.${Schema.Product.ROW_VERSION_CODE})) FROM ${Schema.Product.name} AS product"""
builder += """JOIN ${Schema.Repository.name} AS repository
ON product.${Schema.Product.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID}"""
builder += """LEFT JOIN ${Schema.Lock.name} AS lock
ON product.${Schema.Product.ROW_PACKAGE_NAME} = lock.${Schema.Lock.ROW_PACKAGE_NAME}"""
if (!installed && !updates) {
builder += "LEFT"
}
builder += """JOIN ${Schema.Installed.name} AS installed
ON product.${Schema.Product.ROW_PACKAGE_NAME} = installed.${Schema.Installed.ROW_PACKAGE_NAME}"""
if (section is ProductItem.Section.Category) {
builder += """JOIN ${Schema.Category.name} AS category
ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}"""
}
builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND
repository.${Schema.Repository.ROW_DELETED} == 0"""
if (section is ProductItem.Section.Category) {
builder += "AND category.${Schema.Category.ROW_NAME} = ?"
builder %= section.name
} else if (section is ProductItem.Section.Repository) {
builder += "AND product.${Schema.Product.ROW_REPOSITORY_ID} = ?"
builder %= section.id.toString()
}
if (searchQuery.isNotEmpty()) {
builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0"""
}
builder += "GROUP BY product.${Schema.Product.ROW_PACKAGE_NAME} HAVING 1"
if (updates) {
builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}"
}
builder += "ORDER BY"
if (searchQuery.isNotEmpty()) {
builder += """${Schema.Synthetic.ROW_MATCH_RANK} DESC,"""
}
when (order) {
ProductItem.Order.NAME -> Unit
ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC,"
ProductItem.Order.LAST_UPDATE -> builder += "product.${Schema.Product.ROW_UPDATED} DESC,"
}::class
builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC"
return builder.query(db, signal).observable(Subject.Products)
}
// Done
private fun transform(cursor: Cursor): Product {
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA))
.jsonParse {
Product.deserialize(it).apply {
this.repositoryId = cursor
.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID))
this.description = cursor
.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION))
}
}
}
// Done
fun transformItem(cursor: Cursor): ProductItem {
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM))
.jsonParse {
ProductItem.deserialize(it).apply {
this.repositoryId = cursor
.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID))
this.packageName = cursor
.getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME))
this.name = cursor
.getString(cursor.getColumnIndex(Schema.Product.ROW_NAME))
this.summary = cursor
.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY))
this.installedVersion = cursor
.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION))
.orEmpty()
this.compatible = cursor
.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0
this.canUpdate = cursor
.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0
this.matchRank = cursor
.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK))
}
}
}
}
// Done
object CategoryAdapter {
// Done
fun getAll(signal: CancellationSignal?): Set<String> {
val builder = QueryBuilder()
builder += """SELECT DISTINCT category.${Schema.Category.ROW_NAME}
FROM ${Schema.Category.name} AS category
JOIN ${Schema.Repository.name} AS repository
ON category.${Schema.Category.ROW_REPOSITORY_ID} = repository.${Schema.Repository.ROW_ID}
WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND
repository.${Schema.Repository.ROW_DELETED} == 0"""
return builder.query(db, signal).use {
it.asSequence()
.map { it.getString(it.getColumnIndex(Schema.Category.ROW_NAME)) }.toSet()
}
}
}
// Done
object InstalledAdapter {
// Done
fun get(packageName: String, signal: CancellationSignal?): InstalledItem? {
return db.query(
Schema.Installed.name,
columns = arrayOf(
Schema.Installed.ROW_PACKAGE_NAME, Schema.Installed.ROW_VERSION,
Schema.Installed.ROW_VERSION_CODE, Schema.Installed.ROW_SIGNATURE
),
selection = Pair("${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName)),
signal = signal
).use { it.firstOrNull()?.let(::transform) }
}
// Done in insert
private fun put(installedItem: InstalledItem, notify: Boolean) {
db.insertOrReplace(true, Schema.Installed.name, ContentValues().apply {
put(Schema.Installed.ROW_PACKAGE_NAME, installedItem.packageName)
put(Schema.Installed.ROW_VERSION, installedItem.version)
put(Schema.Installed.ROW_VERSION_CODE, installedItem.versionCode)
put(Schema.Installed.ROW_SIGNATURE, installedItem.signature)
})
if (notify) {
notifyChanged(Subject.Products)
}
}
// Done in insert
fun put(installedItem: InstalledItem) = put(installedItem, true)
// Done in insert
fun putAll(installedItems: List<InstalledItem>) {
db.beginTransaction()
try {
db.delete(Schema.Installed.name, null, null)
installedItems.forEach { put(it, false) }
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
// Done
fun delete(packageName: String) {
val count = db.delete(
Schema.Installed.name,
"${Schema.Installed.ROW_PACKAGE_NAME} = ?",
arrayOf(packageName)
)
if (count > 0) {
notifyChanged(Subject.Products)
}
}
// Unnecessary with Room
private fun transform(cursor: Cursor): InstalledItem {
return InstalledItem(
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_PACKAGE_NAME)),
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)),
cursor.getLong(cursor.getColumnIndex(Schema.Installed.ROW_VERSION_CODE)),
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_SIGNATURE))
)
}
}
// Done with some changes in DAOs
object LockAdapter {
// Done in insert (Lock object instead of pair)
private fun put(lock: Pair<String, Long>, notify: Boolean) {
db.insertOrReplace(true, Schema.Lock.name, ContentValues().apply {
put(Schema.Lock.ROW_PACKAGE_NAME, lock.first)
put(Schema.Lock.ROW_VERSION_CODE, lock.second)
})
if (notify) {
notifyChanged(Subject.Products)
}
}
// Done in insert (Lock object instead of pair)
fun put(lock: Pair<String, Long>) = put(lock, true)
// Done in insert (Lock object instead of pair)
fun putAll(locks: List<Pair<String, Long>>) {
db.beginTransaction()
try {
db.delete(Schema.Lock.name, null, null)
locks.forEach { put(it, false) }
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
// Done
fun delete(packageName: String) {
db.delete(Schema.Lock.name, "${Schema.Lock.ROW_PACKAGE_NAME} = ?", arrayOf(packageName))
notifyChanged(Subject.Products)
}
}
// Done
object UpdaterAdapter {
private val Table.temporaryName: String
get() = "${name}_temporary"
// Done
fun createTemporaryTable() {
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
db.execSQL(Schema.Product.formatCreateTable(Schema.Product.temporaryName))
db.execSQL(Schema.Category.formatCreateTable(Schema.Category.temporaryName))
}
// Done
fun putTemporary(products: List<Product>) {
db.beginTransaction()
try {
for (product in products) {
// Format signatures like ".signature1.signature2." for easier select
val signatures = product.signatures.joinToString { ".$it" }
.let { if (it.isNotEmpty()) "$it." else "" }
db.insertOrReplace(true, Schema.Product.temporaryName, ContentValues().apply {
put(Schema.Product.ROW_REPOSITORY_ID, product.repositoryId)
put(Schema.Product.ROW_PACKAGE_NAME, product.packageName)
put(Schema.Product.ROW_NAME, product.name)
put(Schema.Product.ROW_SUMMARY, product.summary)
put(Schema.Product.ROW_DESCRIPTION, product.description)
put(Schema.Product.ROW_ADDED, product.added)
put(Schema.Product.ROW_UPDATED, product.updated)
put(Schema.Product.ROW_VERSION_CODE, product.versionCode)
put(Schema.Product.ROW_SIGNATURES, signatures)
put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0)
put(Schema.Product.ROW_DATA, jsonGenerate(product::serialize))
put(Schema.Product.ROW_DATA_ITEM, jsonGenerate(product.item()::serialize))
})
for (category in product.categories) {
db.insertOrReplace(
true,
Schema.Category.temporaryName,
ContentValues().apply {
put(Schema.Category.ROW_REPOSITORY_ID, product.repositoryId)
put(Schema.Category.ROW_PACKAGE_NAME, product.packageName)
put(Schema.Category.ROW_NAME, category)
})
}
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
// Done
fun finishTemporary(repository: Repository, success: Boolean) {
if (success) {
db.beginTransaction()
try {
db.delete(
Schema.Product.name, "${Schema.Product.ROW_REPOSITORY_ID} = ?",
arrayOf(repository.id.toString())
)
db.delete(
Schema.Category.name, "${Schema.Category.ROW_REPOSITORY_ID} = ?",
arrayOf(repository.id.toString())
)
db.execSQL("INSERT INTO ${Schema.Product.name} SELECT * FROM ${Schema.Product.temporaryName}")
db.execSQL("INSERT INTO ${Schema.Category.name} SELECT * FROM ${Schema.Category.temporaryName}")
RepositoryAdapter.putWithoutNotification(repository, true)
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
if (success) {
notifyChanged(
Subject.Repositories,
Subject.Repository(repository.id),
Subject.Products
)
}
} else {
db.execSQL("DROP TABLE IF EXISTS ${Schema.Product.temporaryName}")
db.execSQL("DROP TABLE IF EXISTS ${Schema.Category.temporaryName}")
}
}
}
}