Files
michas-droid/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt
T
2020-07-29 23:46:43 +03:00

657 lines
26 KiB
Kotlin

package nya.kitsunyan.foxydroid.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 io.reactivex.rxjava3.core.Observable
import nya.kitsunyan.foxydroid.entity.InstalledItem
import nya.kitsunyan.foxydroid.entity.Product
import nya.kitsunyan.foxydroid.entity.ProductItem
import nya.kitsunyan.foxydroid.entity.Repository
import nya.kitsunyan.foxydroid.utility.extension.android.*
import nya.kitsunyan.foxydroid.utility.extension.json.*
import java.io.ByteArrayOutputStream
object Database {
fun init(context: Context): Boolean {
val helper = Helper(context)
db = helper.writableDatabase
if (helper.created) {
for (repository in Repository.defaultRepositories) {
RepositoryAdapter.put(repository)
}
}
return helper.created || helper.updated
}
private lateinit var db: SQLiteDatabase
private interface Table {
val memory: Boolean
val innerName: String
val createTable: String
val createIndex: String?
get() = null
val databasePrefix: String
get() = if (memory) "memory." else ""
val name: String
get() = "$databasePrefix$innerName"
fun formatCreateTable(name: String): String {
return "CREATE TABLE $name (${QueryBuilder.trimQuery(createTable)})"
}
val createIndexPairFormatted: Pair<String, String>?
get() = createIndex?.let { Pair("CREATE INDEX ${innerName}_index ON $innerName ($it)",
"CREATE INDEX ${name}_index ON $innerName ($it)") }
}
private object Schema {
object Repository: Table {
const val ROW_ID = "_id"
const val ROW_ENABLED = "enabled"
const val ROW_DELETED = "deleted"
const val ROW_DATA = "data"
override val memory = false
override val innerName = "repository"
override val createTable = """
$ROW_ID INTEGER PRIMARY KEY AUTOINCREMENT,
$ROW_ENABLED INTEGER NOT NULL,
$ROW_DELETED INTEGER NOT NULL,
$ROW_DATA BLOB NOT NULL
"""
}
object Product: Table {
const val ROW_REPOSITORY_ID = "repository_id"
const val ROW_PACKAGE_NAME = "package_name"
const val ROW_NAME = "name"
const val ROW_SUMMARY = "summary"
const val ROW_DESCRIPTION = "description"
const val ROW_ADDED = "added"
const val ROW_UPDATED = "updated"
const val ROW_VERSION_CODE = "version_code"
const val ROW_SIGNATURES = "signatures"
const val ROW_COMPATIBLE = "compatible"
const val ROW_DATA = "data"
const val ROW_DATA_ITEM = "data_item"
override val memory = false
override val innerName = "product"
override val createTable = """
$ROW_REPOSITORY_ID INTEGER NOT NULL,
$ROW_PACKAGE_NAME TEXT NOT NULL,
$ROW_NAME TEXT NOT NULL,
$ROW_SUMMARY TEXT NOT NULL,
$ROW_DESCRIPTION TEXT NOT NULL,
$ROW_ADDED INTEGER NOT NULL,
$ROW_UPDATED INTEGER NOT NULL,
$ROW_VERSION_CODE INTEGER NOT NULL,
$ROW_SIGNATURES TEXT NOT NULL,
$ROW_COMPATIBLE INTEGER NOT NULL,
$ROW_DATA BLOB NOT NULL,
$ROW_DATA_ITEM BLOB NOT NULL,
PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME)
"""
override val createIndex = ROW_PACKAGE_NAME
}
object Category: Table {
const val ROW_REPOSITORY_ID = "repository_id"
const val ROW_PACKAGE_NAME = "package_name"
const val ROW_NAME = "name"
override val memory = false
override val innerName = "category"
override val createTable = """
$ROW_REPOSITORY_ID INTEGER NOT NULL,
$ROW_PACKAGE_NAME TEXT NOT NULL,
$ROW_NAME TEXT NOT NULL,
PRIMARY KEY ($ROW_REPOSITORY_ID, $ROW_PACKAGE_NAME, $ROW_NAME)
"""
override val createIndex = "$ROW_PACKAGE_NAME, $ROW_NAME"
}
object Installed: Table {
const val ROW_PACKAGE_NAME = "package_name"
const val ROW_VERSION = "version"
const val ROW_VERSION_CODE = "version_code"
const val ROW_SIGNATURE = "signature"
override val memory = true
override val innerName = "installed"
override val createTable = """
$ROW_PACKAGE_NAME TEXT PRIMARY KEY,
$ROW_VERSION TEXT NOT NULL,
$ROW_VERSION_CODE INTEGER NOT NULL,
$ROW_SIGNATURE TEXT NOT NULL
"""
}
object Lock: Table {
const val ROW_PACKAGE_NAME = "package_name"
const val ROW_VERSION_CODE = "version_code"
override val memory = true
override val innerName = "lock"
override val createTable = """
$ROW_PACKAGE_NAME TEXT PRIMARY KEY,
$ROW_VERSION_CODE INTEGER NOT NULL
"""
}
object Synthetic {
const val ROW_CAN_UPDATE = "can_update"
const val ROW_MATCH_RANK = "match_rank"
}
}
private class Helper(context: Context): SQLiteOpenHelper(context, "foxydroid", null, 1) {
var created = false
private set
var updated = false
private set
override fun onCreate(db: SQLiteDatabase) = Unit
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = onVersionChange(db)
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) = onVersionChange(db)
private fun onVersionChange(db: SQLiteDatabase) {
handleTables(db, true, Schema.Product, Schema.Category)
this.updated = true
}
override fun onOpen(db: SQLiteDatabase) {
val create = handleTables(db, false, Schema.Repository)
val updated = handleTables(db, create, Schema.Product, Schema.Category)
db.execSQL("ATTACH DATABASE ':memory:' AS memory")
handleTables(db, false, Schema.Installed, Schema.Lock)
handleIndexes(db, Schema.Repository, Schema.Product, Schema.Category, Schema.Installed, Schema.Lock)
dropOldTables(db, Schema.Repository, Schema.Product, Schema.Category)
this.created = this.created || create
this.updated = this.updated || create || updated
}
}
private fun handleTables(db: SQLiteDatabase, recreate: Boolean, vararg tables: Table): Boolean {
val shouldRecreate = recreate || tables.any {
val sql = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("sql"),
selection = Pair("type = ? AND name = ?", arrayOf("table", it.innerName)))
.use { it.firstOrNull()?.getString(0) }.orEmpty()
it.formatCreateTable(it.innerName) != sql
}
return shouldRecreate && run {
val shouldVacuum = tables.map {
db.execSQL("DROP TABLE IF EXISTS ${it.name}")
db.execSQL(it.formatCreateTable(it.name))
!it.memory
}
if (shouldVacuum.any { it } && !db.inTransaction()) {
db.execSQL("VACUUM")
}
true
}
}
private fun handleIndexes(db: SQLiteDatabase, vararg tables: Table) {
val shouldVacuum = tables.map {
val sqls = db.query("${it.databasePrefix}sqlite_master", columns = arrayOf("name", "sql"),
selection = Pair("type = ? AND tbl_name = ?", arrayOf("index", it.innerName)))
.use { it.asSequence().mapNotNull { it.getString(1)?.let { sql -> Pair(it.getString(0), sql) } }.toList() }
.filter { !it.first.startsWith("sqlite_") }
val createIndexes = it.createIndexPairFormatted?.let { listOf(it) }.orEmpty()
createIndexes.map { it.first } != sqls.map { it.second } && run {
for (name in sqls.map { it.first }) {
db.execSQL("DROP INDEX IF EXISTS $name")
}
for (createIndexPair in createIndexes) {
db.execSQL(createIndexPair.second)
}
!it.memory
}
}
if (shouldVacuum.any { it } && !db.inTransaction()) {
db.execSQL("VACUUM")
}
}
private fun dropOldTables(db: SQLiteDatabase, vararg neededTables: Table) {
val tables = db.query("sqlite_master", columns = arrayOf("name"),
selection = Pair("type = ?", arrayOf("table")))
.use { it.asSequence().mapNotNull { it.getString(0) }.toList() }
.filter { !it.startsWith("sqlite_") && !it.startsWith("android_") }
.toSet() - neededTables.mapNotNull { if (it.memory) null else it.name }
if (tables.isNotEmpty()) {
for (table in tables) {
db.execSQL("DROP TABLE IF EXISTS $table")
}
if (!db.inTransaction()) {
db.execSQL("VACUUM")
}
}
}
sealed class Subject {
object Repositories: Subject()
data class Repository(val id: Long): Subject()
object Products: Subject()
}
private val observers = mutableMapOf<Subject, MutableSet<() -> Unit>>()
private fun dataObservable(subject: Subject): (Boolean, () -> Unit) -> Unit = { register, observer ->
synchronized(observers) {
val set = observers[subject] ?: run {
val set = mutableSetOf<() -> Unit>()
observers[subject] = set
set
}
if (register) {
set += observer
} else {
set -= observer
}
}
}
fun observable(subject: Subject): Observable<Unit> {
return Observable.create {
val callback: () -> Unit = { it.onNext(Unit) }
val dataObservable = dataObservable(subject)
dataObservable(true, callback)
it.setCancellable { dataObservable(false, callback) }
}
}
private fun notifyChanged(vararg subjects: Subject) {
synchronized(observers) {
subjects.asSequence().mapNotNull { observers[it] }.flatten().forEach { it() }
}
}
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,
signal: CancellationSignal? = null): Cursor {
return query(false, table, columns, selection?.first, selection?.second, null, null, orderBy, null, signal)
}
private fun Cursor.observable(subject: Subject): ObservableCursor {
return ObservableCursor(this, dataObservable(subject))
}
private fun <T> ByteArray.jsonParse(callback: (JsonParser) -> T): T {
return Json.factory.createParser(this).use { it.parseDictionary(callback) }
}
private fun jsonGenerate(callback: (JsonGenerator) -> Unit): ByteArray {
val outputStream = ByteArrayOutputStream()
Json.factory.createGenerator(outputStream).use { it.writeDictionary(callback) }
return outputStream.toByteArray()
}
object RepositoryAdapter {
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))
})
}
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
}
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) }
}
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() }
}
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() }
}
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)
}
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)
}
}
fun query(signal: CancellationSignal?): Cursor {
return db.query(Schema.Repository.name,
selection = Pair("${Schema.Repository.ROW_DELETED} == 0", emptyArray()),
signal = signal).observable(Subject.Repositories)
}
fun transform(cursor: Cursor): Repository {
return cursor.getBlob(cursor.getColumnIndex(Schema.Repository.ROW_DATA))
.jsonParse { Repository.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Repository.ROW_ID)), it) }
}
}
object ProductAdapter {
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() }
}
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 }
}
fun query(installed: Boolean, updates: Boolean, searchQuery: String,
category: String, 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 (category.isNotEmpty()) {
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 (category.isNotEmpty()) {
builder += "AND category.${Schema.Category.ROW_NAME} = ?"
builder %= category
}
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)
}
private fun transform(cursor: Cursor): Product {
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA))
.jsonParse { Product.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_DESCRIPTION)), it) }
}
fun transformItem(cursor: Cursor): ProductItem {
return cursor.getBlob(cursor.getColumnIndex(Schema.Product.ROW_DATA_ITEM))
.jsonParse { ProductItem.deserialize(cursor.getLong(cursor.getColumnIndex(Schema.Product.ROW_REPOSITORY_ID)),
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_PACKAGE_NAME)),
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_NAME)),
cursor.getString(cursor.getColumnIndex(Schema.Product.ROW_SUMMARY)),
cursor.getString(cursor.getColumnIndex(Schema.Installed.ROW_VERSION)).orEmpty(),
cursor.getInt(cursor.getColumnIndex(Schema.Product.ROW_COMPATIBLE)) != 0,
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_CAN_UPDATE)) != 0,
cursor.getInt(cursor.getColumnIndex(Schema.Synthetic.ROW_MATCH_RANK)), it) }
}
}
object CategoryAdapter {
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() }
}
}
object InstalledAdapter {
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) }
}
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)
}
}
fun put(installedItem: InstalledItem) = put(installedItem, true)
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()
}
}
fun delete(packageName: String) {
val count = db.delete(Schema.Installed.name, "${Schema.Installed.ROW_PACKAGE_NAME} = ?", arrayOf(packageName))
if (count > 0) {
notifyChanged(Subject.Products)
}
}
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)))
}
}
object LockAdapter {
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)
}
}
fun put(lock: Pair<String, Long>) = put(lock, true)
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()
}
}
fun delete(packageName: String) {
db.delete(Schema.Lock.name, "${Schema.Lock.ROW_PACKAGE_NAME} = ?", arrayOf(packageName))
notifyChanged(Subject.Products)
}
}
object UpdaterAdapter {
private val Table.temporaryName: String
get() = "${name}_temporary"
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))
}
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()
}
}
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}")
}
}
}
}