From 4724ae5728909ab53e5729fb3360be116702cc03 Mon Sep 17 00:00:00 2001 From: James Magahern Date: Wed, 9 Aug 2023 01:32:38 -0700 Subject: [PATCH] Started working on Cache Database --- backend/build.gradle | 4 ++ .../backend/db/CachedChatDatabase.kt | 46 ++++++++++++++ .../backend/db/model/Conversation.kt | 63 +++++++++++++++++++ .../backend/db/model/Date+Instant.kt | 37 +++++++++++ .../kordophone/backend/db/model/Message.kt | 49 +++++++++++++++ .../kordophone/backend/model/Conversation.kt | 2 +- .../kordophone/backend/DatabaseTests.kt | 31 +++++++++ build.gradle | 1 + 8 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt create mode 100644 backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt create mode 100644 backend/src/main/java/net/buzzert/kordophone/backend/db/model/Date+Instant.kt create mode 100644 backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt create mode 100644 backend/src/test/java/net/buzzert/kordophone/backend/DatabaseTests.kt diff --git a/backend/build.gradle b/backend/build.gradle index 0a92978..b66ebea 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.library' id 'org.jetbrains.kotlin.android' + id 'io.realm.kotlin' } android { @@ -46,6 +47,9 @@ dependencies { implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.google.code.gson:gson:2.9.0' + // Realm + implementation 'io.realm.kotlin:library-base:1.10.0' + // https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core implementation group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.7.3', ext: 'pom' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt new file mode 100644 index 0000000..658b0d0 --- /dev/null +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/CachedChatDatabase.kt @@ -0,0 +1,46 @@ +package net.buzzert.kordophone.backend.db + +import io.realm.kotlin.Realm +import io.realm.kotlin.RealmConfiguration +import net.buzzert.kordophone.backend.db.model.Conversation +import net.buzzert.kordophone.backend.db.model.toDatabaseConversation +import net.buzzert.kordophone.backend.model.Conversation as ModelConversation + +internal class CachedChatDatabase (private val realmConfig: RealmConfiguration) { + companion object { + private val schema = setOf(Conversation::class) + + fun liveDatabase(): CachedChatDatabase { + return CachedChatDatabase( + RealmConfiguration.Builder(schema = schema) + .name("chat-cache") + .build() + ) + } + + fun testDatabase(): CachedChatDatabase { + return CachedChatDatabase( + RealmConfiguration.Builder(schema = schema) + .name("chat-cache-test") + .inMemory() + .build() + ) + } + } + + private val realm = Realm.open(realmConfig) + + fun writeConversations(conversations: List) { + val dbConversations = conversations.map { it.toDatabaseConversation() } + realm.writeBlocking { + dbConversations.forEach { + copyToRealm(it) + } + } + } + + fun fetchConversations(): List { + val items = realm.query(Conversation::class).find() + return items.map { it.toConversation() } + } +} \ No newline at end of file diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt new file mode 100644 index 0000000..ebe31c5 --- /dev/null +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Conversation.kt @@ -0,0 +1,63 @@ +package net.buzzert.kordophone.backend.db.model + +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.realmListOf +import io.realm.kotlin.ext.toRealmList +import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmList +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey +import net.buzzert.kordophone.backend.model.GUID +import org.mongodb.kbson.ObjectId +import java.time.Instant +import net.buzzert.kordophone.backend.model.Conversation as ModelConversation +import java.util.Date + +open class Conversation( + @PrimaryKey + var _id: String, + + var displayName: String?, + var participants: RealmList, + var date: RealmInstant, + var unreadCount: Int, + var lastMessagePreview: String, + var guid: GUID, + + // TODO: Not sure how to do this yet... + // var messages: RealmList, +): RealmObject +{ + constructor(): this( + _id = ObjectId().toString(), + displayName = null, + participants = realmListOf(), + date = RealmInstant.now(), + unreadCount = 0, + lastMessagePreview = "", + guid = "", + ) + + fun toConversation(): ModelConversation { + return ModelConversation( + displayName = displayName, + participants = participants!!.toList(), + date = Date.from(date.toInstant()), + unreadCount = unreadCount, + guid = guid, + lastMessagePreview = lastMessagePreview + ) + } +} + +fun ModelConversation.toDatabaseConversation(): Conversation { + val from = this + return Conversation().apply { + displayName = from.displayName + participants = from.participants.toRealmList() + date = from.date.toInstant().toRealmInstant() + unreadCount = from.unreadCount + lastMessagePreview = from.lastMessagePreview + guid = from.guid + } +} diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Date+Instant.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Date+Instant.kt new file mode 100644 index 0000000..8cd9af1 --- /dev/null +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Date+Instant.kt @@ -0,0 +1,37 @@ +package net.buzzert.kordophone.backend.db.model + +import io.realm.kotlin.types.RealmInstant +import java.time.Instant + +// Copied from Realm's documentation +// https://www.mongodb.com/docs/realm/sdk/kotlin/realm-database/schemas/supported-types/ + +fun RealmInstant.toInstant(): Instant { + val sec: Long = this.epochSeconds + // The value always lies in the range `-999_999_999..999_999_999`. + // minus for timestamps before epoch, positive for after + val nano: Int = this.nanosecondsOfSecond + return if (sec >= 0) { // For positive timestamps, conversion can happen directly + Instant.ofEpochSecond(sec, nano.toLong()) + } else { + // For negative timestamps, RealmInstant starts from the higher value with negative + // nanoseconds, while Instant starts from the lower value with positive nanoseconds + // TODO This probably breaks at edge cases like MIN/MAX + Instant.ofEpochSecond(sec - 1, 1_000_000 + nano.toLong()) + } +} + +fun Instant.toRealmInstant(): RealmInstant { + val sec: Long = this.epochSecond + // The value is always positive and lies in the range `0..999_999_999`. + val nano: Int = this.nano + + return if (sec >= 0) { // For positive timestamps, conversion can happen directly + RealmInstant.from(sec, nano) + } else { + // For negative timestamps, RealmInstant starts from the higher value with negative + // nanoseconds, while Instant starts from the lower value with positive nanoseconds + // TODO This probably breaks at edge cases like MIN/MAX + RealmInstant.from(sec + 1, -1_000_000 + nano) + } +} \ No newline at end of file diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt new file mode 100644 index 0000000..9f41962 --- /dev/null +++ b/backend/src/main/java/net/buzzert/kordophone/backend/db/model/Message.kt @@ -0,0 +1,49 @@ +package net.buzzert.kordophone.backend.db.model + +import android.view.Display.Mode +import io.realm.kotlin.Realm +import io.realm.kotlin.types.RealmInstant +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PrimaryKey +import net.buzzert.kordophone.backend.model.GUID +import org.mongodb.kbson.ObjectId +import net.buzzert.kordophone.backend.model.Message as ModelMessage +import java.util.Date + +open class Message ( + @PrimaryKey + var _id: String, + + var text: String, + var guid: GUID, + var sender: String?, + var date: RealmInstant, +): RealmObject +{ + constructor(): this( + _id = ObjectId().toString(), + text = "", + guid = "", + sender = null, + date = RealmInstant.now(), + ) + + fun toMessage(): ModelMessage { + return ModelMessage( + text = text, + guid = guid, + sender = sender, + date = Date.from(date.toInstant()), + ) + } +} + +fun ModelMessage.toDatabaseMessage(): Message { + val from = this + return Message().apply { + text = from.text + guid = from.guid + sender = from.sender + date = from.date.toInstant().toRealmInstant() + } +} diff --git a/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt b/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt index 947f222..49db800 100644 --- a/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt +++ b/backend/src/main/java/net/buzzert/kordophone/backend/model/Conversation.kt @@ -10,7 +10,7 @@ data class Conversation( val date: Date, @SerializedName("participantDisplayNames") - val participants: List?, + val participants: List, @SerializedName("displayName") val displayName: String?, diff --git a/backend/src/test/java/net/buzzert/kordophone/backend/DatabaseTests.kt b/backend/src/test/java/net/buzzert/kordophone/backend/DatabaseTests.kt new file mode 100644 index 0000000..28cd9f9 --- /dev/null +++ b/backend/src/test/java/net/buzzert/kordophone/backend/DatabaseTests.kt @@ -0,0 +1,31 @@ +package net.buzzert.kordophone.backend + +import net.buzzert.kordophone.backend.db.CachedChatDatabase +import net.buzzert.kordophone.backend.model.Conversation +import org.junit.Assert.assertEquals +import org.junit.Test +import java.util.Date + +class DatabaseTests { + @Test + fun testCreateRetrieve() { + val db = CachedChatDatabase.testDatabase() + + val conversation = Conversation( + date = Date(), + participants = listOf("james@magahern.com"), + displayName = "Test", + unreadCount = 1, + lastMessagePreview = "Hello!", + guid = "1234", + ) + + db.writeConversations(listOf(conversation)) + + val readBackConversations = db.fetchConversations() + assertEquals(readBackConversations.count(), 1) + + val readConversation = readBackConversations[0] + assertEquals(readConversation, conversation) + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 1afff19..524cf5e 100644 --- a/build.gradle +++ b/build.gradle @@ -8,4 +8,5 @@ plugins { id 'com.android.application' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false id 'org.jetbrains.kotlin.android' version '1.9.0' apply false + id 'io.realm.kotlin' version '1.10.0' apply false } \ No newline at end of file