From 5a2ef0fa2f004a7a9592b21a0e466b558f1e0bdc Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Tue, 23 Jul 2024 15:33:06 +0200 Subject: [PATCH] Add local account refresh logic in SyncWorker --- .../app/repositories/FreshRSSRepository.kt | 6 +- .../app/repositories/LocalRSSRepository.kt | 6 +- .../repositories/NextcloudNewsRepository.kt | 4 +- .../readrops/app/repositories/Repository.kt | 6 +- .../java/com/readrops/app/sync/SyncWorker.kt | 101 +++++++++++++-- .../app/timelime/TimelineScreenModel.kt | 120 ++++++++---------- .../java/com/readrops/app/util/Extensions.kt | 15 ++- .../java/com/readrops/db/dao/AccountDao.kt | 3 + 8 files changed, 173 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt index 25d27bf4..91a902fb 100644 --- a/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt +++ b/app/src/main/java/com/readrops/app/repositories/FreshRSSRepository.kt @@ -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, - onUpdate: (Feed) -> Unit + onUpdate: suspend (Feed) -> Unit ): Pair = throw NotImplementedError("This method can't be called here") override suspend fun synchronize(): SyncResult { @@ -86,7 +86,7 @@ class FreshRSSRepository( newFeeds: List, onUpdate: (Feed) -> Unit ): ErrorResult { - val errors = mutableMapOf() + val errors = hashMapOf() for (newFeed in newFeeds) { onUpdate(newFeed) diff --git a/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt b/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt index f0dc9608..ebe5f044 100644 --- a/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt +++ b/app/src/main/java/com/readrops/app/repositories/LocalRSSRepository.kt @@ -29,9 +29,9 @@ class LocalRSSRepository( override suspend fun synchronize( selectedFeeds: List, - onUpdate: (Feed) -> Unit + onUpdate: suspend (Feed) -> Unit ): Pair { - val errors = mutableMapOf() + val errors = hashMapOf() val syncResult = SyncResult() val feeds = selectedFeeds.ifEmpty { @@ -73,7 +73,7 @@ class LocalRSSRepository( newFeeds: List, onUpdate: (Feed) -> Unit ): ErrorResult = withContext(Dispatchers.IO) { - val errors = mutableMapOf() + val errors = hashMapOf() for (newFeed in newFeeds) { onUpdate(newFeed) diff --git a/app/src/main/java/com/readrops/app/repositories/NextcloudNewsRepository.kt b/app/src/main/java/com/readrops/app/repositories/NextcloudNewsRepository.kt index 27df2a73..0dcf20de 100644 --- a/app/src/main/java/com/readrops/app/repositories/NextcloudNewsRepository.kt +++ b/app/src/main/java/com/readrops/app/repositories/NextcloudNewsRepository.kt @@ -39,7 +39,7 @@ class NextcloudNewsRepository( override suspend fun synchronize( selectedFeeds: List, - onUpdate: (Feed) -> Unit + onUpdate: suspend (Feed) -> Unit ): Pair = throw NotImplementedError("This method can't be called here") override suspend fun synchronize(): SyncResult { @@ -85,7 +85,7 @@ class NextcloudNewsRepository( newFeeds: List, onUpdate: (Feed) -> Unit ): ErrorResult { - val errors = mutableMapOf() + val errors = hashMapOf() for (newFeed in newFeeds) { onUpdate(newFeed) diff --git a/app/src/main/java/com/readrops/app/repositories/Repository.kt b/app/src/main/java/com/readrops/app/repositories/Repository.kt index c7daf131..309b9b28 100644 --- a/app/src/main/java/com/readrops/app/repositories/Repository.kt +++ b/app/src/main/java/com/readrops/app/repositories/Repository.kt @@ -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 +typealias ErrorResult = HashMap interface Repository { @@ -26,7 +26,7 @@ interface Repository { */ suspend fun synchronize( selectedFeeds: List, - onUpdate: (Feed) -> Unit + onUpdate: suspend (Feed) -> Unit ): Pair /** @@ -206,7 +206,7 @@ abstract class BaseRepository( foldersAndFeeds: Map>, onUpdate: (Feed) -> Unit ): ErrorResult { - val errors = mutableMapOf() + val errors = hashMapOf() for ((folder, feeds) in foldersAndFeeds) { if (folder != null) { diff --git a/app/src/main/java/com/readrops/app/sync/SyncWorker.kt b/app/src/main/java/com/readrops/app/sync/SyncWorker.kt index b23925ec..1a86fd67 100644 --- a/app/src/main/java/com/readrops/app/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/sync/SyncWorker.kt @@ -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() - 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) { - account.login = sharedPreferences.getString(account.loginKey, null) - account.password = sharedPreferences.getString(account.passwordKey, null) + if (!account.isLocal) { + account.login = sharedPreferences.getString(account.loginKey, null) + account.password = sharedPreferences.getString(account.passwordKey, null) + } val repository = get { parametersOf(account) } @@ -63,21 +74,81 @@ class SyncWorker( ) notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - val syncResult = repository.synchronize() - fetchFeedColors(syncResult, notificationBuilder) + 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 { + 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() .addTag(TAG) + .setInputData(data) .build() WorkManager.getInstance(context).apply { diff --git a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt index de006449..f98b48b1 100644 --- a/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt +++ b/app/src/main/java/com/readrops/app/timelime/TimelineScreenModel.kt @@ -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,31 +143,64 @@ 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 -> - when { - data.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> { - _timelineState.update { - it.copy( - isRefreshing = false, - scrollToTop = true - ) - } + if (!currentAccount!!.isLocal) { + _timelineState.update { + it.copy(isRefreshing = true) + } + } + + SyncWorker.startNow(context, workData) { workInfo -> + when { + 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, + hideReadAllFAB = false, + 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? - data.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> { - _timelineState.update { - it.copy( - syncError = Exception(), // TODO see how to improve this - isRefreshing = false - ) - } + _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) + ) } } } @@ -173,53 +208,6 @@ class TimelineScreenModel( } } - 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 - ) - } - } - ) - - _timelineState.update { - it.copy( - isRefreshing = false, - scrollToTop = true, - hideReadAllFAB = false, - localSyncErrors = if (results!!.second.isNotEmpty()) results.second else null - ) - } - } - fun openDrawer() { _timelineState.update { it.copy(isDrawerOpen = true) } } diff --git a/app/src/main/java/com/readrops/app/util/Extensions.kt b/app/src/main/java/com/readrops/app/util/Extensions.kt index 44f46660..6288f3be 100644 --- a/app/src/main/java/com/readrops/app/util/Extensions.kt +++ b/app/src/main/java/com/readrops/app/util/Extensions.kt @@ -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))) \ No newline at end of file +fun Context.openUrl(url: String) = startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + +val Data.serializables by lazy { + mutableMapOf() +} + +fun Data.putSerializable(key: String, parcelable: Serializable): Data { + serializables[key] = parcelable + return this +} + +fun Data.getSerializable(key: String): Serializable? = serializables[key] diff --git a/db/src/main/java/com/readrops/db/dao/AccountDao.kt b/db/src/main/java/com/readrops/db/dao/AccountDao.kt index 828b0d7a..0abe8a36 100644 --- a/db/src/main/java/com/readrops/db/dao/AccountDao.kt +++ b/db/src/main/java/com/readrops/db/dao/AccountDao.kt @@ -18,6 +18,9 @@ interface AccountDao : BaseDao { @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>