Compare commits

...

4 Commits

Author SHA1 Message Date
Shinokuni 36cdf84b34 Fetch FreshRSS folders and feeds 2024-05-04 21:44:05 +02:00
Shinokuni 0d69cfd66d Fix CI build 2024-05-04 19:41:42 +02:00
Shinokuni 8071f0b477 Add new FeedDao upsert method 2024-05-04 19:29:20 +02:00
Shinokuni 052c83cb35 Add new FolderDao upsert method 2024-05-04 18:13:19 +02:00
9 changed files with 363 additions and 49 deletions

View File

@ -4,12 +4,13 @@ import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
class SyncResult(var items: List<Item> = mutableListOf(),
var starredItems: List<Item> = mutableListOf(),
var feeds: List<Feed> = listOf(),
var folders: List<Folder> = listOf(),
var unreadIds: List<String>? = null,
var readIds: List<String>? = null,
var starredIds: List<String>? = null,
var isError: Boolean = false
data class SyncResult(
var items: List<Item> = mutableListOf(),
var starredItems: List<Item> = mutableListOf(),
var feeds: List<Feed> = listOf(),
var folders: List<Folder> = listOf(),
var unreadIds: List<String>? = null,
var readIds: List<String>? = null,
var starredIds: List<String>? = null,
var isError: Boolean = false
)

View File

@ -1,7 +1,11 @@
package com.readrops.api.services.freshrss
import com.readrops.api.services.SyncResult
import com.readrops.api.services.freshrss.adapters.FreshRSSUserInfo
import com.readrops.db.entities.Item
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import okhttp3.MultipartBody
import java.io.StringReader
import java.util.Properties
@ -28,15 +32,19 @@ class NewFreshRSSDataSource(private val service: NewFreshRSSService) {
suspend fun getUserInfo(): FreshRSSUserInfo = service.userInfo()
suspend fun sync() {
suspend fun sync(): SyncResult = with(CoroutineScope(Dispatchers.IO)) {
return SyncResult().apply {
folders = async { getFolders() }.await()
feeds = async { getFeeds() }.await()
//items = async { getItems(listOf(GOOGLE_READ, GOOGLE_STARRED), MAX_ITEMS, null) }.await()
}
}
suspend fun getFolders() = service.getFolders()
suspend fun getFeeds() = service.getFeeds()
suspend fun getItems(excludeTargets: List<String>, max: Int, lastModified: Long): List<Item> {
suspend fun getItems(excludeTargets: List<String>, max: Int, lastModified: Long?): List<Item> {
return service.getItems(excludeTargets, max, lastModified)
}

View File

@ -6,6 +6,8 @@ import com.readrops.api.services.freshrss.NewFreshRSSDataSource
import com.readrops.api.utils.AuthInterceptor
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import org.koin.core.component.KoinComponent
@ -36,7 +38,14 @@ class FreshRSSRepository(
}
override suspend fun synchronize(): SyncResult {
TODO("Not yet implemented")
val syncResult = dataSource.sync().apply {
insertFolders(folders)
insertFeeds(feeds)
//insertItems(items)
}
return syncResult
}
override suspend fun insertNewFeeds(
@ -45,4 +54,18 @@ class FreshRSSRepository(
): ErrorResult {
TODO("Not yet implemented")
}
private suspend fun insertFeeds(feeds: List<Feed>) {
feeds.forEach { it.accountId = account.id }
database.newFeedDao().upsertFeeds(feeds, account)
}
private suspend fun insertFolders(folders: List<Folder>) {
folders.forEach { it.accountId = account.id }
database.newFolderDao().upsertFolders(folders, account)
}
private suspend fun insertItems(items: List<Item>) {
}
}

View File

@ -95,50 +95,70 @@ class TimelineScreenModel(
fun refreshTimeline() {
screenModelScope.launch(dispatcher) {
val selectedFeeds = if (currentAccount!!.isLocal) {
when (filters.value.subFilter) {
SubFilter.FEED -> listOf(
database.newFeedDao().selectFeed(filters.value.filterFeedId)
if (currentAccount!!.isLocal) {
refreshLocalAccount()
} else {
_timelineState.update { it.copy(isRefreshing = true) }
try {
repository?.synchronize()
} catch (e: Exception) {
// handle sync exceptions
}
_timelineState.update {
it.copy(
isRefreshing = false,
endSynchronizing = true
)
SubFilter.FOLDER -> database.newFeedDao()
.selectFeedsByFolder(filters.value.filterFolderId)
else -> listOf()
}
} else listOf()
_timelineState.update {
it.copy(
feedCount = 0,
feedMax = if (selectedFeeds.isNotEmpty())
selectedFeeds.size
else
database.newFeedDao().selectFeedCount(currentAccount!!.id)
)
}
}
}
_timelineState.update { it.copy(isRefreshing = true) }
val results = repository?.synchronize(
selectedFeeds = selectedFeeds,
onUpdate = { feed ->
_timelineState.update {
it.copy(
currentFeed = feed.name!!,
feedCount = it.feedCount + 1
)
}
}
private suspend fun refreshLocalAccount() {
val selectedFeeds = when (filters.value.subFilter) {
SubFilter.FEED -> listOf(
database.newFeedDao().selectFeed(filters.value.filterFeedId)
)
_timelineState.update {
it.copy(
isRefreshing = false,
endSynchronizing = true,
synchronizationErrors = if (results!!.second.isNotEmpty()) results.second else null
)
SubFilter.FOLDER -> database.newFeedDao()
.selectFeedsByFolder(filters.value.filterFolderId)
else -> listOf()
}
_timelineState.update {
it.copy(
feedCount = 0,
feedMax = if (selectedFeeds.isNotEmpty())
selectedFeeds.size
else
database.newFeedDao().selectFeedCount(currentAccount!!.id)
)
}
_timelineState.update { it.copy(isRefreshing = true) }
val results = repository?.synchronize(
selectedFeeds = selectedFeeds,
onUpdate = { feed ->
_timelineState.update {
it.copy(
currentFeed = feed.name!!,
feedCount = it.feedCount + 1
)
}
}
)
_timelineState.update {
it.copy(
isRefreshing = false,
endSynchronizing = true,
synchronizationErrors = if (results!!.second.isNotEmpty()) results.second else null
)
}
}

View File

@ -91,4 +91,5 @@ dependencies {
testImplementation(libs.bundles.kointest)
implementation(libs.bundles.coroutines)
androidTestImplementation(libs.coroutines.test)
}

View File

@ -0,0 +1,107 @@
package com.readrops.db.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class NewFeedDaoTest {
private lateinit var database: Database
private lateinit var account: Account
@Before
fun before() = runTest {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
account = Account(accountType = AccountType.LOCAL).apply {
id = database.newAccountDao().insert(this).toInt()
}
repeat(2) { time ->
database.newFolderDao().insert(
Folder(
name = "Folder $time",
remoteId = "folder_$time",
accountId = account.id
)
)
}
repeat(3) { time ->
database.newFeedDao().insert(
Feed(
name = "Feed $time",
remoteId = "feed_$time",
remoteFolderId = "folder_${if (time % 2 == 0) 0 else 1}",
accountId = account.id
)
)
}
}
@After
fun after() {
database.close()
}
@Test
fun upsertFeedsTest() = runTest {
val newFeeds = listOf(
// updated feed (name + folder to null)
Feed(
name = "New Feed 0",
remoteId = "feed_0",
remoteFolderId = null,
accountId = account.id
),
// deleted feed
/*Feed(
name = "Feed 1",
remoteId = "feed_1",
remoteFolderId = "folder_1",
accountId = account.id
),*/
// updated feed (folder change)
Feed(
name = "Feed 2",
remoteId = "feed_2",
remoteFolderId = "folder_1",
accountId = account.id
),
// inserted feed
Feed(
name = "Feed 3",
remoteId = "feed_3",
remoteFolderId = "folder_0",
accountId = account.id
),
)
database.newFeedDao().upsertFeeds(newFeeds, account)
val allFeeds = database.newFeedDao().selectFeeds(account.id)
assertTrue(allFeeds.any { it.name == "New Feed 0" && it.folderId == null })
assertTrue(allFeeds.any { it.remoteId == "feed_2" && it.folderId == 2 })
assertFalse(allFeeds.any { it.remoteId == "feed_1" })
assertTrue(allFeeds.any { it.remoteId == "feed_3" && it.folderId == 1 })
}
}

View File

@ -0,0 +1,72 @@
package com.readrops.db.dao
import android.content.Context
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.readrops.db.Database
import com.readrops.db.entities.Folder
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class NewFolderDaoTest {
private lateinit var database: Database
private lateinit var account: Account
@Before
fun before() = runTest {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, Database::class.java).build()
account = Account(accountType = AccountType.LOCAL).apply {
id = database.newAccountDao().insert(this).toInt()
}
repeat(2) { time ->
database.newFolderDao().insert(
Folder(
name = "Folder $time",
remoteId = "folder_$time",
accountId = account.id
)
)
}
}
@After
fun after() {
database.close()
}
@Test
fun upsertFoldersTest() = runTest {
val remoteFolders = listOf(
// updated folder
Folder(name = "New Folder 0", remoteId = "folder_0", accountId = account.id),
// removed folder
//Folder(name = "Folder 1", remoteId = "folder_1"),
// new inserted Folder
Folder(name = "Folder 2", remoteId = "folder_2", accountId = account.id)
)
database.newFolderDao().upsertFolders(remoteFolders, account)
val allFolders = database.newFolderDao().selectFolders(account.id).first()
assertTrue(allFolders.any { it.name == "New Folder 0" })
assertFalse(allFolders.any { it.remoteId == "folder_1" })
assertTrue(allFolders.any { it.remoteId == "folder_2" })
}
}

View File

@ -3,9 +3,11 @@ package com.readrops.db.dao.newdao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.sqlite.db.SupportSQLiteQuery
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Item
import com.readrops.db.entities.account.Account
import com.readrops.db.pojo.FeedWithCount
import kotlinx.coroutines.flow.Flow
@ -35,4 +37,48 @@ abstract class NewFeedDao : NewBaseDao<Feed> {
@Query("Select count(*) from Feed Where account_id = :accountId")
abstract suspend fun selectFeedCount(accountId: Int): Int
@Query("Select remoteId From Feed Where account_id = :accountId")
abstract suspend fun selectFeedRemoteIds(accountId: Int): MutableList<String>
@Query("Select id From Folder Where remoteId = :remoteId And account_id = :accountId")
abstract suspend fun selectRemoteFolderLocalId(remoteId: String, accountId: Int): Int
@Query("Update Feed set name = :name, folder_id = :folderId Where remoteId = :remoteFeedId And account_id = :accountId")
abstract fun updateFeedNameAndFolder(remoteFeedId: String, accountId: Int, name: String, folderId: Int?)
@Query("Delete from Feed Where remoteId in (:ids) And account_id = :accountId")
abstract fun deleteByIds(ids: List<String>, accountId: Int)
/**
* Insert, update and delete feeds by account
*
* @param feeds feeds to insert or update
* @param account owner of the feeds
* @return the list of the inserted feeds ids
*/
@Transaction
open suspend fun upsertFeeds(feeds: List<Feed>, account: Account): List<Long> {
val localFeedIds = selectFeedRemoteIds(account.id)
val feedsToInsert = feeds.filter { feed -> localFeedIds.none { localFeedId -> feed.remoteId == localFeedId } }
val feedsToDelete = localFeedIds.filter { localFeedId -> feeds.none { feed -> localFeedId == feed.remoteId } }
feeds.forEach { feed ->
feed.folderId = if (feed.remoteFolderId == null) {
null
} else {
selectRemoteFolderLocalId(feed.remoteFolderId!!, account.id)
}
// works only for already existing feeds
updateFeedNameAndFolder(feed.remoteId!!, account.id, feed.name!!, feed.folderId)
}
if (feedsToDelete.isNotEmpty()) {
deleteByIds(feedsToDelete, account.id)
}
return insert(feedsToInsert)
}
}

View File

@ -2,7 +2,9 @@ package com.readrops.db.dao.newdao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import com.readrops.db.entities.Folder
import com.readrops.db.entities.account.Account
import com.readrops.db.pojo.FolderWithFeed
import kotlinx.coroutines.flow.Flow
@ -29,4 +31,38 @@ interface NewFolderDao : NewBaseDao<Folder> {
@Query("Select * From Folder Where name = :name And account_id = :accountId")
suspend fun selectFolderByName(name: String, accountId: Int): Folder?
@Query("Select remoteId From Folder Where account_id = :accountId")
suspend fun selectFolderRemoteIds(accountId: Int): List<String>
@Query("Update Folder set name = :name Where remoteId = :remoteId And account_id = :accountId")
suspend fun updateFolderName(name: String, remoteId: String, accountId: Int)
@Query("Delete From Folder Where remoteId in (:ids) And account_id = :accountId")
suspend fun deleteByIds(ids: List<String>, accountId: Int)
/**
* Insert, update and delete folders
*
* @param folders folders to insert or update
* @param account owner of the feeds
* @return the list of the inserted folders ids
*/
@Transaction
suspend fun upsertFolders(folders: List<Folder>, account: Account): List<Long> {
val localFolderIds = selectFolderRemoteIds(account.id)
val foldersToInsert = folders.filter { folder -> localFolderIds.none { localFolderId -> folder.remoteId == localFolderId } }
val foldersToDelete = localFolderIds.filter { localFolderId -> folders.none { folder -> localFolderId == folder.remoteId } }
// folders to update
folders.filter { folder -> localFolderIds.any { localFolderId -> folder.remoteId == localFolderId} }
.forEach { updateFolderName(it.name!!, it.remoteId!!, account.id) }
if (foldersToDelete.isNotEmpty()) {
deleteByIds(foldersToDelete, account.id)
}
return insert(foldersToInsert)
}
}