Add background synchronization launch in PreferencesScreen
This commit is contained in:
parent
5a2ef0fa2f
commit
63867d3acd
@ -14,6 +14,7 @@ import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
@ -23,6 +24,7 @@ import com.readrops.app.R
|
||||
import com.readrops.app.more.preferences.components.ListPreferenceWidget
|
||||
import com.readrops.app.more.preferences.components.PreferenceHeader
|
||||
import com.readrops.app.more.preferences.components.SwitchPreferenceWidget
|
||||
import com.readrops.app.sync.SyncWorker
|
||||
import com.readrops.app.util.components.AndroidScreen
|
||||
import com.readrops.app.util.components.CenteredProgressIndicator
|
||||
|
||||
@ -33,6 +35,7 @@ class PreferencesScreen : AndroidScreen() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val screenModel = getScreenModel<PreferencesScreenModel>()
|
||||
|
||||
val state by screenModel.state.collectAsStateWithLifecycle()
|
||||
@ -93,7 +96,7 @@ class PreferencesScreen : AndroidScreen() {
|
||||
"24" to stringResource(id = R.string.every_day)
|
||||
),
|
||||
title = stringResource(id = R.string.auto_synchro),
|
||||
onValueChange = {}
|
||||
onValueChange = { SyncWorker.startPeriodically(context, it) }
|
||||
)
|
||||
|
||||
PreferenceHeader(text = stringResource(id = R.string.timeline))
|
||||
|
@ -3,17 +3,27 @@ package com.readrops.app.sync
|
||||
import android.annotation.SuppressLint
|
||||
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 androidx.work.BackoffPolicy
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.NetworkType
|
||||
import androidx.work.OneTimeWorkRequestBuilder
|
||||
import androidx.work.PeriodicWorkRequest
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.workDataOf
|
||||
import com.readrops.api.services.Credentials
|
||||
import com.readrops.api.services.SyncResult
|
||||
import com.readrops.api.utils.AuthInterceptor
|
||||
import com.readrops.app.R
|
||||
import com.readrops.app.ReadropsApp
|
||||
import com.readrops.app.repositories.BaseRepository
|
||||
@ -25,7 +35,10 @@ 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
|
||||
|
||||
|
||||
class SyncWorker(
|
||||
appContext: Context,
|
||||
@ -33,63 +46,47 @@ class SyncWorker(
|
||||
) : CoroutineWorker(appContext, params), KoinComponent {
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(appContext)
|
||||
private val database = get<Database>()
|
||||
private val database by inject<Database>()
|
||||
|
||||
// TODO handle notification permission for Android 14+ (or 15?)
|
||||
@SuppressLint("MissingPermission")
|
||||
override suspend fun doWork(): Result {
|
||||
val sharedPreferences = get<SharedPreferences>()
|
||||
var workResult = Result.success(workDataOf(END_SYNC_KEY to true))
|
||||
val workManager = WorkManager.getInstance(applicationContext)
|
||||
|
||||
val infos = workManager.getWorkInfosByTagFlow(TAG).first()
|
||||
if (infos.any { it.state == WorkInfo.State.RUNNING && it.id != id }) {
|
||||
return if (tags.contains(WORK_MANUAL)) {
|
||||
Result.failure(
|
||||
workDataOf(
|
||||
SYNC_FAILURE_KEY to true,
|
||||
)
|
||||
.putSerializable(
|
||||
SYNC_FAILURE_EXCEPTION_KEY,
|
||||
Exception(applicationContext.getString(R.string.background_sync_already_running))
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Result.retry()
|
||||
}
|
||||
}
|
||||
|
||||
var workResult: Result
|
||||
try {
|
||||
require(notificationManager.areNotificationsEnabled())
|
||||
|
||||
val notificationBuilder =
|
||||
NotificationCompat.Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
Builder(applicationContext, ReadropsApp.SYNC_CHANNEL_ID)
|
||||
.setProgress(0, 0, true)
|
||||
.setSmallIcon(R.drawable.ic_notifications) // TODO use better icon
|
||||
.setLargeIcon(BitmapFactory.decodeResource(applicationContext.resources, R.mipmap.ic_launcher_round))
|
||||
.setSmallIcon(R.drawable.ic_sync)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT) // for Android 7.1 and earlier
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
|
||||
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) }
|
||||
|
||||
notificationBuilder.setContentTitle(
|
||||
applicationContext.resources.getString(
|
||||
R.string.updating_account,
|
||||
account.accountName
|
||||
)
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
workResult = refreshAccounts(notificationBuilder)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "${e.message}")
|
||||
Log.e(TAG, "${e::class.simpleName}: ${e.message} ${e.printStackTrace()}")
|
||||
workResult = Result.failure(
|
||||
workDataOf(SYNC_FAILURE_KEY to true)
|
||||
.putSerializable(SYNC_FAILURE_EXCEPTION_KEY, e)
|
||||
@ -101,9 +98,59 @@ class SyncWorker(
|
||||
return workResult
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun refreshAccounts(notificationBuilder: Builder): Result {
|
||||
val sharedPreferences = get<SharedPreferences>()
|
||||
var workResult = Result.success(workDataOf(END_SYNC_KEY to true))
|
||||
|
||||
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) }
|
||||
|
||||
notificationBuilder.setContentTitle(
|
||||
applicationContext.resources.getString(
|
||||
R.string.updating_account,
|
||||
account.accountName
|
||||
)
|
||||
)
|
||||
notificationManager.notify(SYNC_NOTIFICATION_ID, notificationBuilder.build())
|
||||
|
||||
if (account.isLocal) {
|
||||
val result = refreshLocalAccount(repository, account, notificationBuilder)
|
||||
|
||||
if (result.second.isNotEmpty()) {
|
||||
workResult = Result.success(
|
||||
workDataOf(END_SYNC_KEY to true)
|
||||
.putSerializable(LOCAL_SYNC_ERRORS_KEY, result.second)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
get<AuthInterceptor>().credentials = Credentials.toCredentials(account)
|
||||
|
||||
val syncResult = repository.synchronize()
|
||||
fetchFeedColors(syncResult, notificationBuilder)
|
||||
}
|
||||
}
|
||||
|
||||
return workResult
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun refreshLocalAccount(
|
||||
repository: BaseRepository,
|
||||
account: Account
|
||||
account: Account,
|
||||
notificationBuilder: Builder
|
||||
): Pair<SyncResult, ErrorResult> {
|
||||
val feedId = inputData.getInt(FEED_ID_KEY, 0)
|
||||
val folderId = inputData.getInt(FOLDER_ID_KEY, 0)
|
||||
@ -124,11 +171,15 @@ class SyncWorker(
|
||||
val result = repository.synchronize(
|
||||
selectedFeeds = feeds,
|
||||
onUpdate = { feed ->
|
||||
notificationBuilder.setContentText(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
|
||||
FEED_COUNT_KEY to feedCount
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -147,7 +198,7 @@ class SyncWorker(
|
||||
@SuppressLint("MissingPermission")
|
||||
private suspend fun fetchFeedColors(
|
||||
syncResult: SyncResult,
|
||||
notificationBuilder: NotificationCompat.Builder
|
||||
notificationBuilder: Builder
|
||||
) {
|
||||
notificationBuilder.setContentTitle(applicationContext.getString(R.string.get_feeds_colors))
|
||||
|
||||
@ -171,6 +222,9 @@ class SyncWorker(
|
||||
companion object {
|
||||
private val TAG: String = SyncWorker::class.java.simpleName
|
||||
|
||||
private val WORK_AUTO = "$TAG-auto"
|
||||
private val WORK_MANUAL = "$TAG-manual"
|
||||
|
||||
private const val SYNC_NOTIFICATION_ID = 2
|
||||
private const val SYNC_RESULT_NOTIFICATION_ID = 3
|
||||
|
||||
@ -188,13 +242,59 @@ class SyncWorker(
|
||||
suspend fun startNow(context: Context, data: Data, onUpdate: (WorkInfo) -> Unit) {
|
||||
val request = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_MANUAL)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).apply {
|
||||
enqueue(request)
|
||||
enqueueUniqueWork(WORK_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||
getWorkInfoByIdFlow(request.id)
|
||||
.collect { workInfo -> onUpdate(workInfo) }
|
||||
.collect { workInfo ->
|
||||
if (workInfo != null) {
|
||||
onUpdate(workInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startPeriodically(context: Context, period: String) {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
|
||||
val interval = when (period) {
|
||||
"0.30" -> 30L to TimeUnit.MINUTES
|
||||
"1" -> 1L to TimeUnit.HOURS
|
||||
"2" -> 2L to TimeUnit.HOURS
|
||||
"3" -> 3L to TimeUnit.HOURS
|
||||
"6" -> 6L to TimeUnit.HOURS
|
||||
"12" -> 12L to TimeUnit.HOURS
|
||||
"24" -> 1L to TimeUnit.DAYS
|
||||
else -> null
|
||||
}
|
||||
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
if (interval != null) {
|
||||
val request = PeriodicWorkRequest.Builder(
|
||||
SyncWorker::class.java,
|
||||
interval.first,
|
||||
interval.second
|
||||
)
|
||||
.addTag(TAG)
|
||||
.addTag(WORK_AUTO)
|
||||
.setConstraints(constraints)
|
||||
.setInitialDelay(interval.first, interval.second)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, interval.first, interval.second)
|
||||
.build()
|
||||
|
||||
workManager.enqueueUniquePeriodicWork(
|
||||
WORK_AUTO,
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
request
|
||||
)
|
||||
} else {
|
||||
workManager.cancelAllWorkByTag(WORK_AUTO)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ 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.clearSerializables
|
||||
import com.readrops.app.util.getSerializable
|
||||
import com.readrops.db.Database
|
||||
import com.readrops.db.entities.Feed
|
||||
@ -171,6 +172,7 @@ class TimelineScreenModel(
|
||||
when {
|
||||
workInfo.outputData.getBoolean(SyncWorker.END_SYNC_KEY, false) -> {
|
||||
val errors = workInfo.outputData.getSerializable(SyncWorker.LOCAL_SYNC_ERRORS_KEY) as ErrorResult?
|
||||
workInfo.outputData.clearSerializables()
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
@ -183,6 +185,7 @@ class TimelineScreenModel(
|
||||
}
|
||||
workInfo.outputData.getBoolean(SyncWorker.SYNC_FAILURE_KEY, false) -> {
|
||||
val error = workInfo.outputData.getSerializable(SyncWorker.SYNC_FAILURE_EXCEPTION_KEY) as Exception?
|
||||
workInfo.outputData.clearSerializables()
|
||||
|
||||
_timelineState.update {
|
||||
it.copy(
|
||||
|
@ -23,3 +23,7 @@ fun Data.putSerializable(key: String, parcelable: Serializable): Data {
|
||||
}
|
||||
|
||||
fun Data.getSerializable(key: String): Serializable? = serializables[key]
|
||||
|
||||
fun Data.clearSerializables() {
|
||||
serializables.clear()
|
||||
}
|
||||
|
@ -184,4 +184,5 @@
|
||||
<string name="regular">Normal</string>
|
||||
<string name="large">Large</string>
|
||||
<string name="item_size">Taille des items</string>
|
||||
<string name="background_sync_already_running">Une synchronisation en arrière-plan est déjà en cours</string>
|
||||
</resources>
|
@ -193,4 +193,5 @@
|
||||
<string name="regular">Regular</string>
|
||||
<string name="large">Large</string>
|
||||
<string name="item_size">Item size</string>
|
||||
<string name="background_sync_already_running">A background synchronization is already running</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user