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")
|
@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
|
||||||
*
|
*
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue