From b94933d61bf069d38aadadf994549330211b511b Mon Sep 17 00:00:00 2001 From: Shinokuni Date: Wed, 9 Oct 2024 18:57:00 +0200 Subject: [PATCH] Extract synchronization logic from SyncWorker into a separate class --- .../main/java/com/readrops/app/AppModule.kt | 3 + .../java/com/readrops/app/sync/SyncWorker.kt | 254 +++--------------- .../com/readrops/app/sync/Synchronizer.kt | 223 +++++++++++++++ 3 files changed, 265 insertions(+), 215 deletions(-) create mode 100644 app/src/main/java/com/readrops/app/sync/Synchronizer.kt diff --git a/app/src/main/java/com/readrops/app/AppModule.kt b/app/src/main/java/com/readrops/app/AppModule.kt index b44ee70f..6ed2992c 100644 --- a/app/src/main/java/com/readrops/app/AppModule.kt +++ b/app/src/main/java/com/readrops/app/AppModule.kt @@ -24,6 +24,7 @@ import com.readrops.app.repositories.FreshRSSRepository import com.readrops.app.repositories.GetFoldersWithFeeds import com.readrops.app.repositories.LocalRSSRepository import com.readrops.app.repositories.NextcloudNewsRepository +import com.readrops.app.sync.Synchronizer import com.readrops.app.timelime.TimelineScreenModel import com.readrops.app.util.DataStorePreferences import com.readrops.app.util.Preferences @@ -113,4 +114,6 @@ val appModule = module { single { Preferences(get()) } single { NotificationManagerCompat.from(get()) } + + single { Synchronizer(get(), get(), get(), get()) } } \ No newline at end of file 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 cc64e2c1..d9832f04 100644 --- a/app/src/main/java/com/readrops/app/sync/SyncWorker.kt +++ b/app/src/main/java/com/readrops/app/sync/SyncWorker.kt @@ -3,8 +3,6 @@ package com.readrops.app.sync import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.SharedPreferences -import android.graphics.BitmapFactory import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.Action @@ -23,27 +21,17 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.workDataOf -import coil.annotation.ExperimentalCoilApi -import coil.imageLoader -import com.readrops.api.services.Credentials -import com.readrops.api.services.fever.adapters.Favicon -import com.readrops.api.utils.AuthInterceptor import com.readrops.app.MainActivity 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.repositories.SyncResult -import com.readrops.app.util.FeedColors import com.readrops.app.util.putSerializable import com.readrops.db.Database -import com.readrops.db.entities.Feed import com.readrops.db.entities.account.Account import kotlinx.coroutines.flow.first import org.koin.core.component.KoinComponent import org.koin.core.component.get import org.koin.core.component.inject -import org.koin.core.parameter.parametersOf import java.util.concurrent.TimeUnit @@ -64,9 +52,7 @@ class SyncWorker( if (infos.any { it.state == WorkInfo.State.RUNNING && it.id != id }) { return if (isManual) { Result.failure( - workDataOf( - SYNC_FAILURE_KEY to true, - ) + workDataOf(SYNC_FAILURE_KEY to true) .putSerializable( SYNC_FAILURE_EXCEPTION_KEY, Exception(applicationContext.getString(R.string.background_sync_already_running)) @@ -85,14 +71,37 @@ class SyncWorker( .setOnlyAlertOnce(true) return try { - val (workResult, syncResults) = refreshAccounts(notificationBuilder) + val synchronizer = get() + + val (syncResults, errorResult) = synchronizer.synchronizeAccounts( + notificationBuilder = notificationBuilder, + inputData = SyncInputData( + accountId = inputData.getInt(ACCOUNT_ID_KEY, -1), + feedId = inputData.getInt(FEED_ID_KEY, -1), + folderId = inputData.getInt(FOLDER_ID_KEY, -1) + ), + onUpdate = { feed, feedMax, feedCount -> + setProgress( + workDataOf( + FEED_NAME_KEY to feed.name, + FEED_MAX_KEY to feedMax, + FEED_COUNT_KEY to feedCount + ) + ) + } + ) + notificationManager.cancel(SYNC_NOTIFICATION_ID) if (!isManual) { displaySyncResults(syncResults) } - workResult + return Result.success(workDataOf(END_SYNC_KEY to true).apply { + if (errorResult.isNotEmpty() && isManual) { + putSerializable(LOCAL_SYNC_ERRORS_KEY, errorResult) + } + }) } catch (e: Exception) { Log.e(TAG, "${e.printStackTrace()}") @@ -108,188 +117,6 @@ class SyncWorker( } } - private suspend fun refreshAccounts(notificationBuilder: Builder): Pair> { - val sharedPreferences = get() - var workResult = Result.success(workDataOf(END_SYNC_KEY to true)) - val syncResults = mutableMapOf() - - val accountId = inputData.getInt(ACCOUNT_ID_KEY, -1) - val accounts = if (accountId == -1) { - 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 { parametersOf(account) } - - notificationBuilder.setContentTitle( - applicationContext.resources.getString( - R.string.updating_account, - account.name - ) - ) - - if (notificationManager.areNotificationsEnabled()) { - notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - } - - if (account.isLocal) { - val result = refreshLocalAccount(repository, account, notificationBuilder) - - if (result.second.isNotEmpty() && tags.contains(WORK_MANUAL)) { - workResult = Result.success( - workDataOf(END_SYNC_KEY to true) - .putSerializable(LOCAL_SYNC_ERRORS_KEY, result.second) - ) - } - - syncResults[account] = result.first - } else { - get().credentials = Credentials.toCredentials(account) - val syncResult = repository.synchronize() - - if (syncResult.favicons.isNotEmpty()) { - loadFeverFavicons(syncResult.favicons, account, notificationBuilder) - } else { - fetchFeedColors(syncResult, notificationBuilder) - } - - syncResults[account] = syncResult - } - } - - return workResult to syncResults - } - - private suspend fun refreshLocalAccount( - repository: BaseRepository, - account: Account, - notificationBuilder: Builder - ): 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 -> - if (notificationManager.areNotificationsEnabled()) { - notificationBuilder.setContentText(feed.name) - .setStyle(NotificationCompat.BigTextStyle().bigText(feed.name)) - .setProgress(feedMax, ++feedCount, false) - - notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - } - - 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.name}: ${result.second.size} errors" - ) - } - - return result - } - - private suspend fun fetchFeedColors( - syncResult: SyncResult, - notificationBuilder: Builder - ) = with(syncResult) { - notificationBuilder.setContentTitle(applicationContext.getString(R.string.get_feeds_colors)) - - for ((index, feed) in feeds.withIndex()) { - notificationBuilder.setContentText(feed.name) - .setStyle(NotificationCompat.BigTextStyle().bigText(feed.name)) - .setProgress(feeds.size, index + 1, false) - - if (notificationManager.areNotificationsEnabled()) { - notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - } - - try { - if (feed.iconUrl != null) { - val color = FeedColors.getFeedColor(feed.iconUrl!!) - database.feedDao().updateFeedColor(feed.id, color) - } - } catch (e: Exception) { - Log.e(TAG, "${feed.name}: ${e.message}") - } - } - } - - @OptIn(ExperimentalCoilApi::class) - private suspend fun loadFeverFavicons( - favicons: Map, - account: Account, - notificationBuilder: Builder - ) { - if (notificationManager.areNotificationsEnabled()) { - // can't make detailed progress as the favicon might already exist in cache - notificationBuilder.setContentTitle("Loading icons and colors") - .setProgress(0, 0, true) - notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) - } - - val diskCache = applicationContext.imageLoader.diskCache!! - - for ((feed, favicon) in favicons) { - val key = "account_${account.id}_feed_${feed.name!!.replace(" ", "_")}" - val snapshot = diskCache.openSnapshot(key) - - if (snapshot == null) { - try { - diskCache.openEditor(key)!!.apply { - diskCache.fileSystem.write(data) { - write(favicon.data) - } - commit() - } - - database.feedDao().updateFeedIconUrl(feed.id, key) - val bitmap = - BitmapFactory.decodeByteArray(favicon.data, 0, favicon.data.size) - - if (bitmap != null) { - val color = FeedColors.getFeedColor(bitmap) - database.feedDao().updateFeedColor(feed.id, color) - } - } catch (e: Exception) { - Log.e(TAG, "${feed.name}: ${e.message}") - } - } - - snapshot?.close() - } - } - private suspend fun displaySyncResults(syncResults: Map) { val notificationContent = SyncAnalyzer(applicationContext, database) .getNotificationContent(syncResults) @@ -343,14 +170,12 @@ class SyncWorker( putExtra(ITEM_ID_KEY, itemId) } - val pendingIntent = - PendingIntent.getBroadcast( - applicationContext, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - + val pendingIntent = PendingIntent.getBroadcast( + applicationContext, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) return Action.Builder( R.drawable.ic_done_all, @@ -367,13 +192,12 @@ class SyncWorker( putExtra(ITEM_ID_KEY, itemId) } - val pendingIntent = - PendingIntent.getBroadcast( - applicationContext, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) + val pendingIntent = PendingIntent.getBroadcast( + applicationContext, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) return Action.Builder( R.drawable.ic_favorite_border, @@ -390,7 +214,7 @@ class SyncWorker( private val WORK_AUTO = "$TAG-auto" private val WORK_MANUAL = "$TAG-manual" - private const val SYNC_NOTIFICATION_ID = 2 + const val SYNC_NOTIFICATION_ID = 2 const val SYNC_RESULT_NOTIFICATION_ID = 3 const val END_SYNC_KEY = "END_SYNC" diff --git a/app/src/main/java/com/readrops/app/sync/Synchronizer.kt b/app/src/main/java/com/readrops/app/sync/Synchronizer.kt new file mode 100644 index 00000000..c01e1dcd --- /dev/null +++ b/app/src/main/java/com/readrops/app/sync/Synchronizer.kt @@ -0,0 +1,223 @@ +package com.readrops.app.sync + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.BitmapFactory +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat.Builder +import androidx.core.app.NotificationManagerCompat +import coil.annotation.ExperimentalCoilApi +import coil.imageLoader +import com.readrops.api.services.Credentials +import com.readrops.api.services.fever.adapters.Favicon +import com.readrops.api.utils.AuthInterceptor +import com.readrops.app.R +import com.readrops.app.repositories.BaseRepository +import com.readrops.app.repositories.ErrorResult +import com.readrops.app.repositories.SyncResult +import com.readrops.app.sync.SyncWorker.Companion.SYNC_NOTIFICATION_ID +import com.readrops.app.util.FeedColors +import com.readrops.db.Database +import com.readrops.db.entities.Feed +import com.readrops.db.entities.account.Account +import kotlinx.coroutines.flow.first +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import org.koin.core.parameter.parametersOf + +data class SyncInputData( + val accountId: Int, + val feedId: Int, + val folderId: Int +) + +class Synchronizer( + private val notificationManager: NotificationManagerCompat, + private val database: Database, + private val context: Context, + private val encryptedPreferences: SharedPreferences, +) : KoinComponent { + + suspend fun synchronizeAccounts( + notificationBuilder: Builder, + inputData: SyncInputData, + onUpdate: suspend (feed: Feed, feedMax: Int, feedCount: Int) -> Unit + ): Pair, ErrorResult> { + val syncResults = mutableMapOf() + val errorResult = hashMapOf() + + val accounts = if (inputData.accountId == -1) { + database.accountDao().selectAllAccounts().first() + } else { + listOf(database.accountDao().select(inputData.accountId)) + } + + for (account in accounts) { + if (!account.isLocal) { + account.login = encryptedPreferences.getString(account.loginKey, null) + account.password = encryptedPreferences.getString(account.passwordKey, null) + } + + val repository = get { parametersOf(account) } + + notificationBuilder.setContentTitle( + context.resources.getString( + R.string.updating_account, + account.name + ) + ) + + if (notificationManager.areNotificationsEnabled()) { + notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) + } + + if (account.isLocal) { + val result = refreshLocalAccount( + repository = repository, + account = account, + notificationBuilder = notificationBuilder, + inputData = inputData, + onUpdate = onUpdate + ) + + syncResults[account] = result.first + errorResult.putAll(result.second) + } else { + get().credentials = Credentials.toCredentials(account) + val syncResult = repository.synchronize() + + if (syncResult.favicons.isNotEmpty()) { + loadFeverFavicons(syncResult.favicons, account, notificationBuilder) + } else { + fetchFeedColors(syncResult, notificationBuilder) + } + + syncResults[account] = syncResult + } + } + + return syncResults to errorResult + } + + private suspend fun refreshLocalAccount( + repository: BaseRepository, + account: Account, + notificationBuilder: Builder, + inputData: SyncInputData, + onUpdate: suspend (feed: Feed, feedMax: Int, feedCount: Int) -> Unit + ): Pair { + val feedId = inputData.feedId + val folderId = inputData.folderId + + 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 -> + if (notificationManager.areNotificationsEnabled()) { + notificationBuilder.setContentText(feed.name) + .setStyle(NotificationCompat.BigTextStyle().bigText(feed.name)) + .setProgress(feedMax, ++feedCount, false) + + notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) + } + + onUpdate(feed, feedMax, feedCount) + } + ) + + if (result.second.isNotEmpty()) { + Log.e(TAG, "refreshing local account ${account.name}: ${result.second.size} errors") + } + + return result + } + + private suspend fun fetchFeedColors( + syncResult: SyncResult, + notificationBuilder: Builder + ) = with(syncResult) { + notificationBuilder.setContentTitle(context.getString(R.string.get_feeds_colors)) + + for ((index, feed) in feeds.withIndex()) { + notificationBuilder.setContentText(feed.name) + .setStyle(NotificationCompat.BigTextStyle().bigText(feed.name)) + .setProgress(feeds.size, index + 1, false) + + if (notificationManager.areNotificationsEnabled()) { + notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) + } + + try { + if (feed.iconUrl != null) { + val color = FeedColors.getFeedColor(feed.iconUrl!!) + database.feedDao().updateFeedColor(feed.id, color) + } + } catch (e: Exception) { + Log.e(TAG, "${feed.name}: ${e.message}") + } + } + } + + @OptIn(ExperimentalCoilApi::class) + private suspend fun loadFeverFavicons( + favicons: Map, + account: Account, + notificationBuilder: Builder + ) { + if (notificationManager.areNotificationsEnabled()) { + // can't make detailed progress as the favicon might already exist in cache + notificationBuilder.setContentTitle("Loading icons and colors") + .setProgress(0, 0, true) + notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build()) + } + + val diskCache = context.imageLoader.diskCache!! + + for ((feed, favicon) in favicons) { + val key = "account_${account.id}_feed_${feed.name!!.replace(" ", "_")}" + val snapshot = diskCache.openSnapshot(key) + + if (snapshot == null) { + try { + diskCache.openEditor(key)!!.apply { + diskCache.fileSystem.write(data) { + write(favicon.data) + } + + commit() + } + + database.feedDao().updateFeedIconUrl(feed.id, key) + val bitmap = + BitmapFactory.decodeByteArray(favicon.data, 0, favicon.data.size) + + if (bitmap != null) { + val color = FeedColors.getFeedColor(bitmap) + database.feedDao().updateFeedColor(feed.id, color) + } + } catch (e: Exception) { + Log.e(TAG, "${feed.name}: ${e.message}") + } + } + + snapshot?.close() + } + } + + companion object { + private val TAG = Synchronizer::class.java.simpleName + } +} \ No newline at end of file