From 31c208b787742c12d09af3b7e407cf2a53b6c79c Mon Sep 17 00:00:00 2001 From: greenart7c3 Date: Wed, 11 Dec 2024 16:07:25 -0300 Subject: [PATCH] Improved performance when deleting and importing events --- .../1.json | 15 +- .../2.json | 192 ++++++++++++++++++ .../java/com/greenart7c3/citrine/Citrine.kt | 58 ++++++ .../com/greenart7c3/citrine/MainActivity.kt | 15 +- .../citrine/database/AppDatabase.kt | 11 +- .../greenart7c3/citrine/database/EventDao.kt | 48 ++++- .../citrine/database/EventEntity.kt | 5 + .../citrine/server/CustomWebSocketServer.kt | 33 ++- .../citrine/service/WebSocketServerService.kt | 50 +---- .../greenart7c3/citrine/ui/HomeViewModel.kt | 10 +- 10 files changed, 375 insertions(+), 62 deletions(-) create mode 100644 app/schemas/com.greenart7c3.citrine.database.AppDatabase/2.json diff --git a/app/schemas/com.greenart7c3.citrine.database.AppDatabase/1.json b/app/schemas/com.greenart7c3.citrine.database.AppDatabase/1.json index ebc4446..be14f2b 100644 --- a/app/schemas/com.greenart7c3.citrine.database.AppDatabase/1.json +++ b/app/schemas/com.greenart7c3.citrine.database.AppDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "3b07bf4ea94274460773626c1fe0957e", + "identityHash": "bf90c485bbfb3751761eb581ab24cf07", "entities": [ { "tableName": "EventEntity", @@ -73,6 +73,17 @@ "ASC" ], "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_pubkey_kind` ON `${TABLE_NAME}` (`pubkey` ASC, `kind` ASC)" + }, + { + "name": "most_common_search_is_kind", + "unique": false, + "columnNames": [ + "kind" + ], + "orders": [ + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_kind` ON `${TABLE_NAME}` (`kind` ASC)" } ], "foreignKeys": [] @@ -175,7 +186,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3b07bf4ea94274460773626c1fe0957e')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf90c485bbfb3751761eb581ab24cf07')" ] } } \ No newline at end of file diff --git a/app/schemas/com.greenart7c3.citrine.database.AppDatabase/2.json b/app/schemas/com.greenart7c3.citrine.database.AppDatabase/2.json new file mode 100644 index 0000000..b762a39 --- /dev/null +++ b/app/schemas/com.greenart7c3.citrine.database.AppDatabase/2.json @@ -0,0 +1,192 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "bf90c485bbfb3751761eb581ab24cf07", + "entities": [ + { + "tableName": "EventEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `pubkey` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `kind` INTEGER NOT NULL, `content` TEXT NOT NULL, `sig` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pubkey", + "columnName": "pubkey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "kind", + "columnName": "kind", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sig", + "columnName": "sig", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "id_is_hash", + "unique": true, + "columnNames": [ + "id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `id_is_hash` ON `${TABLE_NAME}` (`id`)" + }, + { + "name": "most_common_search_is_pubkey_kind", + "unique": false, + "columnNames": [ + "pubkey", + "kind" + ], + "orders": [ + "ASC", + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_pubkey_kind` ON `${TABLE_NAME}` (`pubkey` ASC, `kind` ASC)" + }, + { + "name": "most_common_search_is_kind", + "unique": false, + "columnNames": [ + "kind" + ], + "orders": [ + "ASC" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `most_common_search_is_kind` ON `${TABLE_NAME}` (`kind` ASC)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "TagEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`pk` INTEGER PRIMARY KEY AUTOINCREMENT, `pkEvent` TEXT, `position` INTEGER NOT NULL, `col0Name` TEXT, `col1Value` TEXT, `col2Differentiator` TEXT, `col3Amount` TEXT, `col4Plus` TEXT NOT NULL, FOREIGN KEY(`pkEvent`) REFERENCES `EventEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "pk", + "columnName": "pk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "pkEvent", + "columnName": "pkEvent", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "col0Name", + "columnName": "col0Name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col1Value", + "columnName": "col1Value", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col2Differentiator", + "columnName": "col2Differentiator", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col3Amount", + "columnName": "col3Amount", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "col4Plus", + "columnName": "col4Plus", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pk" + ] + }, + "indices": [ + { + "name": "tags_by_pk_event", + "unique": false, + "columnNames": [ + "pkEvent" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `tags_by_pk_event` ON `${TABLE_NAME}` (`pkEvent`)" + }, + { + "name": "tags_by_tags_on_person_or_events", + "unique": false, + "columnNames": [ + "col0Name", + "col1Value" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `tags_by_tags_on_person_or_events` ON `${TABLE_NAME}` (`col0Name`, `col1Value`)" + } + ], + "foreignKeys": [ + { + "table": "EventEntity", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "pkEvent" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf90c485bbfb3751761eb581ab24cf07')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/greenart7c3/citrine/Citrine.kt b/app/src/main/java/com/greenart7c3/citrine/Citrine.kt index 4c19e07..14b70be 100644 --- a/app/src/main/java/com/greenart7c3/citrine/Citrine.kt +++ b/app/src/main/java/com/greenart7c3/citrine/Citrine.kt @@ -3,16 +3,27 @@ package com.greenart7c3.citrine import android.app.Application import android.content.ContentResolver import android.util.Log +import com.greenart7c3.citrine.database.AppDatabase +import com.greenart7c3.citrine.server.OlderThan +import com.greenart7c3.citrine.server.Settings import com.greenart7c3.citrine.service.LocalPreferences import com.vitorpamplona.ammolite.relays.Relay import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.EventInterface +import com.vitorpamplona.quartz.utils.TimeUtils +import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch class Citrine : Application() { val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + var isImportingEvents = false + var job: Job? = null override fun onCreate() { super.onCreate() @@ -23,6 +34,53 @@ class Citrine : Application() { fun contentResolverFn(): ContentResolver = contentResolver + fun cancelJob() { + job?.cancelChildren(null) + job?.cancel(null) + } + + suspend fun eventsToDelete(database: AppDatabase) { + if (isImportingEvents) return + + job?.cancel() + job = applicationScope.launch(Dispatchers.IO) { + try { + if (Settings.deleteEphemeralEvents && isActive) { + Log.d(Citrine.TAG, "Deleting ephemeral events") + database.eventDao().deleteEphemeralEvents() + } + + if (Settings.deleteExpiredEvents && isActive) { + Log.d(Citrine.TAG, "Deleting expired events") + database.eventDao().deleteEventsWithExpirations(TimeUtils.now()) + } + + if (Settings.deleteEventsOlderThan != OlderThan.NEVER && isActive) { + val until = when (Settings.deleteEventsOlderThan) { + OlderThan.DAY -> TimeUtils.oneDayAgo() + OlderThan.WEEK -> TimeUtils.oneWeekAgo() + OlderThan.MONTH -> TimeUtils.now() - TimeUtils.ONE_MONTH + OlderThan.YEAR -> TimeUtils.now() - TimeUtils.ONE_YEAR + else -> 0 + } + if (until > 0) { + Log.d(Citrine.TAG, "Deleting old events (older than ${Settings.deleteEventsOlderThan})") + if (Settings.neverDeleteFrom.isNotEmpty()) { + val pubKeys = Settings.neverDeleteFrom.joinToString(separator = ",") { "'$it'" } + database.eventDao().deleteAll(until, pubKeys) + } else { + database.eventDao().deleteAll(until) + } + } + } + } catch (e: Exception) { + if (e is CancellationException) throw e + Log.e(Citrine.TAG, "Error deleting events", e) + } + } + job?.join() + } + companion object { const val TAG = "Citrine" diff --git a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt index db9d931..5c6c2c1 100644 --- a/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt +++ b/app/src/main/java/com/greenart7c3/citrine/MainActivity.kt @@ -307,9 +307,20 @@ class MainActivity : ComponentActivity() { TextButton( onClick = { deleteAllDialog = false - coroutineScope.launch(Dispatchers.IO) { + Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { homeViewModel.setLoading(true) - database.eventDao().deleteAll() + Citrine.getInstance().job?.join() + Citrine.getInstance().isImportingEvents = true + homeViewModel.setProgress("Calculating database size") + val ids = database.eventDao().getAllIds() + val size = ids.size + val batchSize = 500 + ids.chunked(batchSize).forEachIndexed { index, chunk -> + homeViewModel.setProgress("Deleting ${index * batchSize}/$size") + database.eventDao().delete(chunk) + } + + Citrine.getInstance().isImportingEvents = false homeViewModel.setLoading(false) } }, diff --git a/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt b/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt index e33b426..de35e98 100644 --- a/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt +++ b/app/src/main/java/com/greenart7c3/citrine/database/AppDatabase.kt @@ -6,12 +6,14 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.greenart7c3.citrine.BuildConfig import com.greenart7c3.citrine.Citrine @Database( entities = [EventEntity::class, TagEntity::class], - version = 1, + version = 2, ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -29,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() { "citrine_database", ) // .setQueryCallback(AppDatabaseCallback(), Executors.newSingleThreadExecutor()) + .addMigrations(MIGRATION_1_2) .build() database = instance @@ -38,6 +41,12 @@ abstract class AppDatabase : RoomDatabase() { } } +val MIGRATION_1_2 = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("CREATE INDEX IF NOT EXISTS `most_common_search_is_kind` ON `EventEntity` (`kind` ASC)") + } +} + class AppDatabaseCallback : RoomDatabase.QueryCallback { override fun onQuery(sqlQuery: String, bindArgs: List) { if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt b/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt index f586b19..2dcc4f4 100644 --- a/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt +++ b/app/src/main/java/com/greenart7c3/citrine/database/EventDao.kt @@ -38,9 +38,17 @@ interface EventDao { @Transaction fun getById(id: String): EventWithTags? - @Query("DELETE FROM EventEntity WHERE EXISTS (SELECT 1 FROM TagEntity TagEntity WHERE id = TagEntity.pkEvent AND TagEntity.col0Name = 'expiration' AND CASE WHEN TagEntity.col1Value IS NULL THEN :now ELSE CAST(TagEntity.col1Value as INTEGER) END < :now)") @Transaction - fun deleteEventsWithExpirations(now: Long) + fun deleteEventsWithExpirations(now: Long) { + val ids = countEventsWithExpirations(now) + if (ids.isNotEmpty()) { + delete(ids) + } + } + + @Query("SELECT TagEntity.pkEvent FROM TagEntity TagEntity WHERE TagEntity.col0Name = 'expiration' AND CAST(TagEntity.col1Value as INTEGER) < :now") + @Transaction + fun countEventsWithExpirations(now: Long): List @Query("SELECT * FROM EventEntity WHERE pubkey = :pubkey and kind = 3 ORDER BY createdAt DESC, id ASC LIMIT 5") @Transaction @@ -58,6 +66,10 @@ interface EventDao { @Transaction fun getByKind(kind: Int, pubkey: String): List + @Query("SELECT id FROM EventEntity WHERE kind = :kind AND pubkey = :pubkey AND createdAt >= :createdAt") + @Transaction + fun getByKindNewest(kind: Int, pubkey: String, createdAt: Long): List + @Query("DELETE FROM EventEntity WHERE id in (:ids) and pubkey = :pubkey") @Transaction fun delete(ids: List, pubkey: String) @@ -78,9 +90,9 @@ interface EventDao { @Transaction fun deleteOldestByKind(kind: Int, pubkey: String) - @Query("SELECT * FROM EventEntity WHERE kind >= 20000 AND kind < 30000 LIMIT 1000") + @Query("SELECT id FROM EventEntity WHERE kind >= 20000 AND kind < 30000") @Transaction - fun getEphemeralEvents(): List + fun getEphemeralEvents(): List @Query( """ @@ -109,6 +121,26 @@ interface EventDao { @Transaction fun getOldestReplaceable(kind: Int, pubkey: String, dTagValue: String): List + @Query( + """ + SELECT EventEntity.id + FROM EventEntity EventEntity + WHERE EventEntity.pubkey = :pubkey + AND EventEntity.kind = :kind + AND EventEntity.createdAt >= :createdAt + and EventEntity.id in (SELECT EventEntity.id + FROM EventEntity EventEntity + INNER JOIN TagEntity TagEntity ON EventEntity.id = TagEntity.pkEvent + WHERE EventEntity.pubkey = :pubkey + AND EventEntity.kind = :kind + AND TagEntity.col0Name = 'd' + AND TagEntity.col1Value = :dTagValue + ) + """, + ) + @Transaction + fun getNewestReplaceable(kind: Int, pubkey: String, dTagValue: String, createdAt: Long): List + @Query("DELETE FROM EventEntity") @Transaction fun deleteAll() @@ -146,7 +178,11 @@ interface EventDao { @Transaction fun deleteAll(until: Long, pubKeys: String) - @Query("DELETE FROM EventEntity WHERE kind >= 20000 AND kind < 30000") @Transaction - fun deleteEphemeralEvents() + fun deleteEphemeralEvents() { + val ids = getEphemeralEvents() + if (ids.isNotEmpty()) { + delete(ids) + } + } } diff --git a/app/src/main/java/com/greenart7c3/citrine/database/EventEntity.kt b/app/src/main/java/com/greenart7c3/citrine/database/EventEntity.kt index 967c8e9..5a3ee51 100644 --- a/app/src/main/java/com/greenart7c3/citrine/database/EventEntity.kt +++ b/app/src/main/java/com/greenart7c3/citrine/database/EventEntity.kt @@ -25,6 +25,11 @@ import com.vitorpamplona.quartz.events.EventFactory name = "most_common_search_is_pubkey_kind", orders = [Index.Order.ASC, Index.Order.ASC], ), + Index( + value = ["kind"], + name = "most_common_search_is_kind", + orders = [Index.Order.ASC], + ), ], ) data class EventEntity( diff --git a/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt b/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt index 2572b27..791c39a 100644 --- a/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt +++ b/app/src/main/java/com/greenart7c3/citrine/server/CustomWebSocketServer.kt @@ -108,7 +108,8 @@ class CustomWebSocketServer( subscribe(subscriptionId, msgArray.drop(2), connection) } "EVENT" -> { - processEvent(msgArray.get(1), connection) + val event = Event.fromJson(msgArray.get(1)) + processEvent(event, connection) } "CLOSE" -> { EventSubscription.close(msgArray.get(1).asText()) @@ -126,6 +127,7 @@ class CustomWebSocketServer( suspend fun innerProcessEvent(event: Event, connection: Connection?) { if (!event.hasCorrectIDHash()) { + Log.d(Citrine.TAG, "event id hash verification failed ${event.toJson()}") connection?.session?.send( CommandResult.invalid( event, @@ -136,6 +138,7 @@ class CustomWebSocketServer( } if (!event.hasVerifiedSignature()) { + Log.d(Citrine.TAG, "event signature verification failed ${event.toJson()}") connection?.session?.send( CommandResult.invalid( event, @@ -146,17 +149,20 @@ class CustomWebSocketServer( } if (event.isExpired()) { + Log.d(Citrine.TAG, "event expired ${event.toJson()}") connection?.session?.send(CommandResult.invalid(event, "event expired").toJson()) return } if (Settings.allowedKinds.isNotEmpty() && event.kind !in Settings.allowedKinds) { + Log.d(Citrine.TAG, "kind not allowed ${event.toJson()}") connection?.session?.send(CommandResult.invalid(event, "kind not allowed").toJson()) return } if (Settings.allowedTaggedPubKeys.isNotEmpty() && event.taggedUsers().isNotEmpty() && event.taggedUsers().none { it in Settings.allowedTaggedPubKeys }) { if (Settings.allowedPubKeys.isEmpty() || (event.pubKey !in Settings.allowedPubKeys)) { + Log.d(Citrine.TAG, "tagged pubkey not allowed ${event.toJson()}") connection?.session?.send(CommandResult.invalid(event, "tagged pubkey not allowed").toJson()) return } @@ -164,6 +170,7 @@ class CustomWebSocketServer( if (Settings.allowedPubKeys.isNotEmpty() && event.pubKey !in Settings.allowedPubKeys) { if (Settings.allowedTaggedPubKeys.isEmpty() || event.taggedUsers().none { it in Settings.allowedTaggedPubKeys }) { + Log.d(Citrine.TAG, "pubkey not allowed ${event.toJson()}") connection?.session?.send(CommandResult.invalid(event, "pubkey not allowed").toJson()) return } @@ -171,11 +178,28 @@ class CustomWebSocketServer( when { event.shouldDelete() -> deleteEvent(event, connection) - event.isParameterizedReplaceable() -> handleParameterizedReplaceable(event, connection) - event.shouldOverwrite() -> override(event, connection) + event.isParameterizedReplaceable() -> { + val newest = appDatabase.eventDao().getNewestReplaceable(event.kind, event.pubKey, event.tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: "", event.createdAt) + if (newest.isNotEmpty()) { + Log.d(Citrine.TAG, "newest event already in database ${event.toJson()}") + connection?.session?.send(CommandResult.invalid(event, "newest event already in database").toJson()) + return + } + handleParameterizedReplaceable(event, connection) + } + event.shouldOverwrite() -> { + val newest = appDatabase.eventDao().getByKindNewest(event.kind, event.pubKey, event.createdAt) + if (newest.isNotEmpty()) { + Log.d(Citrine.TAG, "newest event already in database ${event.toJson()}") + connection?.session?.send(CommandResult.invalid(event, "newest event already in database").toJson()) + return + } + override(event, connection) + } else -> { val eventEntity = appDatabase.eventDao().getById(event.id) if (eventEntity != null) { + Log.d(Citrine.TAG, "Event already in database ${event.toJson()}") connection?.session?.send(CommandResult.duplicated(event).toJson()) return } @@ -186,8 +210,7 @@ class CustomWebSocketServer( connection?.session?.send(CommandResult.ok(event).toJson()) } - private suspend fun processEvent(eventNode: JsonNode, connection: Connection?) { - val event = objectMapper.treeToValue(eventNode, Event::class.java) + private suspend fun processEvent(event: Event, connection: Connection?) { innerProcessEvent(event, connection) } diff --git a/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt b/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt index 34f9446..8eebed9 100644 --- a/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt +++ b/app/src/main/java/com/greenart7c3/citrine/service/WebSocketServerService.kt @@ -21,67 +21,25 @@ import com.greenart7c3.citrine.R import com.greenart7c3.citrine.database.AppDatabase import com.greenart7c3.citrine.server.CustomWebSocketServer import com.greenart7c3.citrine.server.EventSubscription -import com.greenart7c3.citrine.server.OlderThan import com.greenart7c3.citrine.server.Settings import com.greenart7c3.citrine.utils.ExportDatabaseUtils -import com.vitorpamplona.quartz.utils.TimeUtils import java.util.Timer import java.util.TimerTask import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking class WebSocketServerService : Service() { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val binder = LocalBinder() private var timer: Timer? = null - private var job: Job? = null inner class LocalBinder : Binder() { fun getService(): WebSocketServerService = this@WebSocketServerService } - private fun eventsToDelete(database: AppDatabase) { - job?.cancel() - job = Citrine.getInstance().applicationScope.launch(Dispatchers.IO) { - try { - if (Settings.deleteEphemeralEvents) { - Log.d(Citrine.TAG, "Deleting ephemeral events") - database.eventDao().deleteEphemeralEvents() - } - - if (Settings.deleteExpiredEvents) { - Log.d(Citrine.TAG, "Deleting expired events") - database.eventDao().deleteEventsWithExpirations(TimeUtils.now()) - } - - if (Settings.deleteEventsOlderThan != OlderThan.NEVER) { - val until = when (Settings.deleteEventsOlderThan) { - OlderThan.DAY -> TimeUtils.oneDayAgo() - OlderThan.WEEK -> TimeUtils.oneWeekAgo() - OlderThan.MONTH -> TimeUtils.now() - TimeUtils.ONE_MONTH - OlderThan.YEAR -> TimeUtils.now() - TimeUtils.ONE_YEAR - else -> 0 - } - if (until > 0) { - Log.d(Citrine.TAG, "Deleting old events (older than ${Settings.deleteEventsOlderThan})") - if (Settings.neverDeleteFrom.isNotEmpty()) { - val pubKeys = Settings.neverDeleteFrom.joinToString(separator = ",") { "'$it'" } - database.eventDao().deleteAll(until, pubKeys) - } else { - database.eventDao().deleteAll(until) - } - } - } - } catch (e: Exception) { - if (e is CancellationException) throw e - Log.e(Citrine.TAG, "Error deleting events", e) - } - } - } - @SuppressLint("UnspecifiedRegisterReceiverFlag") override fun onCreate() { super.onCreate() @@ -95,9 +53,11 @@ class WebSocketServerService : Service() { timer?.schedule( object : TimerTask() { override fun run() { - eventsToDelete(database) + runBlocking { + Citrine.getInstance().eventsToDelete(database) + } - if (Settings.autoBackup && Settings.autoBackupFolder.isNotBlank()) { + if (Settings.autoBackup && Settings.autoBackupFolder.isNotBlank() && !Citrine.getInstance().isImportingEvents) { try { val folder = DocumentFile.fromTreeUri(this@WebSocketServerService, Settings.autoBackupFolder.toUri()) folder?.let { diff --git a/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt b/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt index 8cf03da..d678c68 100644 --- a/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt +++ b/app/src/main/java/com/greenart7c3/citrine/ui/HomeViewModel.kt @@ -652,6 +652,7 @@ class HomeViewModel : ViewModel() { } try { + Citrine.getInstance().isImportingEvents = true state.value = state.value.copy( loading = true, progress = context.getString(R.string.reading_file, file.name), @@ -705,7 +706,7 @@ class HomeViewModel : ViewModel() { progress = "", loading = false, ) - + Citrine.getInstance().isImportingEvents = false onFinished() Citrine.getInstance().applicationScope.launch(Dispatchers.Main) { Toast.makeText( @@ -715,6 +716,7 @@ class HomeViewModel : ViewModel() { ).show() } } catch (e: Exception) { + Citrine.getInstance().isImportingEvents = false if (e is CancellationException) throw e Log.d(Citrine.TAG, e.message ?: "", e) Citrine.getInstance().applicationScope.launch(Dispatchers.Main) { @@ -743,4 +745,10 @@ class HomeViewModel : ViewModel() { } super.onCleared() } + + fun setProgress(message: String) { + _state.value = _state.value.copy( + progress = message, + ) + } }