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)
|
coreLibraryDesugaring(libs.jdk.desugar)
|
||||||
|
|
||||||
implementation(libs.encrypted.preferences)
|
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.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".ReadropsApp"
|
android:name=".ReadropsApp"
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package com.readrops.app.compose
|
package com.readrops.app.compose
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.os.Build
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.ImageLoaderFactory
|
import coil.ImageLoaderFactory
|
||||||
import com.readrops.api.apiModule
|
import com.readrops.api.apiModule
|
||||||
|
@ -23,6 +26,8 @@ open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
|
||||||
|
|
||||||
modules(apiModule, dbModule, composeAppModule)
|
modules(apiModule, dbModule, composeAppModule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createNotificationChannels()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun newImageLoader(): ImageLoader {
|
override fun newImageLoader(): ImageLoader {
|
||||||
|
@ -31,4 +36,33 @@ open class ReadropsApp : Application(), KoinComponent, ImageLoaderFactory {
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build()
|
.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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
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.base.TabScreenModel
|
||||||
import com.readrops.app.compose.repositories.ErrorResult
|
import com.readrops.app.compose.repositories.ErrorResult
|
||||||
import com.readrops.app.compose.repositories.GetFoldersWithFeeds
|
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.Database
|
||||||
import com.readrops.db.entities.Feed
|
import com.readrops.db.entities.Feed
|
||||||
import com.readrops.db.entities.Folder
|
import com.readrops.db.entities.Folder
|
||||||
|
@ -102,33 +101,16 @@ class TimelineScreenModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshTimeline() {
|
fun refreshTimeline(context: Context) {
|
||||||
screenModelScope.launch(dispatcher) {
|
screenModelScope.launch(dispatcher) {
|
||||||
if (currentAccount!!.isLocal) {
|
if (currentAccount!!.isLocal) {
|
||||||
refreshLocalAccount()
|
refreshLocalAccount()
|
||||||
} else {
|
} else {
|
||||||
_timelineState.update { it.copy(isRefreshing = true) }
|
_timelineState.update { it.copy(isRefreshing = true) }
|
||||||
|
|
||||||
try {
|
SyncWorker.startNow(context) { data ->
|
||||||
val result = repository!!.synchronize()
|
when {
|
||||||
|
data.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> {
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
database.newFeedDao().updateFeedColor(feedId.toInt(), color)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e("TimelineScreenModel", "${e.message}")
|
|
||||||
_timelineState.update { it.copy(syncError = e, isRefreshing = false) }
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
|
|
||||||
_timelineState.update {
|
_timelineState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
|
@ -136,6 +118,18 @@ class TimelineScreenModel(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> {
|
||||||
|
_timelineState.update {
|
||||||
|
it.copy(
|
||||||
|
syncError = Exception(), // TODO see how to improve this
|
||||||
|
isRefreshing = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,6 +334,10 @@ class TimelineScreenModel(
|
||||||
fun resetEndSynchronizing() {
|
fun resetEndSynchronizing() {
|
||||||
_timelineState.update { it.copy(endSynchronizing = false) }
|
_timelineState.update { it.copy(endSynchronizing = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetSyncError() {
|
||||||
|
_timelineState.update { it.copy(syncError = null) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
|
@ -98,7 +98,7 @@ object TimelineTab : Tab {
|
||||||
// so we need to listen to the internal state change to trigger the refresh
|
// so we need to listen to the internal state change to trigger the refresh
|
||||||
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
LaunchedEffect(pullToRefreshState.isRefreshing) {
|
||||||
if (pullToRefreshState.isRefreshing && !state.isRefreshing) {
|
if (pullToRefreshState.isRefreshing && !state.isRefreshing) {
|
||||||
viewModel.refreshTimeline()
|
viewModel.refreshTimeline(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,6 +158,7 @@ object TimelineTab : Tab {
|
||||||
LaunchedEffect(state.syncError) {
|
LaunchedEffect(state.syncError) {
|
||||||
if (state.syncError != null) {
|
if (state.syncError != null) {
|
||||||
snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context))
|
snackbarHostState.showSnackbar(ErrorMessage.get(state.syncError!!, context))
|
||||||
|
viewModel.resetSyncError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,7 +273,7 @@ object TimelineTab : Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.refreshTimeline() }
|
onClick = { viewModel.refreshTimeline(context) }
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_sync),
|
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_5XX">Erreur HTTP %1$d, erreur serveur</string>
|
||||||
<string name="http_error">Erreur HTTP %1$d</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="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>
|
</resources>
|
|
@ -171,4 +171,5 @@
|
||||||
<string name="http_error_5XX">HTTP error %1$d, server error</string>
|
<string name="http_error_5XX">HTTP error %1$d, server error</string>
|
||||||
<string name="http_error">HTTP error %1$d</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="feed_url_read_only">FreshRSS API doesn\'t support Feed URL modification</string>
|
||||||
|
<string name="updating_account">Updating %1$s account</string>
|
||||||
</resources>
|
</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"
|
encrypted-preferences = "androidx.security:security-crypto:1.1.0-alpha06"
|
||||||
|
|
||||||
|
work-manager = "androidx.work:work-runtime-ktx:2.9.0"
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
compose = ["bom", "compose-foundation", "compose-runtime", "compose-animation",
|
compose = ["bom", "compose-foundation", "compose-runtime", "compose-animation",
|
||||||
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3"]
|
"compose-ui", "compose-ui-tooling", "compose-ui-tooling-preview", "compose-material3"]
|
||||||
|
|
Loading…
Reference in New Issue