Check sync workers properly (closes bitfireAT/davx5#217) (bitfireAT/davx5#220)

* check sync workers properly

* Move utils around

* Merge live data, instead of observing in view, to recalculate account list.

* Update LiveData with same value

* Remove unused context property.

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Sunik Kupfer 2023-02-28 09:52:44 +01:00 committed by Ricki Hirner
parent fe7a928064
commit 9fee782968
No known key found for this signature in database
GPG key ID: 79A019FCAAEDD3AA
11 changed files with 165 additions and 116 deletions

View file

@ -12,11 +12,11 @@ import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Response
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.resource.LocalResource
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.DavUtils
import okhttp3.HttpUrl
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody

View file

@ -14,14 +14,14 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
import android.provider.ContactsContract.Groups
import android.provider.ContactsContract.RawContacts
import android.util.Base64
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.SyncState
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.syncadapter.SyncUtils.removePeriodicSyncs
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.vcard4android.*
import java.io.ByteArrayOutputStream
import java.util.*
@ -287,7 +287,7 @@ open class LocalAddressBook(
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY))
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
SyncUtils.removePeriodicSyncs(account, ContactsContract.AUTHORITY)
removePeriodicSyncs(account, ContactsContract.AUTHORITY)
}

View file

@ -23,7 +23,6 @@ import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.util.closeCompat
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Credentials
@ -33,6 +32,7 @@ import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.closeCompat
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.AndroidEvent
import at.bitfire.ical4android.TaskProvider

View file

@ -13,7 +13,6 @@ import android.content.SyncResult
import android.os.Build
import android.os.Bundle
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger

View file

@ -0,0 +1,63 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.provider.ContactsContract
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import at.bitfire.davdroid.resource.LocalAddressBook
enum class SyncStatus {
ACTIVE, PENDING, IDLE;
companion object {
/**
* Returns the sync status of a given account. Checks the account itself and possible
* sub-accounts (address book accounts).
*
* @param authorities sync authorities to check (usually taken from [syncAuthorities])
*
* @return sync status of the given account
*/
fun fromAccount(context: Context, authorities: Iterable<String>, account: Account): SyncStatus {
// check sync framework syncs are active or pending
if (authorities.any { ContentResolver.isSyncActive(account, it) })
return SyncStatus.ACTIVE
val addrBookAccounts = LocalAddressBook.findAll(context, null, account).map { it.account }
if (addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) })
return SyncStatus.ACTIVE
if (authorities.any { ContentResolver.isSyncPending(account, it) } ||
addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) })
return SyncStatus.PENDING
// Also check SyncWorkers
val workerNames = authorities.map { authority ->
SyncWorker.workerName(account, authority)
}
val workQuery = WorkQuery.Builder
.fromUniqueWorkNames(workerNames)
.addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
.build()
val workInfos = WorkManager.getInstance(context).getWorkInfos(workQuery).get()
when {
workInfos.any { workInfo ->
workInfo.state == WorkInfo.State.RUNNING
} -> return SyncStatus.ACTIVE
workInfos.any { workInfo ->
workInfo.state == WorkInfo.State.ENQUEUED
} -> return SyncStatus.PENDING
}
// None active or pending? Then we're idle ..
return SyncStatus.IDLE
}
}
}

View file

@ -14,11 +14,11 @@ import android.content.pm.PackageManager
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.os.Build
import android.provider.CalendarContract
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Service
@ -29,12 +29,16 @@ import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
/**
* Utility methods related to synchronization management (authorities, workers etc.)
*/
object SyncUtils {
@EntryPoint
@ -89,6 +93,29 @@ object SyncUtils {
ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras)
}
/**
* Returns a list of all available sync authorities for main accounts (!= address book accounts):
*
* 1. address books authority (not [ContactsContract.AUTHORITY], but the one which manages address book accounts)
* 1. calendar authority
* 1. tasks authority (if available)
*
* Checking the availability of authorities may be relatively expensive, so the
* result should be cached for the current operation.
*
* @return list of available sync authorities for main accounts
*/
fun syncAuthorities(context: Context): List<String> {
val result = mutableListOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY
)
TaskUtils.currentProvider(context)?.let { taskProvider ->
result += taskProvider.authority
}
return result
}
// task sync utils

View file

