From c917dc13dabe7781a097383ae89f2d00f32fffcb Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 18:31:47 -0500 Subject: Create initial room models --- .../1.json | 45 ++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json new file mode 100644 index 00000000..7bc2e5e9 --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -0,0 +1,45 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "6af67964d00193c6a3aa2a20200ea0ea", + "entities": [ + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, \"6af67964d00193c6a3aa2a20200ea0ea\")" + ] + } +} \ No newline at end of file -- cgit v1.2.3 From 9a1d9719ad6559054ea1bc4f21f8559559eb9cda Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 5 Mar 2019 21:25:46 -0500 Subject: Port rest of cookie items --- app/src/main/AndroidManifest.xml | 1 + app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt | 12 +++++++++--- app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 13 +++---------- .../main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt | 1 - .../main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt | 12 +++++++++++- .../com.pitchedapps.frost.db.FrostPrivateDatabase/1.json | 11 ++++++----- 6 files changed, 30 insertions(+), 20 deletions(-) (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 68e84e4d..ad1fcbdc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ ) + @Query("DELETE FROM cookies WHERE id = :id") suspend fun deleteById(id: Long) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 6f79da43..02718b78 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -22,7 +22,6 @@ import android.webkit.CookieManager import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity import com.pitchedapps.frost.db.FrostDatabase -import com.pitchedapps.frost.db.removeCookie import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.cookies diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt index 0e9d94e6..594b0acd 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -24,15 +24,19 @@ import android.util.AttributeSet import android.view.View import android.view.ViewGroup import ca.allanwang.kau.utils.AnimHolder +import ca.allanwang.kau.utils.launchMain import com.pitchedapps.frost.contracts.FrostContentContainer import com.pitchedapps.frost.contracts.FrostContentCore import com.pitchedapps.frost.contracts.FrostContentParent +import com.pitchedapps.frost.db.FrostDatabase +import com.pitchedapps.frost.db.currentCookie import com.pitchedapps.frost.facebook.FB_HOME_URL import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC import com.pitchedapps.frost.facebook.USER_AGENT_FULL import com.pitchedapps.frost.fragments.WebFragment import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.ctxCoroutine import com.pitchedapps.frost.utils.frostDownload import com.pitchedapps.frost.web.FrostChromeClient import com.pitchedapps.frost.web.FrostJSI @@ -80,7 +84,13 @@ class FrostWebView @JvmOverloads constructor( webChromeClient = FrostChromeClient(this) addJavascriptInterface(FrostJSI(this), "Frost") setBackgroundColor(Color.TRANSPARENT) - setDownloadListener(context::frostDownload) + val db = FrostDatabase.get() + setDownloadListener { url, userAgent, contentDisposition, mimetype, contentLength -> + context.ctxCoroutine.launchMain { + val cookie = db.cookieDao().currentCookie() ?: return@launchMain + context.frostDownload(cookie, url, userAgent, contentDisposition, mimetype, contentLength) + } + } return this } 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 7bc2e5e9..9816651c 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "6af67964d00193c6a3aa2a20200ea0ea", + "identityHash": "ba6f1d7e47823dac6ed1622fec043d5d", "entities": [ { "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -18,13 +18,13 @@ "fieldPath": "name", "columnName": "name", "affinity": "TEXT", - "notNull": true + "notNull": false }, { "fieldPath": "cookie", "columnName": "cookie", "affinity": "TEXT", - "notNull": true + "notNull": false } ], "primaryKey": { @@ -37,9 +37,10 @@ "foreignKeys": [] } ], + "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, \"6af67964d00193c6a3aa2a20200ea0ea\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"ba6f1d7e47823dac6ed1622fec043d5d\")" ] } } \ No newline at end of file -- cgit v1.2.3 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/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') 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 From b417cc51b28d558195c4cc075d0e6ce8192bf270 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Wed, 6 Mar 2019 23:35:04 -0500 Subject: Update notif database --- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 81 ++++++++++++++- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 6 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 2 +- .../com/pitchedapps/frost/db/NotificationDb.kt | 112 +++++++++++++++------ .../frost/services/FrostNotifications.kt | 1 + .../1.json | 79 +++++---------- .../1.json | 1 + 7 files changed, 190 insertions(+), 92 deletions(-) (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 12092bf6..2e9f1875 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -1,20 +1,93 @@ package com.pitchedapps.frost.db -import com.pitchedapps.frost.facebook.FbItem -import com.pitchedapps.frost.facebook.defaultTabs +import android.database.sqlite.SQLiteConstraintException +import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NotificationContent import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue class NotificationDbTest : BaseDbTest() { private val dao get() = db.notifDao() + private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id") + + private fun notifContent(id: Long, cookie: CookieEntity, time: Long = id) = NotificationContent( + data = cookie, + id = id, + href = "", + title = null, + text = "", + timestamp = time, + profileUrl = null + ) + + @Test + fun saveAndRetrieve() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) + assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received") + } + } + /** - * Note that order is also preserved here + * Primary key is both id and userId, in the event that the same notification to multiple users has the same id */ @Test - fun save() { + fun primaryKeyCheck() { + runBlocking { + val cookie1 = cookie(12345L) + val cookie2 = cookie(12L) + val notifs1 = (0L..2L).map { notifContent(it, cookie1) } + val notifs2 = notifs1.map { it.copy(data = cookie2) } + db.cookieDao().insertCookie(cookie1) + db.cookieDao().insertCookie(cookie2) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2) + } + } + @Test + fun cascadeDeletion() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + db.cookieDao().deleteById(cookie.id) + val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) + assertTrue(dbNotifs.isEmpty(), "Cascade deletion failed") + } + } + + @Test + fun latestEpoch() { + val cookie = cookie(12345L) + // Unique unsorted ids + val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } + runBlocking { + assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed") + db.cookieDao().insertCookie(cookie) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) + assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed") + } + } + + @Test + fun insertionWithInvalidCookies() { + assertFailsWith(SQLiteConstraintException::class) { + runBlocking { + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))) + } + } } } \ No newline at end of file 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 34a88011..d5347f18 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -17,6 +17,7 @@ package com.pitchedapps.frost.db import android.os.Parcelable +import androidx.room.ColumnInfo import androidx.room.Dao import androidx.room.Entity import androidx.room.Insert @@ -38,6 +39,7 @@ import kotlinx.android.parcel.Parcelize @Parcelize data class CookieEntity( @androidx.room.PrimaryKey + @ColumnInfo(name = "cookie_id") val id: Long, val name: String?, val cookie: String? @@ -53,7 +55,7 @@ interface CookieDao { @Query("SELECT * FROM cookies") suspend fun selectAll(): List - @Query("SELECT * FROM cookies WHERE id = :id") + @Query("SELECT * FROM cookies WHERE cookie_id = :id") suspend fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -62,7 +64,7 @@ interface CookieDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCookies(cookies: List) - @Query("DELETE FROM cookies WHERE id = :id") + @Query("DELETE FROM cookies WHERE cookie_id = :id") suspend fun deleteById(id: Long) } 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 a37c2ee9..1ce9e7c2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -14,7 +14,7 @@ interface FrostPrivateDao { } @Database( - entities = [CookieEntity::class, NotificationInfoEntity::class, NotificationEntity::class], + entities = [CookieEntity::class, NotificationEntity::class], version = 1, exportSchema = true ) 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 56c3c5ac..9622ec47 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -16,16 +16,17 @@ */ package com.pitchedapps.frost.db +import androidx.room.ColumnInfo 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.OnConflictStrategy import androidx.room.Query -import androidx.room.Relation import androidx.room.Transaction +import com.pitchedapps.frost.services.NotificationContent import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.ConflictAction import com.raizlabs.android.dbflow.annotation.Database @@ -41,59 +42,108 @@ import com.raizlabs.android.dbflow.kotlinextensions.where 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 -) +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @Entity( tableName = "notifications", + primaryKeys = ["notif_id", "userId"], foreignKeys = [ForeignKey( - entity = NotificationInfoEntity::class, - parentColumns = ["id"], + entity = CookieEntity::class, + parentColumns = ["cookie_id"], childColumns = ["userId"], onDelete = ForeignKey.CASCADE )], - indices = [Index("userId")] + indices = [Index("notif_id"), Index("userId")] ) data class NotificationEntity( - @androidx.room.PrimaryKey var id: Long, + @ColumnInfo(name = "notif_id") + val id: Long, val userId: Long, val href: String, val title: String?, val text: String, val timestamp: Long, - val profileUrl: String? + val profileUrl: String?, + // Type essentially refers to channel + val type: String ) { - @Ignore - val notifId = Math.abs(id.toInt()) + constructor( + type: String, + content: NotificationContent + ) : this( + content.id, + content.data.id, + content.href, + content.title, + content.text, + content.timestamp, + content.profileUrl, + type + ) } -data class NotificationInfo( +data class NotificationContentEntity( @Embedded - val info: NotificationInfoEntity, - @Relation(parentColumn = "id", entityColumn = "userId") - val notifications: List = emptyList() -) + val cookie: CookieEntity, + @Embedded + val notif: NotificationEntity +) { + fun toNotifContent() = NotificationContent( + data = cookie, + id = notif.id, + href = notif.href, + title = notif.title, + text = notif.text, + timestamp = notif.timestamp, + profileUrl = notif.profileUrl + ) +} @Dao interface NotificationDao { - @Query("SELECT * FROM notification_info WHERE id = :id") - fun selectById(id: Long): NotificationInfo? + /** + * Note that notifications are guaranteed to be ordered by descending timestamp + */ + @Transaction + @Query("SELECT * FROM cookies INNER JOIN notifications ON cookie_id = userId WHERE userId = :userId AND type = :type ORDER BY timestamp DESC") + fun _selectNotifications(userId: Long, type: String): List + @Query("SELECT timestamp FROM notifications WHERE userId = :userId AND type = :type ORDER BY timestamp DESC LIMIT 1") + fun _selectEpoch(userId: Long, type: String): Long? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun _insertNotifications(notifs: List) + + @Query("DELETE FROM notifications WHERE userId = :userId AND type = :type") + fun _deleteNotifications(userId: Long, type: String) + + /** + * It is assumed that the notification batch comes from the same user + */ @Transaction - @Insert - fun insertInfo(info: NotificationInfoEntity) + fun _saveNotifications(type: String, notifs: List) { + val userId = notifs.firstOrNull()?.data?.id ?: return + val entities = notifs.map { NotificationEntity(type, it) } + _deleteNotifications(userId, type) + _insertNotifications(entities) + } +} + +suspend fun NotificationDao.selectNotifications(userId: Long, type: String): List = + withContext(Dispatchers.IO) { + _selectNotifications(userId, type).map { it.toNotifContent() } + } + +suspend fun NotificationDao.saveNotifications(type: String, notifs: List) { + withContext(Dispatchers.IO) { + _saveNotifications(type, notifs) + } +} + +suspend fun NotificationDao.latestEpoch(userId: Long, type: String): Long = withContext(Dispatchers.IO) { + _selectEpoch(userId, type) ?: -1 } /** diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index abd871b3..7da3c128 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -248,6 +248,7 @@ enum class NotificationType( * Notification data holder */ data class NotificationContent( + // TODO replace data with userId? val data: CookieEntity, val id: Long, val href: String, 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 f0909a17..c382bce7 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -2,15 +2,15 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "d0911840f629ef359aaf8ac8635dc571", + "identityHash": "77eff76407f59b690b8877cc22fac42f", "entities": [ { "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", "fields": [ { "fieldPath": "id", - "columnName": "id", + "columnName": "cookie_id", "affinity": "INTEGER", "notNull": true }, @@ -29,64 +29,20 @@ ], "primaryKey": { "columnNames": [ - "id" + "cookie_id" ], "autoGenerate": false }, "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 )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", - "columnName": "id", + "columnName": "notif_id", "affinity": "INTEGER", "notNull": true }, @@ -125,15 +81,30 @@ "columnName": "profileUrl", "affinity": "TEXT", "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true } ], "primaryKey": { "columnNames": [ - "id" + "notif_id", + "userId" ], "autoGenerate": false }, "indices": [ + { + "name": "index_notifications_notif_id", + "unique": false, + "columnNames": [ + "notif_id" + ], + "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" + }, { "name": "index_notifications_userId", "unique": false, @@ -145,14 +116,14 @@ ], "foreignKeys": [ { - "table": "notification_info", + "table": "cookies", "onDelete": "CASCADE", "onUpdate": "NO ACTION", "columns": [ "userId" ], "referencedColumns": [ - "id" + "cookie_id" ] } ] @@ -161,7 +132,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, \"d0911840f629ef359aaf8ac8635dc571\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"77eff76407f59b690b8877cc22fac42f\")" ] } } \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json index 687d0bc1..fe2aa83e 100644 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -31,6 +31,7 @@ "foreignKeys": [] } ], + "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, \"fde868470836ff9230f1d406922d7563\")" -- cgit v1.2.3 From f1878133d8af686ce8c27acffe28f26e9dda5165 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 04:00:20 -0500 Subject: Add koin test --- app/build.gradle | 1 + .../com/pitchedapps/frost/db/DatabaseTest.kt | 38 ++++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 3 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 2 ++ .../kotlin/com/pitchedapps/frost/db/FbTabsDb.kt | 5 --- .../1.json | 36 ++++++++++++++++++-- gradle.properties | 2 +- 7 files changed, 78 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/build.gradle b/app/build.gradle index d5dcf87e..74e8015c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -178,6 +178,7 @@ dependencies { androidTestImplementation kauDependency.espresso androidTestImplementation kauDependency.testRules androidTestImplementation kauDependency.testRunner + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}" testImplementation kauDependency.kotlinTest testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}" diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt new file mode 100644 index 00000000..1f1a201b --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/DatabaseTest.kt @@ -0,0 +1,38 @@ +package com.pitchedapps.frost.db + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.runner.RunWith +import org.koin.error.NoBeanDefFoundException +import org.koin.standalone.get +import org.koin.test.KoinTest +import kotlin.reflect.KClass +import kotlin.reflect.full.functions +import kotlin.test.Test +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class DatabaseTest : KoinTest { + + inline fun hasKoin() = hasKoin(T::class) + + fun hasKoin(klazz: KClass): Boolean = + try { + get(clazz = klazz) + true + } catch (e: NoBeanDefFoundException) { + false + } + + /** + * Database and all daos should be loaded as components + */ + @Test + fun testKoins() { + hasKoin() + val members = FrostDatabase::class.java.kotlin.functions.filter { it.name.endsWith("Dao") } + .mapNotNull { it.returnType.classifier as? KClass<*> } + assertTrue(members.isNotEmpty(), "Failed to find dao interfaces") + val missingKoins = (members + FrostDatabase::class).filter { !hasKoin(it) } + assertTrue(missingKoins.isEmpty(), "Missing koins: $missingKoins") + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index bd6bff4b..4d6bc938 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -53,4 +53,5 @@ interface CacheDao { suspend fun deleteById(id: Long) } -suspend fun CacheDao.save(id: String, contents: String) = insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file +suspend fun CacheDao.save(id: String, contents: String) = + insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file 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 b83fce52..29296494 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -71,6 +71,8 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub single { create(context) } single { get().cookieDao() } single { get().tabDao() } + single { get().cacheDao() } + single { get().notifDao() } } /** 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 c2bb0837..f4e74509 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/FbTabsDb.kt @@ -24,14 +24,9 @@ import androidx.room.Query import androidx.room.Transaction import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.defaultTabs -import com.pitchedapps.frost.utils.L import com.raizlabs.android.dbflow.annotation.Database import com.raizlabs.android.dbflow.annotation.PrimaryKey import com.raizlabs.android.dbflow.annotation.Table -import com.raizlabs.android.dbflow.kotlinextensions.database -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.Dispatchers import kotlinx.coroutines.withContext 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 c382bce7..72b86db3 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": "77eff76407f59b690b8877cc22fac42f", + "identityHash": "099ffebd0f0fe80199c0f6413a549ebb", "entities": [ { "tableName": "cookies", @@ -127,12 +127,44 @@ ] } ] + }, + { + "tableName": "frost_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contents", + "columnName": "contents", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] } ], "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, \"77eff76407f59b690b8877cc22fac42f\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"099ffebd0f0fe80199c0f6413a549ebb\")" ] } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 98f29dbe..146591bb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,7 @@ KAU=4.0.0-alpha02 KOTLIN=1.3.21 # https://mvnrepository.com/artifact/com.android.tools.build/gradle?repo=google -ANDROID_GRADLE=3.2.1 +ANDROID_GRADLE=3.3.2 # https://github.com/diffplug/spotless/blob/master/plugin-gradle/CHANGES.md SPOTLESS=3.17.0 -- cgit v1.2.3 From 12f491737ec2e2d774a816e84170ff352d1b6cd6 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 14:59:40 -0500 Subject: Add cache test --- .../kotlin/com/pitchedapps/frost/db/CacheDbTest.kt | 32 ++++++++++++++++++ .../kotlin/com/pitchedapps/frost/db/CacheDb.kt | 38 +++++++++++++++++----- .../com/pitchedapps/frost/db/NotificationDb.kt | 2 +- .../1.json | 29 ++++++++++++++--- 4 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt new file mode 100644 index 00000000..780bbd3e --- /dev/null +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt @@ -0,0 +1,32 @@ +package com.pitchedapps.frost.db + +import kotlinx.coroutines.runBlocking +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class CacheDbTest : BaseDbTest() { + + private val dao get() = db.cacheDao() + private val cookieDao get() = db.cookieDao() + + private fun cookie(id: Long) = CookieEntity(id, "name$id", "cookie$id") + + @Test + fun save() { + val cookie = cookie(1L) + val type = "test" + val content = "long test".repeat(10000) + runBlocking { + cookieDao.insertCookie(cookie) + dao.save(cookie.id, type, content) + val cache = dao.select(cookie.id, type) ?: fail("Cache not found") + assertEquals(content, cache.contents, "Content mismatch") + assertTrue( + System.currentTimeMillis() - cache.lastUpdated < 500, + "Cache retrieval took over 500ms (${System.currentTimeMillis() - cache.lastUpdated})" + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt index 4d6bc938..8c3c9c6b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CacheDb.kt @@ -19,9 +19,11 @@ package com.pitchedapps.frost.db import android.os.Parcelable import androidx.room.Dao import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import com.pitchedapps.frost.utils.L import kotlinx.android.parcel.Parcelize /** @@ -31,11 +33,20 @@ import kotlinx.android.parcel.Parcelize /** * Generic cache to store serialized content */ -@Entity(tableName = "frost_cache") +@Entity( + tableName = "frost_cache", + primaryKeys = ["id", "type"], + foreignKeys = [ForeignKey( + entity = CookieEntity::class, + parentColumns = ["cookie_id"], + childColumns = ["id"], + onDelete = ForeignKey.CASCADE + )] +) @Parcelize data class CacheEntity( - @androidx.room.PrimaryKey - val id: String, + val id: Long, + val type: String, val lastUpdated: Long, val contents: String ) : Parcelable @@ -43,15 +54,24 @@ data class CacheEntity( @Dao interface CacheDao { - @Query("SELECT * FROM frost_cache WHERE id = :id") - suspend fun selectById(id: Long): CacheEntity? + @Query("SELECT * FROM frost_cache WHERE id = :id AND type = :type") + suspend fun select(id: Long, type: String): CacheEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertCache(cache: CacheEntity) - @Query("DELETE FROM frost_cache WHERE id = :id") - suspend fun deleteById(id: Long) + @Query("DELETE FROM frost_cache WHERE id = :id AND type = :type") + suspend fun delete(id: Long, type: String) } -suspend fun CacheDao.save(id: String, contents: String) = - insertCache(CacheEntity(id, System.currentTimeMillis(), contents)) \ No newline at end of file +/** + * Returns true if successful, given that there are constraints to the insertion + */ +suspend fun CacheDao.save(id: Long, type: String, contents: String): Boolean = + try { + insertCache(CacheEntity(id, type, System.currentTimeMillis(), contents)) + true + } catch (e: Exception) { + L.e(e) { "Cache save failed for $type" } + false + } \ No newline at end of file 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 7f41fbf8..d2771754 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -146,7 +146,7 @@ suspend fun NotificationDao.saveNotifications(type: String, notifs: List Date: Thu, 7 Mar 2019 17:28:32 -0500 Subject: Rename some methods --- .../kotlin/com/pitchedapps/frost/db/CacheDbTest.kt | 2 +- .../com/pitchedapps/frost/db/CookieDbTest.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDbTest.kt | 14 +- .../kotlin/com/pitchedapps/frost/StartActivity.kt | 6 +- .../pitchedapps/frost/activities/LoginActivity.kt | 2 +- .../kotlin/com/pitchedapps/frost/db/CookiesDb.kt | 4 +- .../kotlin/com/pitchedapps/frost/db/Database.kt | 5 +- .../com/pitchedapps/frost/facebook/FbCookie.kt | 2 +- .../main/kotlin/com/pitchedapps/frost/utils/L.kt | 6 + .../1.json | 189 --------------------- .../1.json | 40 ----- 11 files changed, 32 insertions(+), 250 deletions(-) delete mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json delete mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt index 780bbd3e..1fe7bbc4 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CacheDbTest.kt @@ -19,7 +19,7 @@ class CacheDbTest : BaseDbTest() { val type = "test" val content = "long test".repeat(10000) runBlocking { - cookieDao.insertCookie(cookie) + cookieDao.save(cookie) dao.save(cookie.id, type, content) val cache = dao.select(cookie.id, type) ?: fail("Cache not found") assertEquals(content, cache.contents, "Content mismatch") 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 5ec771f5..122e3205 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/CookieDbTest.kt @@ -13,7 +13,7 @@ class CookieDbTest : BaseDbTest() { fun basicCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) val cookies = dao.selectAll() assertEquals(listOf(cookie), cookies, "Cookie mismatch") } @@ -24,7 +24,7 @@ class CookieDbTest : BaseDbTest() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) dao.deleteById(cookie.id + 1) assertEquals( listOf(cookie), @@ -40,15 +40,15 @@ class CookieDbTest : BaseDbTest() { fun insertReplaceCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) assertEquals(listOf(cookie), dao.selectAll(), "Cookie insertion failed") - dao.insertCookie(cookie.copy(name = "testName2")) + dao.save(cookie.copy(name = "testName2")) assertEquals( listOf(cookie.copy(name = "testName2")), dao.selectAll(), "Cookie replacement failed" ) - dao.insertCookie(cookie.copy(id = 123L)) + dao.save(cookie.copy(id = 123L)) assertEquals( setOf(cookie.copy(id = 123L), cookie.copy(name = "testName2")), dao.selectAll().toSet(), @@ -61,7 +61,7 @@ class CookieDbTest : BaseDbTest() { fun selectCookie() { val cookie = CookieEntity(id = 1234L, name = "testName", cookie = "testCookie") runBlocking { - dao.insertCookie(cookie) + dao.save(cookie) assertEquals(cookie, dao.selectById(cookie.id), "Cookie selection failed") assertNull(dao.selectById(cookie.id + 1), "Inexistent cookie selection failed") } diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt index 176d0d3a..25c29db4 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -31,7 +31,7 @@ class NotificationDbTest : BaseDbTest() { // Unique unsorted ids val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) assertEquals(notifs.sortedByDescending { it.timestamp }, dbNotifs, "Incorrect notification list received") @@ -45,8 +45,8 @@ class NotificationDbTest : BaseDbTest() { val cookie2 = cookie(12L) val notifs1 = (0L..2L).map { notifContent(it, cookie1) } val notifs2 = (5L..10L).map { notifContent(it, cookie2) } - db.cookieDao().insertCookie(cookie1) - db.cookieDao().insertCookie(cookie2) + db.cookieDao().save(cookie1) + db.cookieDao().save(cookie2) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2) assertEquals( @@ -82,8 +82,8 @@ class NotificationDbTest : BaseDbTest() { val cookie2 = cookie(12L) val notifs1 = (0L..2L).map { notifContent(it, cookie1) } val notifs2 = notifs1.map { it.copy(data = cookie2) } - db.cookieDao().insertCookie(cookie1) - db.cookieDao().insertCookie(cookie2) + db.cookieDao().save(cookie1) + db.cookieDao().save(cookie2) assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed") assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed") } @@ -95,7 +95,7 @@ class NotificationDbTest : BaseDbTest() { // Unique unsorted ids val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) db.cookieDao().deleteById(cookie.id) val dbNotifs = dao.selectNotifications(cookie.id, NOTIF_CHANNEL_GENERAL) @@ -110,7 +110,7 @@ class NotificationDbTest : BaseDbTest() { val notifs = listOf(0L, 4L, 2L, 6L, 99L, 3L).map { notifContent(it, cookie) } runBlocking { assertEquals(-1L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Default epoch failed") - db.cookieDao().insertCookie(cookie) + db.cookieDao().save(cookie) dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs) assertEquals(99L, dao.latestEpoch(cookie.id, NOTIF_CHANNEL_GENERAL), "Latest epoch failed") } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt index 3b7418e1..24e9c548 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/StartActivity.kt @@ -38,6 +38,7 @@ import com.pitchedapps.frost.db.CookieModel import com.pitchedapps.frost.db.FbTabDao import com.pitchedapps.frost.db.FbTabModel import com.pitchedapps.frost.db.save +import com.pitchedapps.frost.db.selectAll import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.EXTRA_COOKIES import com.pitchedapps.frost.utils.L @@ -94,6 +95,7 @@ class StartActivity : KauBaseActivity() { }) } } catch (e: Exception) { + L._e(e) { "Load start failed" } showInvalidWebView() } } @@ -106,9 +108,11 @@ class StartActivity : KauBaseActivity() { private suspend fun migrate() = withContext(Dispatchers.IO) { if (cookieDao.selectAll().isNotEmpty()) return@withContext val cookies = (select from CookieModel::class).queryList().map { CookieEntity(it.id, it.name, it.cookie) } - cookieDao.insertCookies(cookies) + cookieDao.save(cookies) val tabs = (select from FbTabModel::class).queryList().map(FbTabModel::tab) tabDao.save(tabs) + L._d { "Migrated cookies ${cookieDao.selectAll()}" } + L._d { "Migrated tabs ${tabDao.selectAll()}" } } private fun showInvalidWebView() = 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 27dbc37a..e5a50543 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -184,7 +184,7 @@ class LoginActivity : BaseActivity() { } if (cookie.name?.isNotBlank() == false && result != cookie.name) { - cookieDao.insertCookie(cookie.copy(name = result)) + cookieDao.save(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 c6c983fb..7e929370 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/CookiesDb.kt @@ -59,10 +59,10 @@ interface CookieDao { suspend fun selectById(id: Long): CookieEntity? @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCookie(cookie: CookieEntity) + suspend fun save(cookie: CookieEntity) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertCookies(cookies: List) + suspend fun save(cookies: List) @Query("DELETE FROM cookies WHERE cookie_id = :id") suspend fun deleteById(id: Long) 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 29296494..ae96b696 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/Database.kt @@ -5,6 +5,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters +import org.koin.core.Koin import org.koin.dsl.module.module import org.koin.standalone.StandAloneContext @@ -59,11 +60,11 @@ class FrostDatabase(private val privateDb: FrostPrivateDatabase, private val pub val privateDb = Room.databaseBuilder( context, FrostPrivateDatabase::class.java, FrostPrivateDatabase.DATABASE_NAME - ).build() + ).fallbackToDestructiveMigration().build() val publicDb = Room.databaseBuilder( context, FrostPublicDatabase::class.java, FrostPublicDatabase.DATABASE_NAME - ).build() + ).fallbackToDestructiveMigration().build() return FrostDatabase(privateDb, publicDb) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt index 02718b78..6afbea4b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt @@ -85,7 +85,7 @@ object FbCookie { Prefs.userId = id CookieManager.getInstance().flush() val cookie = CookieEntity(Prefs.userId, null, webCookie) - cookieDao.insertCookie(cookie) + cookieDao.save(cookie) } suspend fun reset() { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt index 8364c34e..7d2e0e08 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/L.kt @@ -20,6 +20,7 @@ import android.util.Log import ca.allanwang.kau.logging.KauLogger import com.bugsnag.android.Bugsnag import com.pitchedapps.frost.BuildConfig +import java.lang.Exception /** * Created by Allan Wang on 2017-05-28. @@ -50,6 +51,11 @@ object L : KauLogger("Frost", { d(message) } + inline fun _e(e: Throwable?, message: () -> Any?) { + if (BuildConfig.DEBUG) + e(e, message) + } + override fun logImpl(priority: Int, message: String?, t: Throwable?) { if (BuildConfig.DEBUG) super.logImpl(priority, message, t) diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json deleted file mode 100644 index 60d5cddd..00000000 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json +++ /dev/null @@ -1,189 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "0a9d994786b7e07fea95c11d9210ce0e", - "entities": [ - { - "tableName": "cookies", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "cookie_id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "cookie", - "columnName": "cookie", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "columnNames": [ - "cookie_id" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "notifications", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "notif_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 - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "notif_id", - "userId" - ], - "autoGenerate": false - }, - "indices": [ - { - "name": "index_notifications_notif_id", - "unique": false, - "columnNames": [ - "notif_id" - ], - "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" - }, - { - "name": "index_notifications_userId", - "unique": false, - "columnNames": [ - "userId" - ], - "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" - } - ], - "foreignKeys": [ - { - "table": "cookies", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "userId" - ], - "referencedColumns": [ - "cookie_id" - ] - } - ] - }, - { - "tableName": "frost_cache", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "type", - "columnName": "type", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "lastUpdated", - "columnName": "lastUpdated", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "contents", - "columnName": "contents", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id", - "type" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [ - { - "table": "cookies", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "id" - ], - "referencedColumns": [ - "cookie_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, \"0a9d994786b7e07fea95c11d9210ce0e\")" - ] - } -} \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json deleted file mode 100644 index fe2aa83e..00000000 --- a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "fde868470836ff9230f1d406922d7563", - "entities": [ - { - "tableName": "tabs", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", - "fields": [ - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tab", - "columnName": "tab", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "position" - ], - "autoGenerate": false - }, - "indices": [], - "foreignKeys": [] - } - ], - "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, \"fde868470836ff9230f1d406922d7563\")" - ] - } -} \ No newline at end of file -- cgit v1.2.3 From adf9f2f6f23de17aae51435f792de16a0680f286 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 18:15:46 -0500 Subject: Rebind main activity logic --- .../frost/activities/BaseMainActivity.kt | 72 ++++++-- .../pitchedapps/frost/activities/MainActivity.kt | 20 +-- .../1.json | 189 +++++++++++++++++++++ .../1.json | 40 +++++ 4 files changed, 291 insertions(+), 30 deletions(-) create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json create mode 100644 app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index 4669418d..ca30401a 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -123,7 +123,10 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, FileChooserContract by FileChooserDelegate(), VideoViewHolder, SearchViewHolder { - protected lateinit var adapter: SectionsPagerAdapter + /** + * Note that tabs themselves are initialized through a coroutine during onCreate + */ + protected val adapter: SectionsPagerAdapter = SectionsPagerAdapter() override val frameWrapper: FrameLayout get() = frame_wrapper val viewPager: FrostViewPager get() = container val cookieDao: CookieDao by inject() @@ -136,6 +139,8 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, val appBar: AppBarLayout by bindView(R.id.appbar) val coordinator: CoordinatorLayout by bindView(R.id.main_content) + protected var lastPosition = -1 + override var videoViewer: FrostVideoViewer? = null private lateinit var drawer: Drawer private lateinit var drawerHeader: AccountHeader @@ -156,14 +161,14 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, background(viewPager) } setSupportActionBar(toolbar) - launch { - adapter = SectionsPagerAdapter(tabDao.selectAll()) - viewPager.adapter = adapter - viewPager.offscreenPageLimit = TAB_COUNT - } + viewPager.adapter = adapter + viewPager.offscreenPageLimit = TAB_COUNT tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onNestedCreate(savedInstanceState) L.i { "Main finished loading UI in ${System.currentTimeMillis() - start} ms" } + launch { + adapter.setPages(tabDao.selectAll()) + } controlWebview = WebView(this) if (BuildConfig.VERSION_CODE > Prefs.versionCode) { Prefs.prevVersionCode = Prefs.versionCode @@ -462,16 +467,12 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(adapter.forcedFallbacks)) + adapter.saveInstanceState(outState) } override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) - adapter.forcedFallbacks.clear() - adapter.forcedFallbacks.addAll( - savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) - ?: emptyList() - ) + adapter.restoreInstanceState(savedInstanceState) } override fun onResume() { @@ -526,9 +527,47 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, runOnUiThread { adapter.reloadFragment(fragment) } } - inner class SectionsPagerAdapter(val pages: List) : FragmentPagerAdapter(supportFragmentManager) { + inner class SectionsPagerAdapter : FragmentPagerAdapter(supportFragmentManager) { + + private val pages: MutableList = mutableListOf() - val forcedFallbacks = mutableSetOf() + private val forcedFallbacks = mutableSetOf() + + /** + * Update page list and prompt reload + */ + fun setPages(pages: List) { + this.pages.clear() + this.pages.addAll(pages) + notifyDataSetChanged() + tabs.removeAllTabs() + this.pages.forEachIndexed { index, fbItem -> + tabs.addTab( + tabs.newTab() + .setCustomView(BadgedIcon(this@BaseMainActivity).apply { iicon = fbItem.icon }.also { + it.setAllAlpha(if (index == 0) SELECTED_TAB_ALPHA else UNSELECTED_TAB_ALPHA) + }) + ) + } + lastPosition = 0 + viewPager.setCurrentItem(0, false) + viewPager.post { + if (!fragmentChannel.isClosedForSend) + fragmentChannel.offer(0) + } //trigger hook so title is set + } + + fun saveInstanceState(outState: Bundle) { + outState.putStringArrayList(STATE_FORCE_FALLBACK, ArrayList(forcedFallbacks)) + } + + fun restoreInstanceState(savedInstanceState: Bundle) { + forcedFallbacks.clear() + forcedFallbacks.addAll( + savedInstanceState.getStringArrayList(STATE_FORCE_FALLBACK) + ?: emptyList() + ) + } fun reloadFragment(fragment: BaseFragment) { if (fragment is WebFragment) return @@ -567,4 +606,9 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract, PointF(0f, toolbar.height.toFloat()) else PointF(0f, 0f) + + companion object { + const val SELECTED_TAB_ALPHA = 255f + const val UNSELECTED_TAB_ALPHA = 128f + } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt index 75c9537b..34674cb0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -35,7 +35,6 @@ class MainActivity : BaseMainActivity() { override val fragmentChannel = BroadcastChannel(10) override val headerBadgeChannel = BroadcastChannel(Channel.CONFLATED) - var lastPosition = -1 override fun onNestedCreate(savedInstanceState: Bundle?) { setupTabs() @@ -54,23 +53,18 @@ class MainActivity : BaseMainActivity() { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { super.onPageScrolled(position, positionOffset, positionOffsetPixels) - val delta = positionOffset * (255 - 128).toFloat() + val delta = positionOffset * (SELECTED_TAB_ALPHA - UNSELECTED_TAB_ALPHA) tabsForEachView { tabPosition, view -> view.setAllAlpha( when (tabPosition) { - position -> 255.0f - delta - position + 1 -> 128.0f + delta - else -> 128f + position -> SELECTED_TAB_ALPHA - delta + position + 1 -> UNSELECTED_TAB_ALPHA + delta + else -> UNSELECTED_TAB_ALPHA } ) } } }) - viewPager.post { - if (!fragmentChannel.isClosedForSend) - fragmentChannel.offer(0) - lastPosition = 0 - } //trigger hook so title is set } private fun setupTabs() { @@ -115,11 +109,5 @@ class MainActivity : BaseMainActivity() { L.e(e) { "Header badge error" } } } - adapter.pages.forEach { - tabs.addTab( - tabs.newTab() - .setCustomView(BadgedIcon(this).apply { iicon = it.icon }) - ) - } } } diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json new file mode 100644 index 00000000..60d5cddd --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase/1.json @@ -0,0 +1,189 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "0a9d994786b7e07fea95c11d9210ce0e", + "entities": [ + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`cookie_id` INTEGER NOT NULL, `name` TEXT, `cookie` TEXT, PRIMARY KEY(`cookie_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "cookie_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cookie", + "columnName": "cookie", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "cookie_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "notif_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 + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "notif_id", + "userId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notifications_notif_id", + "unique": false, + "columnNames": [ + "notif_id" + ], + "createSql": "CREATE INDEX `index_notifications_notif_id` ON `${TABLE_NAME}` (`notif_id`)" + }, + { + "name": "index_notifications_userId", + "unique": false, + "columnNames": [ + "userId" + ], + "createSql": "CREATE INDEX `index_notifications_userId` ON `${TABLE_NAME}` (`userId`)" + } + ], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "userId" + ], + "referencedColumns": [ + "cookie_id" + ] + } + ] + }, + { + "tableName": "frost_cache", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `type` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `contents` TEXT NOT NULL, PRIMARY KEY(`id`, `type`), FOREIGN KEY(`id`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contents", + "columnName": "contents", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "cookies", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "id" + ], + "referencedColumns": [ + "cookie_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, \"0a9d994786b7e07fea95c11d9210ce0e\")" + ] + } +} \ No newline at end of file diff --git a/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json new file mode 100644 index 00000000..fe2aa83e --- /dev/null +++ b/app/src/schemas/com.pitchedapps.frost.db.FrostPublicDatabase/1.json @@ -0,0 +1,40 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "fde868470836ff9230f1d406922d7563", + "entities": [ + { + "tableName": "tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `tab` TEXT NOT NULL, PRIMARY KEY(`position`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tab", + "columnName": "tab", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "position" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "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, \"fde868470836ff9230f1d406922d7563\")" + ] + } +} \ No newline at end of file -- cgit v1.2.3 From 7f371a95320a1d8aae29f8ca15f7fd972367b60e Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 22 Apr 2019 03:21:30 -0400 Subject: Update parsers and add widget items --- app/src/main/AndroidManifest.xml | 14 +++ .../kotlin/com/pitchedapps/frost/db/Database.kt | 12 +- .../com/pitchedapps/frost/db/NotificationDb.kt | 9 +- .../frost/facebook/parsers/MessageParser.kt | 3 +- .../frost/facebook/parsers/NotifParser.kt | 3 +- .../frost/services/FrostNotifications.kt | 6 +- .../frost/widgets/NotificationWidget.kt | 135 ++++++++++++++++++++- .../main/res/layout/widget_notification_item.xml | 53 ++++++++ app/src/main/res/layout/widget_notifications.xml | 22 ++++ app/src/main/res/values/dimens.xml | 2 + app/src/main/res/xml/notification_widget_info.xml | 4 + .../1.json | 12 +- 12 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/layout/widget_notification_item.xml create mode 100644 app/src/main/res/layout/widget_notifications.xml create mode 100644 app/src/main/res/xml/notification_widget_info.xml (limited to 'app/src/schemas/com.pitchedapps.frost.db.FrostPrivateDatabase') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ceb309f3..1ca91d62 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -173,6 +173,20 @@ + + + + + + + + + RoomDatabase.Builder.frostBuild() = if (BuildConfig.DEBUG) { + fallbackToDestructiveMigration().build() + } else { + build() + } + fun create(context: Context): FrostDatabase { val privateDb = Room.databaseBuilder( context, FrostPrivateDatabase::class.java, FrostPrivateDatabase.DATABASE_NAME - ).build() + ).frostBuild() val publicDb = Room.databaseBuilder( context, FrostPublicDatabase::class.java, FrostPublicDatabase.DATABASE_NAME - ).build() + ).frostBuild() return FrostDatabase(privateDb, publicDb) } 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 8936d682..532bb435 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -64,7 +64,8 @@ data class NotificationEntity( val timestamp: Long, val profileUrl: String?, // Type essentially refers to channel - val type: String + val type: String, + val unread: Boolean ) { constructor( type: String, @@ -77,7 +78,8 @@ data class NotificationEntity( content.text, content.timestamp, content.profileUrl, - type + type, + content.unread ) } @@ -94,7 +96,8 @@ data class NotificationContentEntity( title = notif.title, text = notif.text, timestamp = notif.timestamp, - profileUrl = notif.profileUrl + profileUrl = notif.profileUrl, + unread = notif.unread ) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt index 80ed8ee8..9979cd2b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/MessageParser.kt @@ -64,7 +64,8 @@ data class FrostMessages( title = title, text = content ?: "", timestamp = time, - profileUrl = img + profileUrl = img, + unread = unread ) } }.toList() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt index 199fc685..c22524ad 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/parsers/NotifParser.kt @@ -53,7 +53,8 @@ data class FrostNotifs( title = null, text = content, timestamp = time, - profileUrl = img + profileUrl = img, + unread = unread ) } }.toList() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 6f039784..68ed859c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -181,7 +181,8 @@ enum class NotificationType( "Debug Notif", "Test 123", System.currentTimeMillis() / 1000, - "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png" + "https://www.iconexperience.com/_img/v_collection_png/256x256/shadow/dog.png", + false ) createNotification(context, content).notify(context) } @@ -266,7 +267,8 @@ data class NotificationContent( val title: String? = null, // defaults to frost title val text: String, val timestamp: Long, - val profileUrl: String? + val profileUrl: String?, + val unread: Boolean ) { val notifId = Math.abs(id.toInt()) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt index 308772d0..e7a17c8e 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/widgets/NotificationWidget.kt @@ -1,7 +1,140 @@ +/* + * Copyright 2019 Allan Wang + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package com.pitchedapps.frost.widgets +import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import androidx.annotation.ColorRes +import ca.allanwang.kau.utils.ContextHelper +import ca.allanwang.kau.utils.withAlpha +import com.bumptech.glide.request.target.AppWidgetTarget +import com.pitchedapps.frost.R +import com.pitchedapps.frost.db.NotificationDao +import com.pitchedapps.frost.db.selectNotifications +import com.pitchedapps.frost.glide.FrostGlide +import com.pitchedapps.frost.glide.GlideApp +import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NotificationContent +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_IDS +import com.pitchedapps.frost.widgets.NotificationWidget.Companion.NOTIF_WIDGET_TYPE +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.runBlocking +import org.koin.standalone.KoinComponent +import org.koin.standalone.inject +import kotlin.coroutines.CoroutineContext class NotificationWidget : AppWidgetProvider() { + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + val views = RemoteViews(context.packageName, com.pitchedapps.frost.R.layout.widget_notifications) + val intent = NotificationWidgetService.createIntent(context, NOTIF_CHANNEL_GENERAL, appWidgetIds) + for (id in appWidgetIds) { + views.setRemoteAdapter(R.id.widget_notification_list, intent) + appWidgetManager.updateAppWidget(id, views) + } + } -} \ No newline at end of file + companion object { + const val NOTIF_WIDGET_TYPE = "notif_widget_type" + const val NOTIF_WIDGET_IDS = "notif_widget_ids" + } +} + +class NotificationWidgetService : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = NotificationWidgetDataProvider(this, intent) + + companion object { + fun createIntent(context: Context, type: String, appWidgetIds: IntArray): Intent = + Intent(context, NotificationWidgetService::class.java) + .putExtra(NOTIF_WIDGET_TYPE, type) + .putExtra(NOTIF_WIDGET_IDS, appWidgetIds) + } +} + +class NotificationWidgetDataProvider(val context: Context, val intent: Intent) : RemoteViewsService.RemoteViewsFactory, + CoroutineScope, KoinComponent { + + private val notifDao: NotificationDao by inject() + @Volatile + private var content: List = emptyList() + + private val type = intent.getStringExtra(NOTIF_WIDGET_TYPE) + + private val widgetIds = intent.getIntArrayExtra(NOTIF_WIDGET_IDS) + + private lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = ContextHelper.dispatcher + job + + private suspend fun loadNotifications() { + content = notifDao.selectNotifications(Prefs.userId, type) + } + + override fun onCreate() { + job = SupervisorJob() + runBlocking { + loadNotifications() + } + } + + override fun onDataSetChanged() { + runBlocking { + loadNotifications() + } + } + + override fun getLoadingView(): RemoteViews? = null + + override fun getItemId(position: Int): Long = content[position].id + + override fun hasStableIds(): Boolean = true + + override fun getViewAt(position: Int): RemoteViews { + val views = RemoteViews(context.packageName, R.layout.widget_notification_item) + val glide = GlideApp.with(context).asBitmap() + val notif = content[position] + views.setBackgroundColor(R.id.item_frame, Prefs.nativeBgColor(notif.unread)) + views.setTextColor(R.id.item_content, Prefs.textColor) + views.setTextViewText(R.id.item_content, notif.text) + views.setTextColor(R.id.item_date, Prefs.textColor.withAlpha(150)) + views.setTextViewText(R.id.item_date, notif.timestamp.toString()) // TODO + glide.load(notif.profileUrl).transform(FrostGlide.circleCrop) + .into(AppWidgetTarget(context, R.id.item_avatar, views)) + return views + } + + private fun RemoteViews.setBackgroundColor(viewId: Int, @ColorRes color: Int) { + setInt(viewId, "setBackgroundColor", color) + } + + override fun getCount(): Int = content.size + + override fun getViewTypeCount(): Int { + TODO("not implemented") + } + + override fun onDestroy() { + job.cancel() + } +} diff --git a/app/src/main/res/layout/widget_notification_item.xml b/app/src/main/res/layout/widget_notification_item.xml new file mode 100644 index 00000000..d02e2611 --- /dev/null +++ b/app/src/main/res/layout/widget_notification_item.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_notifications.xml b/app/src/main/res/layout/widget_notifications.xml new file mode 100644 index 00000000..fd68e20a --- /dev/null +++ b/app/src/main/res/layout/widget_notifications.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 713bd1b4..53dad5ef 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -9,4 +9,6 @@ 50dp 64dp 20dp + + 18dp diff --git a/app/src/main/res/xml/notification_widget_info.xml b/app/src/main/res/xml/notification_widget_info.xml new file mode 100644 index 00000000..3cb97ed7 --- /dev/null +++ b/app/src/main/res/xml/notification_widget_info.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file 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 60d5cddd..a0cc2c2a 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": "0a9d994786b7e07fea95c11d9210ce0e", + "identityHash": "fe8f5b6c27f48d7e0733ee6819f06f40", "entities": [ { "tableName": "cookies", @@ -38,7 +38,7 @@ }, { "tableName": "notifications", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`notif_id` INTEGER NOT NULL, `userId` INTEGER NOT NULL, `href` TEXT NOT NULL, `title` TEXT, `text` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `profileUrl` TEXT, `type` TEXT NOT NULL, `unread` INTEGER NOT NULL, PRIMARY KEY(`notif_id`, `userId`), FOREIGN KEY(`userId`) REFERENCES `cookies`(`cookie_id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -87,6 +87,12 @@ "columnName": "type", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -183,7 +189,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, \"0a9d994786b7e07fea95c11d9210ce0e\")" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"fe8f5b6c27f48d7e0733ee6819f06f40\")" ] } } \ No newline at end of file -- cgit v1.2.3