Add synchronization notifications content analyzer

This commit is contained in:
Shinokuni 2024-07-02 17:58:45 +02:00
parent 3880fb1fc5
commit 8aac6e4bf4
4 changed files with 440 additions and 0 deletions

View File

@ -0,0 +1,271 @@
package com.readrops.app.compose
import android.content.Context
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.readrops.api.services.SyncResult
import com.readrops.app.compose.sync.SyncAnalyzer
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import kotlinx.coroutines.test.runTest
import org.joda.time.LocalDateTime
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class SyncAnalyzerTest {
private lateinit var database: Database
private lateinit var syncAnalyzer: SyncAnalyzer
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val account1 = Account(
accountName = "test account 1",
accountType = AccountType.FRESHRSS,
isNotificationsEnabled = true
)
private val account2 = Account(
accountName = "test account 2",
accountType = AccountType.NEXTCLOUD_NEWS,
isNotificationsEnabled = false
)
private val account3 = Account(
accountName = "test account 3",
accountType = AccountType.LOCAL,
isNotificationsEnabled = true
)
@Before
fun setupDb() = runTest {
database = Room.inMemoryDatabaseBuilder(context, Database::class.java)
.build()
syncAnalyzer = SyncAnalyzer(context, database)
account1.id = database.newAccountDao().insert(account1).toInt()
account2.id = database.newAccountDao().insert(account2).toInt()
account3.id = database.newAccountDao().insert(account3).toInt()
val accountIds = listOf(account1.id, account2.id, account3.id)
for (i in 0..2) {
val feed = Feed().apply {
name = "feed ${i + 1}"
iconUrl =
"https://i0.wp.com/mrmondialisation.org/wp-content/uploads/2017/05/ico_final.gif"
this.accountId = accountIds.find { it == (i + 1) }!!
isNotificationEnabled = i % 2 == 0
}
database.feedDao().insert(feed).subscribe()
}
}
@After
fun closeDb() {
database.close()
}
@Test
fun testOneElementEveryWhere() = runTest {
val item = Item(
title = "caseOneElementEveryWhere",
feedId = 1,
remoteId = "item 1",
pubDate = LocalDateTime.now()
)
database.newItemDao().insert(item)
val syncResult = SyncResult(items = listOf(item))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))
assertEquals("caseOneElementEveryWhere", notificationContent.content)
assertEquals("feed 1", notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.accountId!! > 0)
database.newItemDao().delete(item)
}
@Test
fun testTwoItemsOneFeed() = runTest {
val item = Item(title = "caseTwoItemsOneFeed", feedId = 1)
val syncResult = SyncResult(items = listOf(item, item, item))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))
assertEquals(context.getString(R.string.new_items, 3), notificationContent.content)
assertEquals("feed 1", notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.accountId > 0)
}
@Test
fun testMultipleFeeds() = runTest {
val item = Item(feedId = 1)
val item2 = Item(feedId = 3)
val syncResult = SyncResult(items = listOf(item, item2))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))
assertEquals(context.getString(R.string.new_items, 2), notificationContent.content)
assertEquals(account1.accountName, notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.accountId > 0)
}
@Test
fun testMultipleAccounts() = runTest {
val item = Item(feedId = 1)
val item2 = Item(feedId = 3)
val syncResult = SyncResult(items = listOf(item, item2))
val syncResult2 = SyncResult(items = listOf(item, item2))
val syncResults = mapOf(account1 to syncResult, account3 to syncResult2)
val notificationContent = syncAnalyzer.getNotificationContent(syncResults)
assertEquals(context.getString(R.string.new_items, 4), notificationContent.title)
}
@Test
fun testAccountNotificationsDisabled() = runTest {
val item1 = Item(title = "testAccountNotificationsDisabled", feedId = 1)
val item2 = Item(title = "testAccountNotificationsDisabled2", feedId = 1)
val syncResult = SyncResult(items = listOf(item1, item2))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account2 to syncResult))
assert(notificationContent.title == null)
assert(notificationContent.content == null)
assert(notificationContent.largeIcon == null)
}
@Test
fun testFeedNotificationsDisabled() = runTest {
val item1 = Item(title = "testAccountNotificationsDisabled", feedId = 2)
val item2 = Item(title = "testAccountNotificationsDisabled2", feedId = 2)
val syncResult = SyncResult(items = listOf(item1, item2))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))
assert(notificationContent.title == null)
assert(notificationContent.content == null)
assert(notificationContent.largeIcon == null)
}
@Test
fun testTwoAccountsWithOneAccountNotificationsEnabled() = runTest {
val item1 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled",
feedId = 1,
remoteId = "remoteId 1",
pubDate = LocalDateTime.now()
)
val item2 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled2",
feedId = 3
)
val item3 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled3",
feedId = 3
)
database.newItemDao().insert(item1)
val syncResult1 = SyncResult(items = listOf(item1))
val syncResult2 = SyncResult(items = listOf(item2, item3))
val syncResults = mapOf(account1 to syncResult1, account2 to syncResult2)
val notificationContent = syncAnalyzer.getNotificationContent(syncResults)
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notificationContent.content)
assertEquals("feed 1", notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.item != null)
database.newItemDao().delete(item1)
}
@Test
fun testTwoAccountsWithOneFeedNotificationEnabled() = runTest{
val item1 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled",
feedId = 1,
remoteId = "remoteId 1",
pubDate = LocalDateTime.now()
)
val item2 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled2",
feedId = 2
)
val item3 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled3",
feedId = 2
)
database.newItemDao().insert(item1)
val syncResult1 = SyncResult(items = listOf(item1))
val syncResult2 = SyncResult(items = listOf(item2, item3))
val syncResults = mapOf(account1 to syncResult1, account2 to syncResult2)
val notificationContent = syncAnalyzer.getNotificationContent(syncResults)
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notificationContent.content)
assertEquals("feed 1", notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.item != null)
database.newItemDao().delete(item1)
}
@Test
fun testOneAccountTwoFeedsWithOneFeedNotificationEnabled() = runTest {
val item1 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled",
feedId = 1,
remoteId = "remoteId 1",
pubDate = LocalDateTime.now()
)
val item2 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled2",
feedId = 2
)
val item3 = Item(
title = "testTwoAccountsWithOneAccountNotificationsEnabled3",
feedId = 2
)
database.newItemDao().insert(item1)
val syncResult = SyncResult(items = listOf(item1, item2, item3))
val notificationContent = syncAnalyzer.getNotificationContent(mapOf(account1 to syncResult))
assertEquals("testTwoAccountsWithOneAccountNotificationsEnabled", notificationContent.content)
assertEquals("feed 1", notificationContent.title)
assertTrue(notificationContent.largeIcon != null)
assertTrue(notificationContent.item != null)
assertTrue(notificationContent.accountId > 0)
database.newItemDao().delete(item1)
}
}