@ -15,12 +15,12 @@ import android.provider.ContactsContract
import androidx.concurrent.futures.CallbackToFutureAdapter
import androidx.core.app.NotificationCompat
import androidx.hilt.work.HiltWorker
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.work.*
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.util.LiveDataUtils
import at.bitfire.davdroid.util.closeCompat
import at.bitfire.ical4android.TaskProvider
@ -55,7 +55,7 @@ class SyncWorker @AssistedInject constructor(
* @param account account to sync
*/
fun requestSync(context: Context, account: Account) {
for (authority in DavUtils.syncAuthorities(context))
for (authority in SyncUtils.syncAuthorities(context))
requestSync(context, account, authority)
}
@ -96,7 +96,7 @@ class SyncWorker @AssistedInject constructor(
* @param authorities type of sync work
* @return boolean *true* if at least one worker with matching state was found; *false* otherwise
*/
fun isSomeWorkerInState(context: Context, workState: WorkInfo.State, account: Account, authorities: List<String>) =
fun existsForAccount(context: Context, workState: WorkInfo.State, account: Account, authorities: List<String>) =
LiveDataUtils.liveDataLogicOr(
authorities.map { authority -> isWorkerInState(context, workState, account, authority) }
)
@ -107,6 +107,21 @@ class SyncWorker @AssistedInject constructor(
}
/**
* Finds out whether SyncWorkers with given statuses exist
*
* @param statuses statuses to check
* @return whether SyncWorkers matching the statuses were found
*/
fun existsWithStatuses(context: Context, statuses: List<WorkInfo.State>): LiveData<Boolean> {
val workQuery = WorkQuery.Builder
.fromStates(statuses)
.build()
return Transformations.map(
WorkManager.getInstance(context).getWorkInfosLiveData(workQuery)
) { it.isNotEmpty() }
}
}

View file

@ -12,7 +12,6 @@ import android.content.SyncResult
import android.os.Build
import android.os.Bundle
import at.bitfire.davdroid.HttpClient
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger

View file

@ -10,27 +10,33 @@ import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.app.Activity
import android.app.Application
import android.content.*
import android.content.ContentResolver
import android.content.Intent
import android.content.SyncStatusObserver
import android.content.pm.PackageManager
import android.net.*
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.*
import androidx.annotation.AnyThread
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.bitfire.davdroid.util.DavUtils
import at.bitfire.davdroid.util.DavUtils.SyncStatus
import androidx.work.WorkInfo
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.AccountListBinding
import at.bitfire.davdroid.databinding.AccountListItemBinding
import at.bitfire.davdroid.syncadapter.SyncStatus
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.ui.account.AccountActivity
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
@ -108,6 +114,7 @@ class AccountListFragment: Fragment() {
}
}
// Accounts adapter
val accountAdapter = AccountAdapter(requireActivity())
binding.list.apply {
layoutManager = LinearLayoutManager(requireActivity())
@ -222,9 +229,39 @@ class AccountListFragment: Fragment() {
val storageLow = warnings.storageLow
// Accounts
val accounts = MutableLiveData<List<AccountInfo>>()
private val accountsUpdated = MutableLiveData<Boolean>()
private val syncFrameworkStatusChanged = MutableLiveData<Boolean>()
private val syncWorkersActive = SyncWorker.existsWithStatuses(
application.applicationContext, listOf(WorkInfo.State.RUNNING))
val accounts = object : MediatorLiveData<List<AccountInfo>>() {
init {
addSource(accountsUpdated) { recalculate() }
addSource(syncFrameworkStatusChanged) { recalculate() }
addSource(syncWorkersActive) { recalculate() }
}
fun recalculate() {
val context = getApplication<Application>()
val collator = Collator.getInstance()
val sortedAccounts = accountManager
.getAccountsByType(context.getString(R.string.account_type))
.sortedArrayWith { a, b ->
collator.compare(a.name, b.name)
}
val accountsWithInfo = sortedAccounts.map { account ->
AccountInfo(
account,
SyncStatus.fromAccount(context, syncAuthorities, account)
)
}
value = accountsWithInfo
}
}
private val accountManager = AccountManager.get(application)!!
private val syncAuthorities by lazy { DavUtils.syncAuthorities(application) }
private val syncAuthorities by lazy { SyncUtils.syncAuthorities(application) }
init {
// watch accounts
@ -237,30 +274,14 @@ class AccountListFragment: Fragment() {
)
}
@AnyThread
override fun onAccountsUpdated(newAccounts: Array<out Account>) {
reloadAccounts()
accountsUpdated.postValue(true)
}
@AnyThread
override fun onStatusChanged(which: Int) {
reloadAccounts()
}
private fun reloadAccounts() {
val context = getApplication<Application>()
val collator = Collator.getInstance()
val sortedAccounts = accountManager
.getAccountsByType(context.getString(R.string.account_type))
.sortedArrayWith { a, b ->
collator.compare(a.name, b.name)
}
val accountsWithInfo = sortedAccounts.map { account ->
AccountInfo(
account,
DavUtils.accountSyncStatus(context, syncAuthorities, account)
)
}
accounts.postValue(accountsWithInfo)
syncFrameworkStatusChanged.postValue(true)
}
override fun onCleared() {

View file

@ -316,11 +316,11 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
listOf(context.getString(R.string.address_books_authority), ContactsContract.AUTHORITY)
else
listOf(CalendarContract.AUTHORITY, taskProvider?.authority).filterNotNull()
private val isSyncWorkerRunning = SyncWorker.isSomeWorkerInState(context,
private val isSyncWorkerRunning = SyncWorker.existsForAccount(context,
WorkInfo.State.RUNNING,
accountModel.account,
authorities)
private val isSyncWorkerEnqueued = SyncWorker.isSomeWorkerInState(context,
private val isSyncWorkerEnqueued = SyncWorker.existsForAccount(context,
WorkInfo.State.ENQUEUED,
accountModel.account,
authorities)

View file

@ -4,21 +4,12 @@
package at.bitfire.davdroid.util
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.net.ConnectivityManager
import android.os.Build
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.core.content.getSystemService
import androidx.work.WorkInfo
import at.bitfire.davdroid.Android10Resolver
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.SyncWorker
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.MediaType.Companion.toMediaType
@ -27,14 +18,10 @@ import java.net.InetAddress
import java.util.*
/**
* Some WebDAV and related network utility methods
*/
* Some WebDAV and HTTP network utility methods.
*/
object DavUtils {
enum class SyncStatus {
ACTIVE, PENDING, IDLE
}
val DNS_QUAD9 = InetAddress.getByAddress(byteArrayOf(9,9,9,9))
const val MIME_TYPE_ACCEPT_ALL = "*/*"
@ -51,7 +38,6 @@ object DavUtils {
return String.format(Locale.ROOT, "#%06X%02X", color, alpha)
}
fun lastSegmentOfUrl(url: HttpUrl): String {
// the list returned by HttpUrl.pathSegments() is unmodifiable, so we have to create a copy
val segments = LinkedList(url.pathSegments)
@ -60,7 +46,6 @@ object DavUtils {
return segments.firstOrNull { it.isNotEmpty() } ?: "/"
}
fun prepareLookup(context: Context, lookup: Lookup) {
if (Build.VERSION.SDK_INT >= 29) {
/* Since Android 10, there's a native DnsResolver API that allows to send SRV queries without
@ -164,66 +149,6 @@ object DavUtils {
}
/**
* Returns the sync status of a given account. Checks the account itself and possible
* sub-accounts (address book accounts).
*
* @param authorities sync authorities to check (usually taken from [syncAuthorities])
*
* @return sync status of the given account
*/
fun accountSyncStatus(context: Context, authorities: Iterable<String>, account: Account): SyncStatus {
// check active syncs
if (authorities.any { ContentResolver.isSyncActive(account, it) })
return SyncStatus.ACTIVE
val addrBookAccounts = LocalAddressBook.findAll(context, null, account).map { it.account }
if (addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) })
return SyncStatus.ACTIVE
// check pending syncs
if (authorities.any { ContentResolver.isSyncPending(account, it) } ||
addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) })
return SyncStatus.PENDING
// Also check SyncWorkers
val pending = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.ENQUEUED, account, authorities.toList()).value
if (pending != null && pending == true)
return SyncStatus.PENDING
val running = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.RUNNING, account, authorities.toList()).value
if (running != null && running == true)
return SyncStatus.ACTIVE
return SyncStatus.IDLE
}
/**
* Returns a list of all available sync authorities for main accounts (!= address book accounts):
*
* 1. address books authority (not [ContactsContract.AUTHORITY], but the one which manages address book accounts)
* 1. calendar authority
* 1. tasks authority (if available)
*
* Checking the availability of authorities may be relatively expensive, so the
* result should be cached for the current operation.
*
* @return list of available sync authorities for main accounts
*/
fun syncAuthorities(context: Context): List<String> {
val result = mutableListOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY
)
TaskUtils.currentProvider(context)?.let { taskProvider ->
result += taskProvider.authority
}
return result
}
// extension methods
/**