Add local account refresh logic in SyncWorker

This commit is contained in:
Shinokuni 2024-07-23 15:33:06 +02:00
parent f95c808aa0
commit 5a2ef0fa2f
8 changed files with 173 additions and 88 deletions

View File

@ -3,8 +3,8 @@ package com.readrops.app.repositories
import com.readrops.api.services.Credentials
import com.readrops.api.services.SyncResult
import com.readrops.api.services.SyncType
import com.readrops.api.services.freshrss.FreshRSSSyncData
import com.readrops.api.services.freshrss.FreshRSSDataSource
import com.readrops.api.services.freshrss.FreshRSSSyncData
import com.readrops.api.utils.AuthInterceptor
import com.readrops.app.util.Utils
import com.readrops.db.Database
@ -38,7 +38,7 @@ class FreshRSSRepository(
override suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
onUpdate: suspend (Feed) -> Unit
): Pair<SyncResult, ErrorResult> = throw NotImplementedError("This method can't be called here")
override suspend fun synchronize(): SyncResult {
@ -86,7 +86,7 @@ class FreshRSSRepository(
newFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): ErrorResult {
val errors = mutableMapOf<Feed, Exception>()
val errors = hashMapOf<Feed, Exception>()
for (newFeed in newFeeds) {
onUpdate(newFeed)

View File

@ -29,9 +29,9 @@ class LocalRSSRepository(
override suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
onUpdate: suspend (Feed) -> Unit
): Pair<SyncResult, ErrorResult> {
val errors = mutableMapOf<Feed, Exception>()
val errors = hashMapOf<Feed, Exception>()
val syncResult = SyncResult()
val feeds = selectedFeeds.ifEmpty {
@ -73,7 +73,7 @@ class LocalRSSRepository(
newFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): ErrorResult = withContext(Dispatchers.IO) {
val errors = mutableMapOf<Feed, Exception>()
val errors = hashMapOf<Feed, Exception>()
for (newFeed in newFeeds) {
onUpdate(newFeed)

View File

@ -39,7 +39,7 @@ class NextcloudNewsRepository(
override suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
onUpdate: suspend (Feed) -> Unit
): Pair<SyncResult, ErrorResult> = throw NotImplementedError("This method can't be called here")
override suspend fun synchronize(): SyncResult {
@ -85,7 +85,7 @@ class NextcloudNewsRepository(
newFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
): ErrorResult {
val errors = mutableMapOf<Feed, Exception>()
val errors = hashMapOf<Feed, Exception>()
for (newFeed in newFeeds) {
onUpdate(newFeed)

View File

@ -8,7 +8,7 @@ import com.readrops.db.entities.Item
import com.readrops.db.entities.ItemState
import com.readrops.db.entities.account.Account
typealias ErrorResult = Map<Feed, Exception>
typealias ErrorResult = HashMap<Feed, Exception>
interface Repository {
@ -26,7 +26,7 @@ interface Repository {
*/
suspend fun synchronize(
selectedFeeds: List<Feed>,
onUpdate: (Feed) -> Unit
onUpdate: suspend (Feed) -> Unit
): Pair<SyncResult, ErrorResult>
/**
@ -206,7 +206,7 @@ abstract class BaseRepository(
foldersAndFeeds: Map<Folder?, List<Feed>>,
onUpdate: (Feed) -> Unit
): ErrorResult {
val errors = mutableMapOf<Feed, Exception>()
val errors = hashMapOf<Feed, Exception>()
for ((folder, feeds) in foldersAndFeeds) {
if (folder != null) {

View File

@ -7,6 +7,7 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
@ -16,8 +17,11 @@ import com.readrops.api.services.SyncResult
import com.readrops.app.R
import com.readrops.app.ReadropsApp
import com.readrops.app.repositories.BaseRepository
import com.readrops.app.repositories.ErrorResult
import com.readrops.app.util.FeedColors
import com.readrops.app.util.putSerializable
import com.readrops.db.Database
import com.readrops.db.entities.account.Account
import kotlinx.coroutines.flow.first
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
@ -34,7 +38,7 @@ class SyncWorker(
@SuppressLint("MissingPermission")
override suspend fun doWork(): Result {
val sharedPreferences = get<SharedPreferences>()
var result = Result.success(workDataOf(END_SYNC_KEY to true))
var workResult = Result.success(workDataOf(END_SYNC_KEY to true))
try {
require(notificationManager.areNotificationsEnabled())
@ -47,11 +51,18 @@ class SyncWorker(
.setStyle(NotificationCompat.BigTextStyle())
.setOnlyAlertOnce(true)
val accounts = database.accountDao().selectAllAccounts().first()
val accountId = inputData.getInt(ACCOUNT_ID_KEY, 0)
val accounts = if (accountId == 0) {
database.accountDao().selectAllAccounts().first()
} else {
listOf(database.accountDao().select(accountId))
}
for (account in accounts) {
if (!account.isLocal) {
account.login = sharedPreferences.getString(account.loginKey, null)
account.password = sharedPreferences.getString(account.passwordKey, null)
}
val repository = get<BaseRepository> { parametersOf(account) }
@ -63,21 +74,81 @@ class SyncWorker(
)
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
if (account.isLocal) {
val result = refreshLocalAccount(repository, account)
if (result.second.isNotEmpty()) {
workResult = Result.success(
workDataOf(END_SYNC_KEY to true)
.putSerializable(LOCAL_SYNC_ERRORS_KEY, result.second)
)
}
} else {
val syncResult = repository.synchronize()
fetchFeedColors(syncResult, notificationBuilder)
}
}
} catch (e: Exception) {
Log.e(TAG, "${e.message}")
result = Result.failure(workDataOf(SYNC_FAILURE_KEY to true))
workResult = Result.failure(
workDataOf(SYNC_FAILURE_KEY to true)
.putSerializable(SYNC_FAILURE_EXCEPTION_KEY, e)
)
} finally {
notificationManager.cancel(SYNC_NOTIFICATION_ID)
}
return workResult
}
private suspend fun refreshLocalAccount(
repository: BaseRepository,
account: Account
): Pair<SyncResult, ErrorResult> {
val feedId = inputData.getInt(FEED_ID_KEY, 0)
val folderId = inputData.getInt(FOLDER_ID_KEY, 0)
val feeds = when {
feedId > 0 -> listOf(database.feedDao().selectFeed(feedId))
folderId > 0 -> database.feedDao().selectFeedsByFolder(folderId)
else -> listOf()
}
var feedCount = 0
val feedMax = if (feeds.isNotEmpty()) {
feeds.size
} else {
database.feedDao().selectFeedCount(account.id)
}
val result = repository.synchronize(
selectedFeeds = feeds,
onUpdate = { feed ->
setProgress(
workDataOf(
FEED_NAME_KEY to feed.name,
FEED_MAX_KEY to feedMax,
FEED_COUNT_KEY to ++feedCount
)
)
}
)
if (result.second.isNotEmpty()) {
Log.e(
TAG,
"refreshing local account ${account.accountName}: ${result.second.size} errors"
)
}
return result
}
@SuppressLint("MissingPermission")
private suspend fun fetchFeedColors(syncResult: SyncResult, notificationBuilder: NotificationCompat.Builder) {
private suspend fun fetchFeedColors(
syncResult: SyncResult,
notificationBuilder: NotificationCompat.Builder
) {
notificationBuilder.setContentTitle(applicationContext.getString(R.string.get_feeds_colors))
for ((index, feedId) in syncResult.newFeedIds.withIndex()) {
@ -88,7 +159,8 @@ class SyncWorker(
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
try {
val color = FeedColors.getFeedColor(syncResult.feeds.first { it.id == feedId.toInt() }.iconUrl!!)
val color =
FeedColors.getFeedColor(syncResult.feeds.first { it.id == feedId.toInt() }.iconUrl!!)
database.feedDao().updateFeedColor(feedId.toInt(), color)
} catch (e: Exception) {
Log.e(TAG, "$feedName: ${e.message}")
@ -104,10 +176,19 @@ class SyncWorker(
const val END_SYNC_KEY = "END_SYNC"
const val SYNC_FAILURE_KEY = "SYNC_FAILURE"
const val SYNC_FAILURE_EXCEPTION_KEY = "SYNC_FAILURE_EXCEPTION"
const val ACCOUNT_ID_KEY = "ACCOUNT_ID"
const val FEED_ID_KEY = "FEED_ID"
const val FOLDER_ID_KEY = "FOLDER_ID"
const val FEED_NAME_KEY = "FEED_NAME"
const val FEED_MAX_KEY = "FEED_MAX"
const val FEED_COUNT_KEY = "FEED_COUNT"
const val LOCAL_SYNC_ERRORS_KEY = "LOCAL_SYNC_ERRORS"
suspend fun startNow(context: Context, onUpdate: (WorkInfo) -> Unit) {
suspend fun startNow(context: Context, data: Data, onUpdate: (WorkInfo) -> Unit) {
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag(TAG)
.setInputData(data)
.build()
WorkManager.getInstance(context).apply {

View File

@ -7,12 +7,14 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.work.workDataOf
import cafe.adriel.voyager.core.model.screenModelScope
import com.readrops.app.base.TabScreenModel
import com.readrops.app.repositories.ErrorResult
import com.readrops.app.repositories.GetFoldersWithFeeds
import com.readrops.app.sync.SyncWorker
import com.readrops.app.util.Preferences
import com.readrops.app.util.getSerializable
import com.readrops.db.Database
import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder
@ -141,84 +143,70 @@ class TimelineScreenModel(
}
}
@Suppress("UNCHECKED_CAST")
fun refreshTimeline(context: Context) {
screenModelScope.launch(dispatcher) {
if (currentAccount!!.isLocal) {
refreshLocalAccount()
val filterPair = with(filters.value) {
when (subFilter) {
SubFilter.FEED -> SyncWorker.FEED_ID_KEY to filterFeedId
SubFilter.FOLDER -> SyncWorker.FOLDER_ID_KEY to filterFolderId
else -> null
}
}
val accountPair = SyncWorker.ACCOUNT_ID_KEY to currentAccount!!.id
val workData = if (filterPair != null) {
workDataOf(filterPair, accountPair)
} else {
_timelineState.update { it.copy(isRefreshing = true) }
workDataOf(accountPair)
}
SyncWorker.startNow(context) { data ->
if (!currentAccount!!.isLocal) {
_timelineState.update {
it.copy(isRefreshing = true)
}
}
SyncWorker.startNow(context, workData) { workInfo ->
when {
data.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> {
_timelineState.update {
it.copy(
isRefreshing = false,
scrollToTop = true
)
}
}
data.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> {
_timelineState.update {
it.copy(
syncError = Exception(), // TODO see how to improve this
isRefreshing = false
)
}
}
}
}
}
}
}
private suspend fun refreshLocalAccount() {
val selectedFeeds = when (filters.value.subFilter) {
SubFilter.FEED -> listOf(
database.feedDao().selectFeed(filters.value.filterFeedId)
)
SubFilter.FOLDER -> database.feedDao()
.selectFeedsByFolder(filters.value.filterFolderId)
else -> listOf()
}
_timelineState.update {
it.copy(
feedCount = 0,
feedMax = if (selectedFeeds.isNotEmpty())
selectedFeeds.size
else
database.feedDao().selectFeedCount(currentAccount!!.id)
)
}
_timelineState.update { it.copy(isRefreshing = true, hideReadAllFAB = true) }
val results = repository?.synchronize(
selectedFeeds = selectedFeeds,
onUpdate = { feed ->
_timelineState.update {
it.copy(
currentFeed = feed.name!!,
feedCount = it.feedCount + 1
)
}
}
)
workInfo.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> {
val errors = workInfo.outputData.getSerializable(SyncWorker.LOCAL_SYNC_ERRORS_KEY) as ErrorResult?
_timelineState.update {
it.copy(
isRefreshing = false,
scrollToTop = true,
hideReadAllFAB = false,
localSyncErrors = if (results!!.second.isNotEmpty()) results.second else null
scrollToTop = true,
localSyncErrors = errors?.ifEmpty { null }
)
}
}
workInfo.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> {
val error = workInfo.outputData.getSerializable(SyncWorker.SYNC_FAILURE_EXCEPTION_KEY) as Exception?
_timelineState.update {
it.copy(
syncError = error,
isRefreshing = false,
hideReadAllFAB = false
)
}
}
workInfo.progress.getString(SyncWorker.FEED_NAME_KEY) != null -> {
_timelineState.update {
it.copy(
isRefreshing = true,
hideReadAllFAB = true,
currentFeed = workInfo.progress.getString(SyncWorker.FEED_NAME_KEY) ?: "",
feedCount = workInfo.progress.getInt(SyncWorker.FEED_COUNT_KEY, 0),
feedMax = workInfo.progress.getInt(SyncWorker.FEED_MAX_KEY, 0)
)
}
}
}
}
}
}
fun openDrawer() {
_timelineState.update { it.copy(isDrawerOpen = true) }

View File

@ -6,7 +6,20 @@ import android.net.Uri
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.work.Data
import java.io.Serializable
fun TextStyle.toDp(): Dp = fontSize.value.dp
fun Context.openUrl(url: String) = startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
val Data.serializables by lazy {
mutableMapOf<String, Serializable>()
}
fun Data.putSerializable(key: String, parcelable: Serializable): Data {
serializables[key] = parcelable
return this
}
fun Data.getSerializable(key: String): Serializable? = serializables[key]

View File

@ -18,6 +18,9 @@ interface AccountDao : BaseDao<Account> {
@Insert
suspend fun insertAccount(entity: Account): Long
@Query("Select * From Account Where id = :accountId")
suspend fun select(accountId: Int): Account
@Query("Select * From Account")
fun selectAllAccounts(): Flow<List<Account>>