mirror of https://github.com/readrops/Readrops.git
Add synchronization notifications content analyzer
This commit is contained in:
parent
3880fb1fc5
commit
8aac6e4bf4
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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!!
|
||||
}
|
|
@ -67,6 +67,9 @@ abstract class NewFeedDao : NewBaseDao<Feed> {
|
|||
@Query("Update Feed set notification_enabled = :enabled Where account_id = :accountId")
|
||||
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
|
||||
*
|
||||
|
|
|
@ -22,6 +22,9 @@ abstract class NewItemDao : NewBaseDao<Item> {
|
|||
@RawQuery(observedEntities = [Item::class, ItemState::class])
|
||||
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")
|
||||
abstract suspend fun updateReadState(itemId: Int, read: Boolean)
|
||||
|
||||
|
|
Loading…
Reference in New Issue