View File

@ -0,0 +1,163 @@
package com.readrops.app.compose.sync
import android.content.Context
import android.graphics.Bitmap
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.imageLoader
import coil.request.ImageRequest
import com.readrops.api.services.SyncResult
import com.readrops.app.compose.R
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import org.koin.core.component.KoinComponent
data class NotificationContent(
val title: String? = null,
val content: String? = null,
val largeIcon: Bitmap? = null,
val item: Item? = null,
val accountId: Int = 0
)
class SyncAnalyzer(
val context: Context,
val database: Database
) : KoinComponent {
suspend fun getNotificationContent(syncResults: Map<Account, SyncResult>): NotificationContent {
return if (newItemsInMultipleAccounts(syncResults)) { // new items from several accounts
val feeds = database.newFeedDao().selectFromIds(getFeedsIdsForNewItems(syncResults))
var itemCount = 0
syncResults.values.forEach { syncResult ->
itemCount += syncResult.items.filter {
isFeedNotificationEnabledForItem(feeds, it)
}.size
}
NotificationContent(title = context.getString(R.string.new_items, itemCount.toString()))
} else { // new items from only one account
getContentFromOneAccount(syncResults)
}
}
private suspend fun getContentFromOneAccount(syncResults: Map<Account, SyncResult>): NotificationContent {
val syncResultMap = syncResults.filterValues { it.items.isNotEmpty() }
if (syncResultMap.values.isNotEmpty()) {
val account = syncResultMap.keys.first()
val syncResult = syncResultMap.values.first()
val feedsIdsForNewItems = getFeedsIdsForNewItems(syncResult)
if (account.isNotificationsEnabled) {
val feeds = database.newFeedDao().selectFromIds(feedsIdsForNewItems)
val items =
syncResult.items.filter { isFeedNotificationEnabledForItem(feeds, it) }
val itemCount = items.size
// new items from several feeds from one account
return when {
feedsIdsForNewItems.size > 1 && itemCount > 1 -> {
NotificationContent(
title = account.accountName!!,
content = context.getString(R.string.new_items, itemCount.toString()),
largeIcon = ContextCompat.getDrawable(
context,
account.accountType!!.iconRes
)!!.toBitmap(),
accountId = account.id
)
}
// new items from only one feed from one account
feedsIdsForNewItems.size == 1 ->
oneFeedCase(feedsIdsForNewItems.first(), syncResult.items, account)
itemCount == 1 -> oneFeedCase(items.first().feedId, items, account)
else -> NotificationContent()
}
}
}
return NotificationContent()
}
private suspend fun oneFeedCase(
feedId: Int,
items: List<Item>,
account: Account
): NotificationContent {
val feed = database.newFeedDao().selectFeed(feedId)
if (feed.isNotificationEnabled) {
val icon = feed.iconUrl?.let {
val target = context.imageLoader
.execute(
ImageRequest.Builder(context)
.data(it)
.build()
)
target.drawable!!.toBitmap()
}
val (item, content) = if (items.size == 1) {
val item = database.newItemDao().selectByRemoteId(
items.first().remoteId!!,
items.first().feedId
)
item to item.title
} else {
null to context.getString(R.string.new_items, items.size.toString())
}
return NotificationContent(
title = feed.name,
largeIcon = icon,
content = content,
item = item,
accountId = account.id
)
}
return NotificationContent()
}
private fun newItemsInMultipleAccounts(syncResults: Map<Account, SyncResult>): Boolean {
val itemsNotEmptyByAccount = mutableListOf<Boolean>()
for ((account, syncResult) in syncResults) {
if (account.isNotificationsEnabled) {
itemsNotEmptyByAccount += syncResult.items.isNotEmpty()
}
}
// return true it there is at least two true in the list
return (itemsNotEmptyByAccount.groupingBy { it }.eachCount()[true] ?: 0) > 1
}
private fun getFeedsIdsForNewItems(syncResult: SyncResult): List<Int> {
val feedsIds = mutableListOf<Int>()
syncResult.items.forEach {
if (it.feedId !in feedsIds)
feedsIds += it.feedId
}
return feedsIds
}
private fun getFeedsIdsForNewItems(syncResults: Map<Account, SyncResult>): List<Int> {
val feedsIds = mutableListOf<Int>()
syncResults.values.forEach { feedsIds += getFeedsIdsForNewItems(it) }
return feedsIds
}
private fun isFeedNotificationEnabledForItem(feeds: List<Feed>, item: Item): Boolean =
feeds.find { it.id == item.feedId }?.isNotificationEnabled!!
}

