From 8b70d80070209eb19791eecf207a8fdefea17a4e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 6 Mar 2019 17:42:31 -0500 Subject: Make db entities immutable --- .../com/pitchedapps/frost/db/CookieDbTest.kt | 34 +++--- .../com/pitchedapps/frost/db/FbTabsDbTest.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 20 ++++ .../pitchedapps/frost/activities/LoginActivity.kt | 3 +- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 6 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 7 +- .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 28 +++-- .../com/pitchedapps/frost/db/NotificationDb.kt | 64 +++++++++++ .../1.json | 125 ++++++++++++++++++++- 9 files changed, 259 insertions(+), 40 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt (limited to 'app/src') diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt index 351490e2..20592347 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -6,13 +6,15 @@ import kotlin.test.assertEquals import kotlin.test.assertNull class CookieDbTest : BaseDbTest() { + + private val dao get() = db.cookieDao() @Test fun basicCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - val cookies = db.cookieDao().selectAll() + dao.insertCookie(cookie) + val cookies = dao.selectAll() assertEquals(listOf(cookie), cookies, "Cookie mismatch") } } @@ -22,15 +24,15 @@ class CookieDbTest : BaseDbTest() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - db.cookieDao().deleteById(cookie.id + 1) + dao.insertCookie(cookie) + dao.deleteById(cookie.id + 1) assertEquals( listOf(cookie), - db.cookieDao().selectAll(), + dao.selectAll(), "Cookie list should be the same after inexistent deletion" ) - db.cookieDao().deleteById(cookie.id) - assertEquals(emptyList(), db.cookieDao().selectAll(), "Cookie list should be empty after deletion") + dao.deleteById(cookie.id) + assertEquals(emptyList(), dao.selectAll(), "Cookie list should be empty after deletion") } } @@ -38,18 +40,18 @@ class CookieDbTest : BaseDbTest() { fun insertReplaceCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - assertEquals(listOf(cookie), db.cookieDao().selectAll(), "Cookie insertion failed") - db.cookieDao().insertCookie(cookie.copy(name = "testName2")) + dao.insertCookie(cookie) + assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed") + dao.insertCookie(cookie.copy(name = "testName2")) assertEquals( listOf(cookie.copy(name = "testName2")), - db.cookieDao().selectAll(), + dao.selectAll(), "Cookie replacement failed" ) - db.cookieDao().insertCookie(cookie.copy(id = 123L)) + dao.insertCookie(cookie.copy(id = 123L)) assertEquals( setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), - db.cookieDao().selectAll().toSet(), + dao.selectAll().toSet(), "New cookie insertion failed" ) } @@ -59,9 +61,9 @@ class CookieDbTest : BaseDbTest() { fun selectCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - db.cookieDao().insertCookie(cookie) - assertEquals(cookie, db.cookieDao().selectById(cookie.id), "Cookie selection failed") - assertNull(db.cookieDao().selectById(cookie.id + 1), "Inexistent cookie selection failed") + dao.insertCookie(cookie) + assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed") + assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed") } } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt index a2dce692..91a0bf9a 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/FbTabsDbTest.kt @@ -7,6 +7,8 @@ import kotlin.test.Test import kotlin.test.assertEquals class FbTabsDbTest : BaseDbTest() { + + private val dao get() = db.tabDao() /** * Note that order is also preserved here @@ -15,18 +17,18 @@ class FbTabsDbTest : BaseDbTest() { fun save() { val tabs = listOf(FbItem.ACTIVITY_LOG, FbItem.BIRTHDAYS, FbItem.EVENTS, FbItem.MARKETPLACE, FbItem.ACTIVITY_LOG) runBlocking { - db.tabDao().save(tabs) - assertEquals(tabs, db.tabDao().selectAll(), "Tab saving failed") + dao.save(tabs) + assertEquals(tabs, dao.selectAll(), "Tab saving failed") val newTabs = listOf(FbItem.PAGES, FbItem.MENU) - db.tabDao().save(newTabs) - assertEquals(newTabs, db.tabDao().selectAll(), "Tab saving does not delete preexisting items") + dao.save(newTabs) + assertEquals(newTabs, dao.selectAll(), "Tab saving does not delete preexisting items") } } @Test fun defaultRetrieve() { runBlocking { - assertEquals(defaultTabs(), db.tabDao().selectAll(), "Default retrieval failed") + assertEquals(defaultTabs(), dao.selectAll(), "Default retrieval failed") } } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt new file mode 100644 index 00000000..12092bf6 --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -0,0 +1,20 @@ +package com.pitchedapps.frost.db + +import com.pitchedapps.frost.facebook.FbItem +import com.pitchedapps.frost.facebook.defaultTabs +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals + +class NotificationDbTest : BaseDbTest() { + + private val dao get() = db.notifDao() + + /** + * Note that order is also preserved here + */ + @Test + fun save() { + + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 5649cc73..27dbc37a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -184,8 +184,7 @@ class LoginActivity : BaseActivity() { } if (cookie.name?.isNotBlank() == false && result != cookie.name) { - cookie.name = result - cookieDao.insertCookie(cookie) + cookieDao.insertCookie(cookie.copy(name = result)) } cookie.name ?: "" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt index 128abae3..34a88011 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -38,9 +38,9 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class CookieEntity( @androidx.room.PrimaryKey - var id: Long, - var name: String?, - var cookie: String? + val id: Long, + val name: String?, + val cookie: String? ) : Parcelable { override fun toString(): String = "CookieEntity(${hashCode()})" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt index 161ed93d..a37c2ee9 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -10,9 +10,14 @@ import org.koin.standalone.StandAloneContext interface FrostPrivateDao { fun cookieDao(): CookieDao + fun notifDao(): NotificationDao } -@Database(entities = [CookieEntity::class], version = 1, exportSchema = true) +@Database( + entities = [CookieEntity::class, NotificationInfoEntity::class, NotificationEntity::class], + version = 1, + exportSchema = true +) abstract class FrostPrivateDatabase : RoomDatabase(), FrostPrivateDao { companion object { const val DATABASE_NAME = "frost-priv-db" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt index 44e62938..582d57fb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -33,7 +33,7 @@ import com.raizlabs.android.dbflow.kotlinextensions.fastSave import com.raizlabs.android.dbflow.kotlinextensions.from import com.raizlabs.android.dbflow.kotlinextensions.select import com.raizlabs.android.dbflow.structure.BaseModel -import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext /** @@ -41,42 +41,48 @@ import kotlinx.coroutines.withContext */ @Entity(tableName = "tabs") -data class FbTabEntity(@androidx.room.PrimaryKey var position: Int, var tab: FbItem) +data class FbTabEntity(@androidx.room.PrimaryKey val position: Int, val tab: FbItem) @Dao interface FbTabDao { @Query("SELECT * FROM tabs ORDER BY position ASC") - suspend fun _selectAll(): List + fun _selectAll(): List @Query("DELETE FROM tabs") - suspend fun _deleteAll() + fun _deleteAll() @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun _insertAll(items: List) + fun _insertAll(items: List) + + @Transaction + fun _save(items: List) { + _deleteAll() + _insertAll(items) + } } /** * Saving tabs operates by deleting all db items and saving the new list. * Transactions can't be done with suspensions in room as switching threads during the process * may result in a deadlock. - * In this case, there may be a chance that the 'transaction' completes partially, - * but we'll just fallback to the default anyways. + * That's why we disallow thread switching within the transaction, but wrap the entire thing in a coroutine */ suspend fun FbTabDao.save(items: List) { - withContext(NonCancellable) { - _deleteAll() + withContext(Dispatchers.IO) { val entities = (items.takeIf { it.isNotEmpty() } ?: defaultTabs()).mapIndexed { index, fbItem -> FbTabEntity( index, fbItem ) } - _insertAll(entities) + _save(entities) } } -suspend fun FbTabDao.selectAll(): List = _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +suspend fun FbTabDao.selectAll(): List = withContext(Dispatchers.IO) { + _selectAll().map { it.tab }.takeIf { it.isNotEmpty() } ?: defaultTabs() +} object FbItemConverter { @androidx.room.TypeConverter diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt index 5b501792..56c3c5ac 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -16,6 +16,16 @@ */ package com.pitchedapps.frost.db +import androidx.room.Dao +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Ignore +import androidx.room.Index +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Relation +import androidx.room.Transaction import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database @@ -32,6 +42,60 @@ import com.raizlabs.android.dbflow.sql.SQLiteType import com.raizlabs.android.dbflow.sql.migration.AlterTableMigration import com.raizlabs.android.dbflow.structure.BaseModel +@Entity( + tableName = "notification_info", + foreignKeys = [ForeignKey( + entity = CookieEntity::class, + parentColumns = ["id"], childColumns = ["id"], onDelete = ForeignKey.CASCADE + )] +) +data class NotificationInfoEntity( + @androidx.room.PrimaryKey val id: Long, + val epoch: Long, + val epochIm: Long +) + +@Entity( + tableName = "notifications", + foreignKeys = [ForeignKey( + entity = NotificationInfoEntity::class, + parentColumns = ["id"], + childColumns = ["userId"], + onDelete = ForeignKey.CASCADE + )], + indices = [Index("userId")] +) +data class NotificationEntity( + @androidx.room.PrimaryKey var id: Long, + val userId: Long, + val href: String, + val title: String?, + val text: String, + val timestamp: Long, + val profileUrl: String? +) { + @Ignore + val notifId = Math.abs(id.toInt()) +} + +data class NotificationInfo( + @Embedded + val info: NotificationInfoEntity, + @Relation(parentColumn = "id", entityColumn = "userId") + val notifications: List = emptyList() +) + +@Dao +interface NotificationDao { + + @Query("SELECT * FROM notification_info WHERE id = :id") + fun selectById(id: Long): NotificationInfo? + + @Transaction + @Insert + fun insertInfo(info: NotificationInfoEntity) +} + /** * Created by Allan Wang on 2017-05-30. */ diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json index 9816651c..f0909a17 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "ba6f1d7e47823dac6ed1622fec043d5d", + "identityHash": "d0911840f629ef359aaf8ac8635dc571", "entities": [ { "tableName": "cookies", @@ -35,12 +35,133 @@ }, "indices": [], "foreignKeys": [] + }, + { + "tableName": "notification_info", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `epoch` INTEGER NOT NULL, `epochIm` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`id`) REFERENCES `cookies`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "epoch", + "columnName": "epoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "epochIm", + "columnName": "epochIm", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, PRIMARY KEY(`id`), FOREIGN KEY(`userId`) REFERENCES `notification_info`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "href", + "columnName": "href", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileUrl", + "columnName": "profileUrl", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notifications_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "notification_info", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "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, \"ba6f1d7e47823dac6ed1622fec043d5d\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d0911840f629ef359aaf8ac8635dc571\")" ] } } \ No newline at end of file -- cgit v1.2.3