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? 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_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_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" } } 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.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.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") } db.execSQL("VACUUM") } } sealed class Subject { object Repositories: Subject() data class Repository(val id: Long): Subject() object Products: Subject() } private val observers = mutableMapOf 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 { 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? = null, selection: Pair>? = 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 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 { 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> { 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>) { 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 { return db.query(Schema.Product.name, columns = arrayOf(Schema.Product.ROW_REPOSITORY_ID, 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}, 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 (product.${Schema.Product.ROW_PACKAGE_NAME} LIKE ? OR product.${Schema.Product.ROW_NAME} LIKE ? OR product.${Schema.Product.ROW_SUMMARY} LIKE ?)""" builder %= List(3) { "%$searchQuery%" } } builder += "GROUP BY product.${Schema.Product.ROW_PACKAGE_NAME} HAVING 1" if (updates) { builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" } builder += "ORDER BY" 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)), 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, it) } } } object CategoryAdapter { fun getAll(signal: CancellationSignal?): Set { 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) { 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, 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) = put(lock, true) fun putAll(locks: List>) { 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) { 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_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}") } } } }