mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2025-12-23 23:17:50 -05:00
Update sync progress bar when pending in SAF (#1445)
* Provide flow to check if sync of account and authority is pending in sync framework Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Show if sync is pending in sync framework Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Show if sync is pending in sync framework Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Fix kdoc Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Cancel any pending SAF syncs on sync request Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Cancel sync adapter sync only after having enqueued our worker sync Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Improve accuracy by also checking isSyncActive Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Remove log statements Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Only query pending state Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Cancel sync adapter sync only on android 14 and 15 Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Cancel sync adapter sync with authority Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Cancel sync adapter sync by using sync request instead of canceling directly Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Include android 16 Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Include all versions after Android 14 Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Add test which documents wrong pending sync check behaviour Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Revert "Add test which documents wrong pending sync check behaviour" This reverts commit 8c538149ff2cb032d6355232c1736e103dcc9a18. * Drop Android 14+ always pending sync work around Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Differentiate better between enqueued and pending syncs Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Refactor sync pending check to use flow for address book accounts Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Always return false for sync pending state check on Android 14+ Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Refactor sync pending check to use flow for address book accounts Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Add comments Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Update comment Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Shorten variable name Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Update comments and variable name Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Remvoe obsolete call and add argument names as comments Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> * Remove sync active check from listener Signed-off-by: Sunik Kupfer <kupfer@bitfire.at> --------- Signed-off-by: Sunik Kupfer <kupfer@bitfire.at>
This commit is contained in:
@@ -6,6 +6,7 @@ package at.bitfire.davdroid.resource
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.accounts.OnAccountsUpdateListener
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
@@ -23,6 +24,9 @@ import at.bitfire.davdroid.sync.account.setAndVerifyUserData
|
||||
import at.bitfire.davdroid.util.DavUtils.lastSegment
|
||||
import com.google.common.base.CharMatcher
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
@@ -115,17 +119,10 @@ class LocalAddressBookStore @Inject constructor(
|
||||
return addressBookAccount
|
||||
}
|
||||
|
||||
override fun getAll(account: Account, provider: ContentProviderClient): List<LocalAddressBook> {
|
||||
val accountManager = AccountManager.get(context)
|
||||
return accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
|
||||
.filter { addressBookAccount ->
|
||||
accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_NAME) == account.name &&
|
||||
accountManager.getUserData(addressBookAccount, LocalAddressBook.USER_DATA_ACCOUNT_TYPE) == account.type
|
||||
}
|
||||
.map { addressBookAccount ->
|
||||
localAddressBookFactory.create(account, addressBookAccount, provider)
|
||||
}
|
||||
}
|
||||
override fun getAll(account: Account, provider: ContentProviderClient): List<LocalAddressBook> =
|
||||
getAddressBookAccounts(account).map { addressBookAccount ->
|
||||
localAddressBookFactory.create(account, addressBookAccount, provider)
|
||||
}
|
||||
|
||||
override fun update(provider: ContentProviderClient, localCollection: LocalAddressBook, fromCollection: Collection) {
|
||||
var currentAccount = localCollection.addressBookAccount
|
||||
@@ -197,6 +194,45 @@ class LocalAddressBookStore @Inject constructor(
|
||||
accountManager.removeAccountExplicitly(addressBookAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all address book accounts that belong to the given account.
|
||||
*
|
||||
* @param account Account which has the address books.
|
||||
* @return List of address book accounts.
|
||||
*/
|
||||
fun getAddressBookAccounts(account: Account): List<Account> =
|
||||
AccountManager.get(context).let { accountManager ->
|
||||
accountManager.getAccountsByType(context.getString(R.string.account_type_address_book))
|
||||
.filter { addressBookAccount ->
|
||||
account.name == accountManager.getUserData(
|
||||
addressBookAccount,
|
||||
LocalAddressBook.USER_DATA_ACCOUNT_NAME
|
||||
) && account.type == accountManager.getUserData(
|
||||
addressBookAccount,
|
||||
LocalAddressBook.USER_DATA_ACCOUNT_TYPE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all address book accounts that belong to the given account in a flow.
|
||||
*
|
||||
* @param account Account which has the address books.
|
||||
* @return List of address book accounts as flow.
|
||||
*/
|
||||
fun getAddressBookAccountsFlow(account: Account): Flow<List<Account>> = callbackFlow {
|
||||
val accountManager = AccountManager.get(context)
|
||||
val listener = OnAccountsUpdateListener { accounts ->
|
||||
trySend(getAddressBookAccounts(account))
|
||||
}
|
||||
accountManager.addOnAccountsUpdatedListener(
|
||||
/* listener = */ listener,
|
||||
/* handler = */ null,
|
||||
/* updateImmediately = */ true
|
||||
)
|
||||
awaitClose { accountManager.removeOnAccountsUpdatedListener(listener) }
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
||||
|
||||
@@ -143,13 +143,13 @@ abstract class SyncAdapterService: Service() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check sync conditions
|
||||
val accountSettings = try {
|
||||
accountSettingsFactory.create(account)
|
||||
} catch (e: InvalidAccountException) {
|
||||
logger.log(Level.WARNING, "Account doesn't exist anymore", e)
|
||||
return
|
||||
}
|
||||
|
||||
val syncConditions = syncConditionsFactory.create(accountSettings)
|
||||
// Should we run the sync at all?
|
||||
if (!syncConditions.wifiConditionsMet()) {
|
||||
@@ -160,6 +160,7 @@ abstract class SyncAdapterService: Service() {
|
||||
logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it")
|
||||
val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.fromAuthority(authority), fromUpload = upload)
|
||||
|
||||
|
||||
/* Because we are not allowed to observe worker state on a background thread, we can not
|
||||
use it to block the sync adapter. Instead we use a Flow to get notified when the sync
|
||||
has finished. */
|
||||
|
||||
@@ -6,8 +6,18 @@ package at.bitfire.davdroid.sync
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentResolver
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import androidx.annotation.WorkerThread
|
||||
import at.bitfire.davdroid.resource.LocalAddressBookStore
|
||||
import dagger.Lazy
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -19,6 +29,7 @@ import javax.inject.Inject
|
||||
* Sync requests from the Sync Adapter Framework are handled by [SyncAdapterService].
|
||||
*/
|
||||
class SyncFrameworkIntegration @Inject constructor(
|
||||
private val localAddressBookStore: Lazy<LocalAddressBookStore>,
|
||||
private val logger: Logger
|
||||
) {
|
||||
|
||||
@@ -134,4 +145,59 @@ class SyncFrameworkIntegration @Inject constructor(
|
||||
/* return */ !ContentResolver.getSyncAutomatically(account, authority)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe whether any of the given data types is currently pending for sync.
|
||||
*
|
||||
* @param account account to observe sync status for
|
||||
* @param dataTypes data types to observe sync status for
|
||||
* @return flow emitting true if any of the given data types has a sync pending, false otherwise
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun isSyncPending(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> =
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
// On Android 14+ pending sync checks always return true (bug), so we don't need to check.
|
||||
// See: https://github.com/bitfireAT/davx5-ose/issues/1458
|
||||
flowOf(false)
|
||||
} else {
|
||||
val authorities = dataTypes.flatMap { it.possibleAuthorities() }
|
||||
|
||||
// Use address book accounts if needed
|
||||
val accountsFlow = if (dataTypes.contains(SyncDataType.CONTACTS))
|
||||
localAddressBookStore.get().getAddressBookAccountsFlow(account)
|
||||
else
|
||||
flowOf(listOf(account))
|
||||
|
||||
// Observe sync pending state for the given accounts and authorities
|
||||
accountsFlow.flatMapLatest { accounts ->
|
||||
callbackFlow {
|
||||
// Observe sync pending state
|
||||
val listener = ContentResolver.addStatusChangeListener(
|
||||
ContentResolver.SYNC_OBSERVER_TYPE_PENDING
|
||||
) {
|
||||
trySend(anyPendingSync(accounts, authorities))
|
||||
}
|
||||
|
||||
// Emit initial value
|
||||
trySend(anyPendingSync(accounts, authorities))
|
||||
|
||||
// Clean up listener on close
|
||||
awaitClose { ContentResolver.removeStatusChangeListener(listener) }
|
||||
}
|
||||
}.distinctUntilChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the given accounts and authorities have a sync pending.
|
||||
*
|
||||
* @param accounts accounts to check sync status for
|
||||
* @param authorities authorities to check sync status for
|
||||
* @return true if any of the given accounts and authorities has a sync pending, false otherwise
|
||||
*/
|
||||
private fun anyPendingSync(accounts: List<Account>, authorities: List<String>): Boolean =
|
||||
accounts.any { account ->
|
||||
authorities.any { authority ->
|
||||
ContentResolver.isSyncPending(account, authority)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,7 @@ 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.SyncFrameworkIntegration
|
||||
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
|
||||
import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
|
||||
@@ -40,11 +41,14 @@ import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.text.Collator
|
||||
@@ -58,7 +62,8 @@ class AccountsModel @AssistedInject constructor(
|
||||
private val db: AppDatabase,
|
||||
introPageFactory: IntroPageFactory,
|
||||
private val logger: Logger,
|
||||
private val syncWorkerManager: SyncWorkerManager
|
||||
private val syncWorkerManager: SyncWorkerManager,
|
||||
private val syncFrameWork: SyncFrameworkIntegration
|
||||
): ViewModel() {
|
||||
|
||||
@AssistedFactory
|
||||
@@ -66,7 +71,6 @@ class AccountsModel @AssistedInject constructor(
|
||||
fun create(syncAccountsOnInit: Boolean): AccountsModel
|
||||
}
|
||||
|
||||
|
||||
// Accounts UI state
|
||||
|
||||
enum class FABStyle {
|
||||
@@ -92,7 +96,41 @@ class AccountsModel @AssistedInject constructor(
|
||||
private val workManager = WorkManager.getInstance(context)
|
||||
private val runningWorkers = workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING))
|
||||
|
||||
val accountInfos: Flow<List<AccountInfo>> = combine(accounts, runningWorkers) { accounts, workInfos ->
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val accountsSyncPending: Flow<List<Account>> =
|
||||
accounts.flatMapLatest { accounts ->
|
||||
if (accounts.isEmpty())
|
||||
flowOf(emptyList())
|
||||
else {
|
||||
// To create the Flow<List<Account?>> that emits the accounts with pending sync,
|
||||
val pendingSyncAccountsFlows: List<Flow<Account?>> =
|
||||
// for each existing account with unknown sync pending state ...
|
||||
accounts.map { account ->
|
||||
// ... create a Flow<Boolean> which emits the sync pending state
|
||||
syncFrameWork.isSyncPending(account, SyncDataType.entries)
|
||||
.map { hasPendingSync ->
|
||||
// ... and map this boolean answer back to its Account if it is pending, or null if not.
|
||||
if (hasPendingSync) account else null
|
||||
}
|
||||
}
|
||||
// Combine all account flows Flow<Account?> in the list into a single flow, emitting a list of
|
||||
// accounts with pending sync. The null values which we filter out are the non-pending accounts.
|
||||
// Now, whenever any account's pending state changes, the combined flow emits the updated list.
|
||||
combine(pendingSyncAccountsFlows) { combinedAccounts ->
|
||||
// combinedAccounts is an Array<Account?> of the most recently emitted values of the
|
||||
// pendingSyncCheckFlows, with one entry for every pendingSyncCheckFlow that is either
|
||||
// the account name (sync pending) or null (no sync pending).
|
||||
combinedAccounts.filterNotNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val accountInfos: Flow<List<AccountInfo>> = combine(
|
||||
accounts,
|
||||
runningWorkers,
|
||||
accountsSyncPending
|
||||
) { accounts, workInfos, accountsSyncPending ->
|
||||
val collator = Collator.getInstance()
|
||||
|
||||
accounts
|
||||
@@ -116,6 +154,9 @@ class AccountsModel @AssistedInject constructor(
|
||||
}
|
||||
} -> AccountProgress.Pending
|
||||
|
||||
account in accountsSyncPending
|
||||
-> AccountProgress.Pending
|
||||
|
||||
else -> AccountProgress.Idle
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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.SyncFrameworkIntegration
|
||||
import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker
|
||||
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
@@ -22,22 +23,32 @@ import javax.inject.Inject
|
||||
|
||||
class AccountProgressUseCase @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
private val syncFramework: SyncFrameworkIntegration,
|
||||
private val syncWorkerManager: SyncWorkerManager
|
||||
) {
|
||||
|
||||
/**
|
||||
* Returns the current sync state of the account.
|
||||
*/
|
||||
operator fun invoke(
|
||||
account: Account,
|
||||
serviceFlow: Flow<Service?>,
|
||||
dataTypes: Iterable<SyncDataType>
|
||||
): Flow<AccountProgress> {
|
||||
val serviceRefreshing = isServiceRefreshing(serviceFlow)
|
||||
val syncPending = isSyncPending(account, dataTypes)
|
||||
val syncEnqueued = isSyncEnqueued(account, dataTypes)
|
||||
val syncPending = syncFramework.isSyncPending(account, dataTypes)
|
||||
val syncRunning = isSyncRunning(account, dataTypes)
|
||||
|
||||
return combine(serviceRefreshing, syncPending, syncRunning) { refreshing, pending, syncing ->
|
||||
return combine(
|
||||
serviceRefreshing,
|
||||
syncEnqueued,
|
||||
syncPending,
|
||||
syncRunning
|
||||
) { refreshing, enqueued, pending, syncing ->
|
||||
when {
|
||||
refreshing || syncing -> AccountProgress.Active
|
||||
pending -> AccountProgress.Pending
|
||||
enqueued || pending -> AccountProgress.Pending
|
||||
else -> AccountProgress.Idle
|
||||
}
|
||||
}
|
||||
@@ -53,13 +64,13 @@ class AccountProgressUseCase @Inject constructor(
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun isSyncPending(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> =
|
||||
fun isSyncEnqueued(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> =
|
||||
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
|
||||
// we are only interested in enqueued OneTimeSyncWorkers because there's always an enqueued PeriodicSyncWorker
|
||||
OneTimeSyncWorker.workerName(account, authority)
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user