diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt index ea80ea29b..593dbdd28 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt @@ -7,6 +7,7 @@ import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.ui.NotificationRegistry import at.bitfire.davdroid.ui.account.AccountActivity import dagger.hilt.android.qualifiers.ApplicationContext @@ -20,16 +21,16 @@ class PushNotificationManager @Inject constructor( /** * Generates the notification ID for a push notification. */ - private fun notificationId(account: Account, authority: String): Int { - return account.name.hashCode() + account.type.hashCode() + authority.hashCode() + private fun notificationId(account: Account, dataType: SyncDataType): Int { + return account.name.hashCode() + account.type.hashCode() + dataType.hashCode() } /** * Sends a notification to inform the user that a push notification has been received, the * sync has been scheduled, but it still has not run. */ - fun notify(account: Account, authority: String) { - notificationRegistry.notifyIfPossible(notificationId(account, authority)) { + fun notify(account: Account, dataType: SyncDataType) { + notificationRegistry.notifyIfPossible(notificationId(account, dataType)) { NotificationCompat.Builder(context, notificationRegistry.CHANNEL_STATUS) .setSmallIcon(R.drawable.ic_sync) .setContentTitle(context.getString(R.string.sync_notification_pending_push_title)) @@ -57,9 +58,9 @@ class PushNotificationManager @Inject constructor( * Once the sync has been started, the notification is no longer needed and can be dismissed. * It's safe to call this method even if the notification has not been shown. */ - fun dismiss(account: Account, authority: String) { + fun dismiss(account: Account, dataType: SyncDataType) { NotificationManagerCompat.from(context) - .cancel(notificationId(account, authority)) + .cancel(notificationId(account, dataType)) } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt index 1429e8789..6e561306d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt @@ -22,6 +22,7 @@ import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.account.AccountsCleanupWorker import at.bitfire.davdroid.sync.account.SystemAccountUtils @@ -241,9 +242,8 @@ class AccountRepository @Inject constructor( syncWorkerManager.cancelAllWork(oldAccount) // disable periodic syncs for old account - syncIntervals.forEach { (authority, _) -> - syncWorkerManager.disablePeriodic(oldAccount, authority) - } + for (dataType in SyncDataType.entries) + syncWorkerManager.disablePeriodic(oldAccount, dataType) // update account name references in database serviceRepository.renameAccount(oldName, newName) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt index 7ad390bf4..894f57323 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettingsMigrations.kt @@ -26,6 +26,7 @@ import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalAddressBookStore import at.bitfire.davdroid.resource.LocalTask +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.SyncWorkerManager import at.bitfire.davdroid.util.setAndVerifyUserData @@ -121,7 +122,7 @@ class AccountSettingsMigrations @AssistedInject constructor( /* A maybe existing periodic worker references the old class name (even if it failed and/or is not active). So we need to explicitly disable and prune all workers. Just updating the worker is not enough – WorkManager will update the work details, but not the class name. */ - val disableOp = syncWorkerManager.disablePeriodic(account, authority) + val disableOp = syncWorkerManager.disablePeriodic(account, SyncDataType.fromAuthority(context, authority)) disableOp.result.get() // block until worker with old name is disabled val pruneOp = WorkManager.getInstance(context).pruneWork() @@ -131,7 +132,7 @@ class AccountSettingsMigrations @AssistedInject constructor( if (interval != null && interval != AccountSettings.SYNC_INTERVAL_MANUALLY) { // There's a sync interval for this account/authority; a periodic sync worker should be there, too. val onlyWifi = accountSettings.getSyncWifiOnly() - syncWorkerManager.enablePeriodic(account, authority, interval, onlyWifi) + syncWorkerManager.enablePeriodic(account, SyncDataType.fromAuthority(context, authority), interval, onlyWifi) } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt index ead8869fd..eeb23f7df 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt @@ -5,7 +5,9 @@ package at.bitfire.davdroid.sync import android.accounts.Account +import android.content.Context import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject /** @@ -15,6 +17,7 @@ import javax.inject.Inject * - synchronization on local data changes. */ class AutomaticSyncManager @Inject constructor( + @ApplicationContext private val context: Context, private val syncFramework: SyncFrameworkIntegration, private val workerManager: SyncWorkerManager ) { @@ -23,7 +26,7 @@ class AutomaticSyncManager @Inject constructor( * Disable automatic synchronization for the given account and data type. */ fun disable(account: Account, authority: String) { - workerManager.disablePeriodic(account, authority) + workerManager.disablePeriodic(account, SyncDataType.fromAuthority(context, authority)) syncFramework.disableSyncAbility(account, authority) } @@ -41,9 +44,9 @@ class AutomaticSyncManager @Inject constructor( fun setSyncInterval(account: Account, authority: String, seconds: Long?, wifiOnly: Boolean) { if (seconds != null) { // update sync workers (needs already updated sync interval in AccountSettings) - workerManager.enablePeriodic(account, authority, seconds, wifiOnly) + workerManager.enablePeriodic(account, SyncDataType.fromAuthority(context, authority), seconds, wifiOnly) } else - workerManager.disablePeriodic(account, authority) + workerManager.disablePeriodic(account, SyncDataType.fromAuthority(context, authority)) // Also enable/disable content change triggered syncs if (seconds != null) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt new file mode 100644 index 000000000..4d609d526 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt @@ -0,0 +1,64 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.content.Context +import android.provider.CalendarContract +import android.provider.ContactsContract +import at.bitfire.davdroid.R +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +enum class SyncDataType { + + CONTACTS, + EVENTS, + TASKS; + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncDataTypeEntryPoint { + fun tasksAppManager(): TasksAppManager + } + + + fun toContentAuthority(context: Context): String? { + when (this) { + CONTACTS -> + return ContactsContract.AUTHORITY + EVENTS -> + return CalendarContract.AUTHORITY + TASKS -> { + val entryPoint = EntryPointAccessors.fromApplication(context) + val tasksAppManager = entryPoint.tasksAppManager() + return tasksAppManager.currentProvider()?.authority + } + } + } + + + companion object { + + fun fromAuthority(context: Context, authority: String): SyncDataType { + return when (authority) { + context.getString(R.string.address_books_authority), + ContactsContract.AUTHORITY -> + CONTACTS + CalendarContract.AUTHORITY -> + EVENTS + TaskProvider.ProviderName.JtxBoard.authority, + TaskProvider.ProviderName.TasksOrg.authority, + TaskProvider.ProviderName.OpenTasks.authority -> + TASKS + else -> throw IllegalArgumentException("Unknown authority: $authority") + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt index d77c801cd..623c63f42 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt @@ -25,6 +25,7 @@ import at.bitfire.davdroid.sync.AddressBookSyncer import at.bitfire.davdroid.sync.CalendarSyncer import at.bitfire.davdroid.sync.JtxSyncer import at.bitfire.davdroid.sync.SyncConditions +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncResult import at.bitfire.davdroid.sync.Syncer import at.bitfire.davdroid.sync.TaskSyncer @@ -40,7 +41,7 @@ import java.util.logging.Logger import javax.inject.Inject abstract class BaseSyncWorker( - context: Context, + private val context: Context, private val workerParams: WorkerParameters, private val syncDispatcher: CoroutineDispatcher ) : CoroutineWorker(context, workerParams) { @@ -76,12 +77,12 @@ abstract class BaseSyncWorker( override suspend fun doWork(): Result { // ensure we got the required arguments val account = Account( - inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("$INPUT_ACCOUNT_NAME required"), - inputData.getString(INPUT_ACCOUNT_TYPE) ?: throw IllegalArgumentException("$INPUT_ACCOUNT_TYPE required") + inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("INPUT_ACCOUNT_NAME required"), + inputData.getString(INPUT_ACCOUNT_TYPE) ?: throw IllegalArgumentException("INPUT_ACCOUNT_TYPE required") ) - val authority = inputData.getString(INPUT_AUTHORITY) ?: throw IllegalArgumentException("$INPUT_AUTHORITY required") + val dataType = SyncDataType.valueOf(inputData.getString(INPUT_DATA_TYPE) ?: throw IllegalArgumentException("INPUT_SYNC_DATA_TYPE required")) - val syncTag = commonTag(account, authority) + val syncTag = commonTag(account, dataType) logger.info("${javaClass.simpleName} called for $syncTag") if (!runningSyncs.add(syncTag)) { @@ -90,7 +91,7 @@ abstract class BaseSyncWorker( } // Dismiss any pending push notification - pushNotificationManager.dismiss(account, authority) + pushNotificationManager.dismiss(account, dataType) try { val accountSettings = try { @@ -123,7 +124,12 @@ abstract class BaseSyncWorker( } } - return doSyncWork(account, authority, accountSettings) + val authority = dataType.toContentAuthority(context) + return if (authority == null) { + logger.warning("No content authority found for sync data type $dataType") + Result.failure() + } else + doSyncWork(account, authority, accountSettings) } finally { logger.info("${javaClass.simpleName} finished for $syncTag") runningSyncs -= syncTag @@ -241,7 +247,7 @@ abstract class BaseSyncWorker( // common worker input parameters const val INPUT_ACCOUNT_NAME = "accountName" const val INPUT_ACCOUNT_TYPE = "accountType" - const val INPUT_AUTHORITY = "authority" + const val INPUT_DATA_TYPE = "dataType" /** set to `true` for user-initiated sync that skips network checks */ const val INPUT_MANUAL = "manual" @@ -263,8 +269,6 @@ abstract class BaseSyncWorker( /** * How often this work will be retried to run after soft (network) errors. - * - * Retry strategy is defined in work request ([enqueue]). */ internal const val MAX_RUN_ATTEMPTS = 5 @@ -276,8 +280,8 @@ abstract class BaseSyncWorker( /** * This tag shall be added to every worker that is enqueued by a subclass. */ - fun commonTag(account: Account, authority: String): String = - "sync-$authority ${account.type}/${account.name}" + fun commonTag(account: Account, dataType: SyncDataType): String = + "sync-$dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt index b00a465ec..af4bb7f60 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt @@ -12,6 +12,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncDispatcher import at.bitfire.davdroid.ui.NotificationRegistry import dagger.assisted.Assisted @@ -56,12 +57,13 @@ class OneTimeSyncWorker @AssistedInject constructor( * * Mainly used to query [WorkManager] for work state (by unique work name or tag). * - * @param account the account this worker is running for - * @param authority the authority this worker is running for - * @return Name of this worker composed as "onetime-sync $authority ${account.type}/${account.name}" + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * + * @return Name of this worker composed as "onetime-sync $authority ${account.type}/${account.name}" */ - fun workerName(account: Account, authority: String): String = - "onetime-sync $authority ${account.type}/${account.name}" + fun workerName(account: Account, dataType: SyncDataType): String = + "onetime-sync $dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt index 09de3e045..4a0238dce 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting import androidx.hilt.work.HiltWorker import androidx.work.WorkManager import androidx.work.WorkerParameters +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncDispatcher import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -51,12 +52,13 @@ class PeriodicSyncWorker @AssistedInject constructor( * * Mainly used to query [WorkManager] for work state (by unique work name or tag). * - * @param account the account this worker is running for - * @param authority the authority this worker is running for + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * * @return Name of this worker composed as "periodic-sync $authority ${account.type}/${account.name}" */ - fun workerName(account: Account, authority: String): String = - "periodic-sync $authority ${account.type}/${account.name}" + fun workerName(account: Account, dataType: SyncDataType): String = + "periodic-sync $dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt index 694cd42e4..4f6fa54c4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt @@ -26,10 +26,11 @@ import androidx.work.WorkQuery import androidx.work.WorkRequest import at.bitfire.davdroid.R import at.bitfire.davdroid.push.PushNotificationManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_NAME import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_TYPE -import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_AUTHORITY +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_DATA_TYPE import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_MANUAL import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_RESYNC import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_UPLOAD @@ -67,14 +68,14 @@ class SyncWorkerManager @Inject constructor( */ fun buildOneTime( account: Account, - authority: String, + dataType: SyncDataType, manual: Boolean = false, @InputResync resync: Int = NO_RESYNC, upload: Boolean = false ): OneTimeWorkRequest { // worker arguments val argumentsBuilder = Data.Builder() - .putString(INPUT_AUTHORITY, authority) + .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) .putString(INPUT_ACCOUNT_TYPE, account.type) if (manual) @@ -88,8 +89,8 @@ class SyncWorkerManager @Inject constructor( .setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection .build() return OneTimeWorkRequestBuilder() - .addTag(OneTimeSyncWorker.workerName(account, authority)) - .addTag(commonTag(account, authority)) + .addTag(OneTimeSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) .setInputData(argumentsBuilder.build()) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, @@ -106,11 +107,20 @@ class SyncWorkerManager @Inject constructor( .build() } + @Deprecated("Use buildOneTime(account, dataType, manual, resync, upload) instead") + fun buildOneTime( + account: Account, + authority: String, + manual: Boolean = false, + @InputResync resync: Int = NO_RESYNC, + upload: Boolean = false + ) = buildOneTime(account, SyncDataType.fromAuthority(context, authority), manual, resync, upload) + /** * Requests immediate synchronization of an account with a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]) + * @param dataType type of data to synchronize * @param manual user-initiated sync (ignores network checks) * @param resync whether to request (full) re-synchronization or not * @param upload see [ContentResolver.SYNC_EXTRAS_UPLOAD] – only used for contacts sync and Android 7 workaround @@ -120,24 +130,24 @@ class SyncWorkerManager @Inject constructor( */ fun enqueueOneTime( account: Account, - authority: String, + dataType: SyncDataType, manual: Boolean = false, @InputResync resync: Int = NO_RESYNC, upload: Boolean = false, fromPush: Boolean = false ): String { // enqueue and start syncing - val name = OneTimeSyncWorker.workerName(account, authority) + val name = OneTimeSyncWorker.workerName(account, dataType) val request = buildOneTime( account = account, - authority = authority, + dataType = dataType, manual = manual, resync = resync, upload = upload ) if (fromPush) { logger.fine("Showing push sync pending notification for $name") - pushNotificationManager.notify(account, authority) + pushNotificationManager.notify(account, dataType) } logger.info("Enqueueing unique worker: $name, tags = ${request.tags}") WorkManager.getInstance(context).enqueueUniqueWork( @@ -151,6 +161,16 @@ class SyncWorkerManager @Inject constructor( return name } + @Deprecated("Use enqueueOneTime(account, dataType, manual, resync, upload) instead") + fun enqueueOneTime( + account: Account, + authority: String, + manual: Boolean = false, + @InputResync resync: Int = NO_RESYNC, + upload: Boolean = false, + fromPush: Boolean = false + ): String = enqueueOneTime(account, SyncDataType.fromAuthority(context, authority), manual, resync, upload, fromPush) + /** * Requests immediate synchronization of an account with all applicable * authorities (contacts, calendars, …). @@ -185,9 +205,9 @@ class SyncWorkerManager @Inject constructor( * * @return periodic sync work request for the given arguments */ - fun buildPeriodic(account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest { + fun buildPeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest { val arguments = Data.Builder() - .putString(INPUT_AUTHORITY, authority) + .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) .putString(INPUT_ACCOUNT_TYPE, account.type) .build() @@ -199,25 +219,29 @@ class SyncWorkerManager @Inject constructor( NetworkType.CONNECTED ).build() return PeriodicWorkRequestBuilder(interval, TimeUnit.SECONDS) - .addTag(PeriodicSyncWorker.workerName(account, authority)) - .addTag(commonTag(account, authority)) + .addTag(PeriodicSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) .setInputData(arguments) .setConstraints(constraints) .build() } + @Deprecated("Use buildPeriodic(account, dataType, interval, syncWifiOnly) instead") + fun buildPeriodic(account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest = + buildPeriodic(account, SyncDataType.fromAuthority(context, authority), interval, syncWifiOnly) + /** * Activate periodic synchronization of an account with a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]]) + * @param dataType type of data to synchronize * @param interval interval between recurring syncs in seconds * @return operation object to check when and whether activation was successful */ - fun enablePeriodic(account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): Operation { - val workRequest = buildPeriodic(account, authority, interval, syncWifiOnly) + fun enablePeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): Operation { + val workRequest = buildPeriodic(account, dataType, interval, syncWifiOnly) return WorkManager.getInstance(context).enqueueUniquePeriodicWork( - PeriodicSyncWorker.workerName(account, authority), + PeriodicSyncWorker.workerName(account, dataType), // if a periodic sync exists already, we want to update it with the new interval // and/or new required network type (applies on next iteration of periodic worker) ExistingPeriodicWorkPolicy.UPDATE, @@ -229,12 +253,12 @@ class SyncWorkerManager @Inject constructor( * Disables periodic synchronization of an account for a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]]) + * @param dataType type of data to synchronize * @return operation object to check process state of work cancellation */ - fun disablePeriodic(account: Account, authority: String): Operation = + fun disablePeriodic(account: Account, dataType: SyncDataType): Operation = WorkManager.getInstance(context) - .cancelUniqueWork(PeriodicSyncWorker.workerName(account, authority)) + .cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) // common / helpers @@ -244,10 +268,8 @@ class SyncWorkerManager @Inject constructor( */ fun cancelAllWork(account: Account) { val workManager = WorkManager.getInstance(context) - for (authority in syncAuthorities()) { - workManager.cancelUniqueWork(OneTimeSyncWorker.workerName(account, authority)) - workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, authority)) - } + for (dataType in SyncDataType.entries) + workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) } /** @@ -264,15 +286,15 @@ class SyncWorkerManager @Inject constructor( fun hasAnyFlow( workStates: List, account: Account? = null, - authorities: List? = null, - whichTag: (account: Account, authority: String) -> String = { account, authority -> - commonTag(account, authority) + dataTypes: Iterable? = null, + whichTag: (account: Account, dataType: SyncDataType) -> String = { account, dataType -> + commonTag(account, dataType) } ): Flow { val workQuery = WorkQuery.Builder.fromStates(workStates) - if (account != null && authorities != null) + if (account != null && dataTypes != null) workQuery.addTags( - authorities.map { authority -> whichTag(account, authority) } + dataTypes.map { dataType -> whichTag(account, dataType) } ) return WorkManager.getInstance(context) .getWorkInfosFlow(workQuery.build()) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt index 650d3661e..309a7dc78 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt @@ -18,6 +18,7 @@ import androidx.work.WorkQuery import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker import at.bitfire.davdroid.sync.worker.SyncWorkerManager @@ -84,7 +85,6 @@ class AccountsModel @AssistedInject constructor( private val runningWorkers = workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING)) val accountInfos: Flow> = combine(accounts, runningWorkers) { accounts, workInfos -> - val authorities = syncWorkerManager.syncAuthorities() val collator = Collator.getInstance() accounts @@ -96,15 +96,15 @@ class AccountsModel @AssistedInject constructor( info.state == WorkInfo.State.RUNNING && ( services.any { serviceId -> info.tags.contains(RefreshCollectionsWorker.workerName(serviceId)) - } || authorities.any { authority -> - info.tags.contains(BaseSyncWorker.commonTag(account, authority)) + } || SyncDataType.entries.any { dataType -> + info.tags.contains(BaseSyncWorker.commonTag(account, dataType)) } ) } -> AccountProgress.Active workInfos.any { info -> - info.state == WorkInfo.State.ENQUEUED && authorities.any { authority -> - info.tags.contains(OneTimeSyncWorker.workerName(account, authority)) + info.state == WorkInfo.State.ENQUEUED && SyncDataType.entries.any { dataType -> + info.tags.contains(OneTimeSyncWorker.workerName(account, dataType)) } } -> AccountProgress.Pending diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt index 05384f5a3..7a6dd3618 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -46,6 +46,7 @@ import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncFrameworkIntegration import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.ical4android.TaskProvider @@ -559,20 +560,14 @@ class DebugInfoModel @AssistedInject constructor( */ private fun dumpSyncWorkersInfo(account: Account): String { val table = TextTable("Tags", "Authority", "State", "Next run", "Retries", "Generation", "Periodicity") - listOf( - context.getString(R.string.address_books_authority), - CalendarContract.AUTHORITY, - TaskProvider.ProviderName.JtxBoard.authority, - TaskProvider.ProviderName.OpenTasks.authority, - TaskProvider.ProviderName.TasksOrg.authority - ).forEach { authority -> - val tag = BaseSyncWorker.commonTag(account, authority) + for (dataType in SyncDataType.entries) { + val tag = BaseSyncWorker.commonTag(account, dataType) WorkManager.getInstance(context).getWorkInfos( WorkQuery.Builder.fromTags(listOf(tag)).build() ).get().forEach { workInfo -> table.addLine( workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, - authority, + dataType, "${workInfo.state} (${workInfo.stopReason})", workInfo.nextScheduleTimeMillis.let { nextRun -> when (nextRun) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt index ddc81a4a8..786164e3b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt @@ -9,6 +9,7 @@ import android.content.Context import androidx.work.WorkInfo import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.hilt.android.qualifiers.ApplicationContext @@ -27,11 +28,11 @@ class AccountProgressUseCase @Inject constructor( operator fun invoke( account: Account, serviceFlow: Flow, - authoritiesFlow: Flow> + dataTypes: Iterable ): Flow { val serviceRefreshing = isServiceRefreshing(serviceFlow) - val syncPending = isSyncPending(account, authoritiesFlow) - val syncRunning = isSyncRunning(account, authoritiesFlow) + val syncPending = isSyncPending(account, dataTypes) + val syncRunning = isSyncRunning(account, dataTypes) return combine(serviceRefreshing, syncPending, syncRunning) { refreshing, pending, syncing -> when { @@ -52,27 +53,23 @@ class AccountProgressUseCase @Inject constructor( } @OptIn(ExperimentalCoroutinesApi::class) - fun isSyncPending(account: Account, authoritiesFlow: Flow>): Flow = - authoritiesFlow.flatMapLatest { authorities -> - syncWorkerManager.hasAnyFlow( - workStates = listOf(WorkInfo.State.ENQUEUED), - account = account, - authorities = authorities, - whichTag = { _, authority -> - // we are only interested in pending OneTimeSyncWorkers because there's always a pending PeriodicSyncWorker - OneTimeSyncWorker.workerName(account, authority) - } - ) - } + fun isSyncPending(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.ENQUEUED), + account = account, + dataTypes = dataTypes, + whichTag = { _, authority -> + // we are only interested in pending OneTimeSyncWorkers because there's always a pending PeriodicSyncWorker + OneTimeSyncWorker.workerName(account, authority) + } + ) @OptIn(ExperimentalCoroutinesApi::class) - fun isSyncRunning(account: Account, authoritiesFlow: Flow>): Flow = - authoritiesFlow.flatMapLatest { authorities -> - syncWorkerManager.hasAnyFlow( - workStates = listOf(WorkInfo.State.RUNNING), - account = account, - authorities = authorities - ) - } + fun isSyncRunning(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.RUNNING), + account = account, + dataTypes = dataTypes + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt index 16b04d924..78e3d6eee 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui.account import android.accounts.Account import android.content.Context -import android.provider.CalendarContract import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -23,6 +22,7 @@ import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.assisted.Assisted @@ -35,7 +35,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -95,7 +94,7 @@ class AccountScreenModel @AssistedInject constructor( val cardDavProgress: Flow = accountProgressUseCase( account = account, serviceFlow = cardDavSvc, - authoritiesFlow = flowOf(listOf(context.getString(R.string.address_books_authority))) + dataTypes = listOf(SyncDataType.CONTACTS) ) val addressBooks = getServiceCollectionPager(cardDavSvc, Collection.TYPE_ADDRESSBOOK, showOnlyPersonal) @@ -107,13 +106,10 @@ class AccountScreenModel @AssistedInject constructor( homeSets.isNotEmpty() } val tasksProvider = tasksAppManager.currentProviderFlow(viewModelScope) - private val calDavAuthorities = tasksProvider.map { tasks -> - listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority) - } val calDavProgress = accountProgressUseCase( account = account, serviceFlow = calDavSvc, - authoritiesFlow = calDavAuthorities + dataTypes = listOf(SyncDataType.EVENTS, SyncDataType.TASKS) ) val calendars = getServiceCollectionPager(calDavSvc, Collection.TYPE_CALENDAR, showOnlyPersonal) val subscriptions = getServiceCollectionPager(calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal)