Include address book account syncs, when querying sync status (bitfireAT/davx5#378)

* Use tags instead of uniqueWorkNames for work queries.

* Also include address book accounts, when querying sync status.

Give address book account sync workers their parent (main account) sync workers tag too, such that they will be included at the query for sync status of their parent account.

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Sunik Kupfer 2023-10-06 12:51:02 +02:00 committed by Ricki Hirner
parent 6db1473e00
commit 6bbdcb332f
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
4 changed files with 67 additions and 38 deletions

View file

@ -19,10 +19,11 @@ import java.util.concurrent.TimeUnit
/**
* Handles scheduled sync requests.
*
* Enqueues immediate [SyncWorker] syncs at the appropriate moment. This will prevent the actual
* sync code from running twice simultaneously (for manual and scheduled sync).
* Enqueues immediate [SyncWorker] syncs at the appropriate moment.
*
* For each account there will be multiple dedicated workers running for each authority.
* The different periodic sync workers each carry a unique work name composed of the account and
* authority which they are responsible for. For each account there will be multiple dedicated periodic
* sync workers for each authority. See [PeriodicSyncWorker.workerName] for more information.
*/
@HiltWorker
class PeriodicSyncWorker @AssistedInject constructor(
@ -31,21 +32,22 @@ class PeriodicSyncWorker @AssistedInject constructor(
) : Worker(appContext, workerParams) {
companion object {
private const val WORKER_TAG = "periodic-sync"
// Worker input parameters
internal const val ARG_ACCOUNT_NAME = "accountName"
internal const val ARG_ACCOUNT_TYPE = "accountType"
internal const val ARG_AUTHORITY = "authority"
/**
* Name of this worker.
* Used to distinguish between other work processes. A worker names are unique. There can
* never be two running workers with the same name.
* Unique work name of this worker. Can also be used as tag.
*
* 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 "sync $authority ${account.type}/${account.name}"
*/
fun workerName(account: Account, authority: String): String =
"$WORKER_TAG $authority ${account.type}/${account.name}"
"periodic-sync $authority ${account.type}/${account.name}"
/**
* Activate scheduled synchronization of an account with a specific authority.
@ -69,7 +71,6 @@ class PeriodicSyncWorker @AssistedInject constructor(
NetworkType.CONNECTED
).build()
val workRequest = PeriodicWorkRequestBuilder<PeriodicSyncWorker>(interval, TimeUnit.SECONDS)
.addTag(WORKER_TAG)
.setInputData(arguments)
.setConstraints(constraints)
.build()
@ -104,7 +105,7 @@ class PeriodicSyncWorker @AssistedInject constructor(
WorkManager.getInstance(context)
.getWorkInfos(
WorkQuery.Builder
.fromUniqueWorkNames(listOf(workerName(account, authority)))
.fromTags(listOf(workerName(account, authority)))
.addStates(listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING))
.build()
).get()

View file

@ -15,6 +15,7 @@ import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -89,22 +90,26 @@ object SyncUtils {
}
/**
* Returns a list of all available sync authorities for main accounts (!= address book accounts):
* Returns a list of all available sync authorities:
*
* 1. address books authority (not [ContactsContract.AUTHORITY], but the one which manages address book accounts)
* 1. calendar authority
* 1. tasks authority (if available)
* 2. contacts authority (only if [withContacts] is *true* - mostly we don't want it included)
* 3. address books authority
* 4. tasks authority/ies (if available, when tasks managing app(s) installed)
*
* Checking the availability of authorities may be relatively expensive, so the
* result should be cached for the current operation.
*
* @param withContacts whether to add contacts authority
* @return list of available sync authorities for main accounts
*/
fun syncAuthorities(context: Context): List<String> {
fun syncAuthorities(context: Context, withContacts: Boolean = false): List<String> {
val result = mutableListOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY
CalendarContract.AUTHORITY,
context.getString(R.string.address_books_authority)
)
if (withContacts)
result.add(ContactsContract.AUTHORITY)
TaskUtils.currentProvider(context)?.let { taskProvider ->
result += taskProvider.authority
}

View file

@ -41,6 +41,7 @@ import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.ConnectionUtils.internetAvailable
import at.bitfire.davdroid.network.ConnectionUtils.wifiAvailable
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
@ -55,7 +56,21 @@ import java.util.concurrent.TimeUnit
import java.util.logging.Level
/**
* Handles immediate sync requests, status queries and cancellation for one or multiple authorities
* Handles immediate sync requests and cancellations of accounts and respective content authorities,
* by creating appropriate workers.
*
* The different sync workers each carry a unique work name composed of the account and authority they
* are syncing. See [SyncWorker.workerName] for more information.
*
* By enqueuing this worker ([SyncWorker.enqueue]) a sync will be started immediately (as soon as
* possible). Currently, there are three scenarios starting a sync:
* 1) *manual sync*: User presses an in-app sync button and enqueues this worker directly.
* 2) *periodic sync*: User defines time interval to sync in app settings. The [PeriodicSyncWorker] runs
* in the background and enqueues this worker when due.
* 3) *content-triggered sync*: User changes a calendar event, task or contact, or presses a sync
* button in one of the responsible apps. The [SyncAdapterService] is notified of this and enqueues
* this worker.
*
*/
@HiltWorker
class SyncWorker @AssistedInject constructor(
@ -79,9 +94,6 @@ class SyncWorker @AssistedInject constructor(
const val RESYNC = 1
const val FULL_RESYNC = 2
// This SyncWorker's tag
const val TAG_SYNC = "sync"
/**
* How often this work will be retried to run after soft (network) errors.
*
@ -90,11 +102,20 @@ class SyncWorker @AssistedInject constructor(
internal const val MAX_RUN_ATTEMPTS = 5
/**
* Name of this worker.
* Used to distinguish between other work processes. There must only ever be one worker with the exact same name.
* Unique work name of this worker. Can also be used as tag.
*
* Mainly used to query [WorkManager] for work state (by unique work name or tag).
*
* *NOTE:* SyncWorkers for address book accounts bear the unique worker name of their parent
* account (main account) as tag. This makes it easier to query the overall sync status of a
* main account.
*
* @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 "sync $authority ${account.type}/${account.name}"
*/
fun workerName(account: Account, authority: String) =
"$TAG_SYNC $authority ${account.type}/${account.name}"
"sync $authority ${account.type}/${account.name}"
/**
* Requests immediate synchronization of an account with all applicable
@ -143,7 +164,6 @@ class SyncWorker @AssistedInject constructor(
.setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection
.build()
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag(TAG_SYNC)
.setInputData(argumentsBuilder.build())
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.setBackoffCriteria(
@ -152,11 +172,18 @@ class SyncWorker @AssistedInject constructor(
TimeUnit.MILLISECONDS
)
.setConstraints(constraints)
.apply {
// If this is a sub sync worker (address book sync), add the main account tag as well
if (account.type == context.getString(R.string.account_type_address_book)) {
val mainAccount = LocalAddressBook.mainAccount(context, account)
addTag(workerName(mainAccount, authority))
}
}
.build()
// enqueue and start syncing
val name = workerName(account, authority)
Logger.log.log(Level.INFO, "Enqueueing unique worker: $name")
Logger.log.log(Level.INFO, "Enqueueing unique worker: $name, with tags: ${workRequest.tags}")
WorkManager.getInstance(context).enqueueUniqueWork(
name,
ExistingWorkPolicy.KEEP, // If sync is already running, just continue.
@ -191,10 +218,9 @@ class SyncWorker @AssistedInject constructor(
authorities: List<String>? = null
): LiveData<Boolean> {
val workQuery = WorkQuery.Builder
.fromTags(listOf(TAG_SYNC))
.addStates(workStates)
.fromStates(workStates)
if (account != null && authorities != null)
workQuery.addUniqueWorkNames(
workQuery.addTags(
authorities.map { authority -> workerName(account, authority) }
)
return WorkManager.getInstance(context)

View file

@ -40,7 +40,6 @@ import androidx.work.WorkQuery
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AccountListBinding
import at.bitfire.davdroid.databinding.AccountListItemBinding
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.syncadapter.SyncUtils.syncAuthorities
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.ui.account.AccountActivity
@ -261,7 +260,7 @@ class AccountListFragment: Fragment() {
val accountsWithInfo = sortedAccounts.map { account ->
AccountInfo(
account,
SyncStatus.fromAccount(context, account, syncAuthorities)
SyncStatus.fromAccount(context, account)
)
}
value = accountsWithInfo
@ -269,7 +268,6 @@ class AccountListFragment: Fragment() {
}
private val accountManager = AccountManager.get(application)!!
private val syncAuthorities by lazy { SyncUtils.syncAuthorities(application) }
init {
// watch accounts
@ -297,17 +295,16 @@ class AccountListFragment: Fragment() {
* sub-accounts (address book accounts).
*
* @param account account to check
* @param authorities sync authorities to check (usually taken from [syncAuthorities])
*
* @return sync status of the given account
*/
fun fromAccount(context: Context, account: Account, authorities: List<String>): SyncStatus {
val workerNames = authorities.map { authority ->
fun fromAccount(context: Context, account: Account): SyncStatus {
// Add contacts authority, so sync status of address-book-accounts is also checked
val workerNames = syncAuthorities(context, true).map { authority ->
SyncWorker.workerName(account, authority)
}
val workQuery = WorkQuery.Builder
.fromTags(listOf(SyncWorker.TAG_SYNC))
.addUniqueWorkNames(workerNames)
.fromTags(workerNames)
.addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
.build()