mirror of https://github.com/readrops/Readrops.git
Add background worker for synchronization
This commit is contained in:
parent
bc3015ab95
commit
303d7b6786
|
@ -93,4 +93,5 @@ dependencies {
|
|||
coreLibraryDesugaring(libs.jdk.desugar)
|
||||
|
||||
implementation(libs.encrypted.preferences)
|
||||
implementation(libs.work.manager)
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
android:name=".ReadropsApp"
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package com.readrops.app.compose
|
||||
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.os.Build
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.readrops.api.apiModule
|
||||
|
@ -23,6 +26,8 @@ open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
|
|||
|
||||
modules(apiModule, dbModule, composeAppModule)
|
||||
}
|
||||
|
||||
createNotificationChannels()
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
|
@ -31,4 +36,33 @@ open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
|
|||
.crossfade(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
// TODO check each channel usefulness
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val feedsColorsChannel = NotificationChannel(FEEDS_COLORS_CHANNEL_ID,
|
||||
getString(R.string.feeds_colors), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
feedsColorsChannel.description = getString(R.string.get_feeds_colors)
|
||||
|
||||
val opmlExportChannel = NotificationChannel(OPML_EXPORT_CHANNEL_ID,
|
||||
getString(R.string.opml_export), NotificationManager.IMPORTANCE_DEFAULT)
|
||||
opmlExportChannel.description = getString(R.string.opml_export_description)
|
||||
|
||||
val syncChannel = NotificationChannel(SYNC_CHANNEL_ID,
|
||||
getString(R.string.auto_synchro), NotificationManager.IMPORTANCE_LOW)
|
||||
syncChannel.description = getString(R.string.account_synchro)
|
||||
|
||||
val manager = getSystemService(NotificationManager::class.java)!!
|
||||
|
||||
manager.createNotificationChannel(feedsColorsChannel)
|
||||
manager.createNotificationChannel(opmlExportChannel)
|
||||
manager.createNotificationChannel(syncChannel)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FEEDS_COLORS_CHANNEL_ID = "feedsColorsChannel"
|
||||
const val OPML_EXPORT_CHANNEL_ID = "opmlExportChannel"
|
||||
const val SYNC_CHANNEL_ID = "syncChannel"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.readrops.app.compose.sync
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.app.compose.R
|
||||
import com.readrops.app.compose.ReadropsApp
|
||||
import com.readrops.app.compose.repositories.BaseRepository
|
||||
import com.readrops.app.compose.util.FeedColors
|
||||
import com.readrops.db.Database
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
class SyncWorker(
|
||||
appContext: Context,
|
||||
params: WorkerParameters
|
||||
) : CoroutineWorker(appContext, params), KoinComponent {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(appContext)
|
||||
private val database = get<Database>()
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun doWork(): Result {
|
||||
val sharedPreferences = get<SharedPreferences>()
|
||||
var result = Result.success(workDataOf(END_SYNC_KEY to true))
|
||||
|
||||
try {
|
||||
require(notificationManager.areNotificationsEnabled())
|
||||
|
||||
val notificationBuilder =
|
||||
NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(R.drawable.ic_notifications) // TODO use better icon
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // for Android 7.1 and earlier
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
val accounts = database.newAccountDao().selectAllAccounts().first()
|
||||
|
||||
for (account in accounts) {
|
||||
account.login = sharedPreferences.getString(account.loginKey, null)
|
||||
account.password = sharedPreferences.getString(account.passwordKey, null)
|
||||
|
||||
val repository = get<BaseRepository> { parametersOf(account) }
|
||||
|
||||
notificationBuilder.setContentTitle(
|
||||
applicationContext.resources.getString(
|
||||
R.string.updating_account,
|
||||
account.accountName
|
||||
)
|
||||
)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
val syncResult = repository.synchronize()
|
||||
fetchFeedColors(syncResult, notificationBuilder)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "${e.message}")
|
||||
result = Result.failure(workDataOf(SYNC_FAILURE_KEY to true))
|
||||
} finally {
|
||||
notificationManager.cancel(SYNC_NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
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()) {
|
||||
val feedName = syncResult.feeds.first { it.id == feedId.toInt() }.name
|
||||
|
||||
notificationBuilder.setContentText(feedName)
|
||||
.setProgress(syncResult.newFeedIds.size, index + 1, false)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
val color = try {
|
||||
FeedColors.getFeedColor(syncResult.feeds.first { it.id == feedId.toInt() }.iconUrl!!)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "${e.message}")
|
||||
0
|
||||
}
|
||||
|
||||
database.newFeedDao().updateFeedColor(feedId.toInt(), color)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = SyncWorker::class.java.simpleName
|
||||
|
||||
private const val SYNC_NOTIFICATION_ID = 2
|
||||
private const val SYNC_RESULT_NOTIFICATION_ID = 3
|
||||
|
||||
const val END_SYNC_KEY = "END_SYNC"
|
||||
const val SYNC_FAILURE_KEY = "SYNC_FAILURE"
|
||||
|
||||
suspend fun startNow(context: Context, onUpdate: (WorkInfo) -> Unit) {
|
||||
val request = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).apply {
|
||||
enqueue(request)
|
||||
getWorkInfoByIdFlow(request.id)
|
||||
.collect { workInfo -> onUpdate(workInfo) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package com.readrops.app.compose.timelime
|
|||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
|
@ -12,7 +11,7 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||
import com.readrops.app.compose.base.TabScreenModel
|
||||
import com.readrops.app.compose.repositories.ErrorResult
|
||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
||||
import com.readrops.app.compose.util.FeedColors
|
||||
import com.readrops.app.compose.sync.SyncWorker
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
import com.readrops.db.entities.Folder
|
||||
|
@ -102,38 +101,33 @@ class TimelineScreenModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun refreshTimeline() {
|
||||
fun refreshTimeline(context: Context) {
|
||||
screenModelScope.launch(dispatcher) {
|
||||
if (currentAccount!!.isLocal) {
|
||||
refreshLocalAccount()
|
||||
} else {
|
||||
_timelineState.update { it.copy(isRefreshing = true) }
|
||||
|
||||
try {
|
||||
val result = repository!!.synchronize()
|
||||
|
||||
// TODO put this in a foreground worker
|
||||
for (feedId in result.newFeedIds) {
|
||||
val color = try {
|
||||
FeedColors.getFeedColor(result.feeds.first { it.id == feedId.toInt() }.iconUrl!!)
|
||||
} catch (e: Exception) {
|
||||
0
|
||||
SyncWorker.startNow(context) { data ->
|
||||
when {
|
||||
data.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> {
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
isRefreshing = false,
|
||||
endSynchronizing = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
database.newFeedDao().updateFeedColor(feedId.toInt(), color)
|
||||
data.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> {
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
syncError = Exception(), // TODO see how to improve this
|
||||
isRefreshing = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("TimelineScreenModel", "${e.message}")
|
||||
_timelineState.update { it.copy(syncError = e, isRefreshing = false) }
|
||||
return@launch
|
||||
}
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
isRefreshing = false,
|
||||
endSynchronizing = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -340,6 +334,10 @@ class TimelineScreenModel(
|
|||
fun resetEndSynchronizing() {
|
||||
_timelineState.update { it.copy(endSynchronizing = false) }
|
||||
}
|
||||
|
||||
fun resetSyncError() {
|
||||
_timelineState.update { it.copy(syncError = null) }
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
|
@ -98,7 +98,7 @@ object TimelineTab : Tab {
|
|||
// so we need to listen to the internal state change to trigger the refresh
|
||||
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
||||
if (pullToRefreshState.isRefreshing && !state.isRefreshing) {
|
||||
viewModel.refreshTimeline()
|
||||
viewModel.refreshTimeline(context)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,6 +158,7 @@ object TimelineTab : Tab {
|
|||
LaunchedEffect(state.syncError) {
|
||||
if (state.syncError != null) {
|
||||
snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context))
|
||||
viewModel.resetSyncError()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,7 +273,7 @@ object TimelineTab : Tab {
|
|||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { viewModel.refreshTimeline() }
|
||||
onClick = { viewModel.refreshTimeline(context) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_sync),
|
||||
|
|
|
@ -165,4 +165,5 @@
|
|||
<string name="http_error_5XX">Erreur HTTP %1$d, erreur serveur</string>
|
||||
<string name="http_error">Erreur HTTP %1$d</string>
|
||||
<string name="feed_url_read_only">L\'API de FreshRSS ne permet pas de modifier l\'URL</string>
|
||||
<string name="updating_account">Mise à jour du compte %1$s</string>
|
||||
</resources>
|
|
@ -171,4 +171,5 @@
|
|||
<string name="http_error_5XX">HTTP error %1$d, server error</string>
|
||||
<string name="http_error">HTTP error %1$d</string>
|
||||
<string name="feed_url_read_only">FreshRSS API doesn\'t support Feed URL modification</string>
|
||||
<string name="updating_account">Updating %1$s account</string>
|
||||
</resources>
|
|
@ -67,6 +67,8 @@ jdk-desugar = "com.android.tools:desugar_jdk_libs:2.0.4"
|
|||
|
||||
encrypted-preferences = "androidx.security:security-crypto:1.1.0-alpha06"
|
||||
|
||||
work-manager = "androidx.work:work-runtime-ktx:2.9.0"
|
||||
|
||||
[bundles]
|
||||
compose = ["bom", "compose-foundation", "compose-runtime", "compose-animation",
|
||||
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3"]
|
||||
|
|
Loading…
Reference in New Issue