View File

@ -67,6 +67,9 @@ abstract class NewFeedDao : NewBaseDao<Feed> {
@Query("Update Feed set notification_enabled = :enabled Where account_id = :accountId") @Query("Update Feed set notification_enabled = :enabled Where account_id = :accountId")
abstract suspend fun updateAllFeedsNotificationState(accountId: Int, enabled: Boolean) abstract suspend fun updateAllFeedsNotificationState(accountId: Int, enabled: Boolean)
@Query("Select * From Feed Where id in (:ids)")
abstract suspend fun selectFromIds(ids: List<Int>): List<Feed>
/** /**
* Insert, update and delete feeds by account * Insert, update and delete feeds by account
* *

View File

@ -22,6 +22,9 @@ abstract class NewItemDao : NewBaseDao<Item> {
@RawQuery(observedEntities = [Item::class, ItemState::class]) @RawQuery(observedEntities = [Item::class, ItemState::class])
abstract fun selectItemById(query: SupportSQLiteQuery): Flow<ItemWithFeed> abstract fun selectItemById(query: SupportSQLiteQuery): Flow<ItemWithFeed>
@Query("Select * From Item Where remoteId = :remoteId And feed_id = :feedId")
abstract suspend fun selectByRemoteId(remoteId: String, feedId: Int): Item
@Query("Update Item Set read = :read Where id = :itemId") @Query("Update Item Set read = :read Where id = :itemId")
abstract suspend fun updateReadState(itemId: Int, read: Boolean) abstract suspend fun updateReadState(itemId: Int, read: Boolean)