From 7f1f2247de1d61354adfd2cec011cc475a20f683 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 7 Mar 2019 00:12:24 -0500 Subject: [PATCH] Migrate to dao and add filter to title --- .../frost/db/NotificationDbTest.kt | 51 +++++++++++++++--- .../pitchedapps/frost/db/NotificationDb.kt | 26 +++++----- .../frost/services/FrostNotifications.kt | 52 +++++++++++-------- .../frost/services/NotificationService.kt | 4 +- 4 files changed, 91 insertions(+), 42 deletions(-) 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 2e9f18751..176d0d3a1 100644 --- a/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt +++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/db/NotificationDbTest.kt @@ -1,12 +1,12 @@ package com.pitchedapps.frost.db -import android.database.sqlite.SQLiteConstraintException import com.pitchedapps.frost.services.NOTIF_CHANNEL_GENERAL +import com.pitchedapps.frost.services.NOTIF_CHANNEL_MESSAGES 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.assertFalse import kotlin.test.assertTrue class NotificationDbTest : BaseDbTest() { @@ -38,6 +38,40 @@ class NotificationDbTest : BaseDbTest() { } } + @Test + fun selectConditions() { + runBlocking { + val cookie1 = cookie(12345L) + 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) + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1) + dao.saveNotifications(NOTIF_CHANNEL_MESSAGES, notifs2) + assertEquals( + emptyList(), + dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_MESSAGES), + "Filtering by type did not work for cookie1" + ) + assertEquals( + notifs1.sortedByDescending { it.timestamp }, + dao.selectNotifications(cookie1.id, NOTIF_CHANNEL_GENERAL), + "Selection for cookie1 failed" + ) + assertEquals( + emptyList(), + dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_GENERAL), + "Filtering by type did not work for cookie2" + ) + assertEquals( + notifs2.sortedByDescending { it.timestamp }, + dao.selectNotifications(cookie2.id, NOTIF_CHANNEL_MESSAGES), + "Selection for cookie2 failed" + ) + } + } + /** * Primary key is both id and userId, in the event that the same notification to multiple users has the same id */ @@ -50,8 +84,8 @@ class NotificationDbTest : BaseDbTest() { 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) + assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs1), "Notif1 save failed") + assertTrue(dao.saveNotifications(NOTIF_CHANNEL_GENERAL, notifs2), "Notif2 save failed") } } @@ -84,10 +118,11 @@ class NotificationDbTest : BaseDbTest() { @Test fun insertionWithInvalidCookies() { - assertFailsWith(SQLiteConstraintException::class) { - runBlocking { - dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))) - } + runBlocking { + assertFalse( + dao.saveNotifications(NOTIF_CHANNEL_GENERAL, listOf(notifContent(1L, cookie(2L)))), + "Notif save should not have passed without relevant cookie entries" + ) } } } \ 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 9622ec472..60ae2ae79 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/db/NotificationDb.kt @@ -136,9 +136,19 @@ suspend fun NotificationDao.selectNotifications(userId: Long, type: String): Lis _selectNotifications(userId, type).map { it.toNotifContent() } } -suspend fun NotificationDao.saveNotifications(type: String, notifs: List) { - withContext(Dispatchers.IO) { - _saveNotifications(type, notifs) +/** + * Returns true if successful, given that there are constraints to the insertion + */ +suspend fun NotificationDao.saveNotifications(type: String, notifs: List): Boolean { + if (notifs.isEmpty()) return true + return withContext(Dispatchers.IO) { + try { + _saveNotifications(type, notifs) + true + } catch (e: Exception) { + L.e(e) { "Notif save failed" } + false + } } } @@ -175,12 +185,4 @@ data class NotificationModel( fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() - ?: NotificationModel(id = id) - -fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) { - notificationModel.async save { - L.d { "Fb notification model saved" } - L._d { notificationModel } - callback?.invoke() - } -} + ?: NotificationModel(id = id) \ No newline at end of file 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 7da3c128e..eb81ff046 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -32,9 +32,11 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.FrostWebActivity import com.pitchedapps.frost.db.CookieEntity -import com.pitchedapps.frost.db.CookieModel +import com.pitchedapps.frost.db.FrostDatabase import com.pitchedapps.frost.db.NotificationModel import com.pitchedapps.frost.db.lastNotificationTime +import com.pitchedapps.frost.db.latestEpoch +import com.pitchedapps.frost.db.saveNotifications import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.parsers.FrostParser @@ -65,8 +67,8 @@ enum class NotificationType( private val overlayContext: OverlayContext, private val fbItem: FbItem, private val parser: FrostParser, + // Legacy; remove with dbflow private val getTime: (notif: NotificationModel) -> Long, - private val putTime: (notif: NotificationModel, time: Long) -> NotificationModel, private val ringtone: () -> String ) { @@ -76,7 +78,6 @@ enum class NotificationType( FbItem.NOTIFICATIONS, NotifParser, NotificationModel::epoch, - { notif, time -> notif.copy(epoch = time) }, Prefs::notificationRingtone ) { @@ -90,7 +91,6 @@ enum class NotificationType( FbItem.MESSAGES, MessageParser, NotificationModel::epochIm, - { notif, time -> notif.copy(epochIm = time) }, Prefs::messageRingtone ); @@ -117,7 +117,8 @@ enum class NotificationType( * Returns the number of notifications generated, * or -1 if an error occurred */ - fun fetch(context: Context, data: CookieEntity): Int { + suspend fun fetch(context: Context, data: CookieEntity): Int { + val notifDao = FrostDatabase.get().notifDao() val response = try { parser.parse(data.cookie) } catch (ignored: Exception) { @@ -128,35 +129,44 @@ enum class NotificationType( return -1 } val notifContents = response.data.getUnreadNotifications(data).filter { notif -> - val text = notif.text - Prefs.notificationKeywords.none { text.contains(it, true) } + val inText = notif.text.let { text -> + Prefs.notificationKeywords.none { text.contains(it, true) } + } + val inTitle = notif.title?.let { title -> + Prefs.notificationKeywords.none { title.contains(it, true) } + } ?: false + inText || inTitle } if (notifContents.isEmpty()) return 0 val userId = data.id - val prevNotifTime = lastNotificationTime(userId) - val prevLatestEpoch = getTime(prevNotifTime) + // Legacy, remove with dbflow + val prevLatestEpoch = + notifDao.latestEpoch(userId, channelId).takeIf { it != -1L } ?: getTime(lastNotificationTime(userId)) L.v { "Notif $name prev epoch $prevLatestEpoch" } - var newLatestEpoch = prevLatestEpoch - val notifs = mutableListOf() - notifContents.forEach { notif -> - L.v { "Notif timestamp ${notif.timestamp}" } - if (notif.timestamp <= prevLatestEpoch) return@forEach - notifs.add(createNotification(context, notif)) - if (notif.timestamp > newLatestEpoch) - newLatestEpoch = notif.timestamp - } - if (newLatestEpoch > prevLatestEpoch) - putTime(prevNotifTime, newLatestEpoch).save() - L.d { "Notif $name new epoch ${getTime(lastNotificationTime(userId))}" } if (prevLatestEpoch == -1L && !BuildConfig.DEBUG) { L.d { "Skipping first notification fetch" } return 0 // do not notify the first time } + + val newNotifContents = notifContents.filter { it.timestamp > prevLatestEpoch } + + if (newNotifContents.isEmpty()) { + L.d { "No new notifs found for $name" } + return 0 + } + + L.d { "Notif $name new epoch ${newNotifContents.map { it.timestamp }.max()}" } + + val notifs = newNotifContents.map { createNotification(context, it) } + + notifDao.saveNotifications(channelId, newNotifContents) + frostEvent("Notifications", "Type" to name, "Count" to notifs.size) if (notifs.size > 1) summaryNotification(context, userId, notifs.size).notify(context) val ringtone = ringtone() notifs.forEachIndexed { i, notif -> + // Ring at most twice notif.withAlert(i < 2, ringtone).notify(context) } return notifs.size diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 088d8e0aa..e1db5fa69 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -23,6 +23,7 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.db.CookieDao import com.pitchedapps.frost.db.CookieEntity +import com.pitchedapps.frost.db.NotificationDao import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.frostEvent @@ -44,6 +45,7 @@ import org.koin.android.ext.android.inject class NotificationService : BaseJobService() { val cookieDao: CookieDao by inject() + val notifDao: NotificationDao by inject() override fun onStopJob(params: JobParameters?): Boolean { super.onStopJob(params) @@ -110,7 +112,7 @@ class NotificationService : BaseJobService() { * Implemented fetch to also notify when an error occurs * Also normalized the output to return the number of notifications received */ - private fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int { + private suspend fun fetch(jobId: Int, type: NotificationType, cookie: CookieEntity): Int { val count = type.fetch(this, cookie) if (count < 0) { if (jobId == NOTIFICATION_JOB_NOW)