Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit a0932388 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

Make "sync pending" work in AccountsScreen and AccountScreen (#1615)



* Add sync pending check for Android 14+

* Update sync pending UI logic to use selected authorities only

* Fix isSyncPending not handling multiple dataTypes

* Extract the accounts flow map to boolean flow logic

* Rename method

* Enhance kdoc

* Pass only one authority for pending check

* Update kdoc

* Update kdoc

* Update kdoc

* Fix whitespace

* Rename authority method to currentAuthority

* Update kdoc

---------

Co-authored-by: default avatarSunik Kupfer <kupfer@bitfire.at>
parent 293daf1e
Loading
Loading
Loading
Loading
+30 −9
Original line number Diff line number Diff line
@@ -4,11 +4,13 @@

package at.bitfire.davdroid.sync

import android.content.Context
import android.provider.CalendarContract
import android.provider.ContactsContract
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 {
@@ -23,19 +25,38 @@ enum class SyncDataType {
        fun tasksAppManager(): TasksAppManager
    }


    /**
     * Returns authorities which exist for this sync data type. Used on [TASKS] the method
     * may return an empty list if there are no tasks providers (installed tasks apps).
     *
     * @return list of authorities matching this data type
     */
    fun possibleAuthorities(): List<String> =
        when (this) {
            CONTACTS -> listOf(
                ContactsContract.AUTHORITY
            )
            EVENTS -> listOf(
                CalendarContract.AUTHORITY
            )
            TASKS ->
                TaskProvider.ProviderName.entries.map { it.authority }
            CONTACTS -> listOf(ContactsContract.AUTHORITY)
            EVENTS -> listOf(CalendarContract.AUTHORITY)
            TASKS -> TaskProvider.ProviderName.entries.map { it.authority }
        }

    /**
     * Returns the authority corresponding to this datatype.
     * When more than one tasks provider exists (tasks apps installed) the authority for the active
     * tasks provider (user selected tasks app) is returned.
     *
     * @param context android context used to determine the active/selected tasks provider
     * @return the authority matching this data type or *null* for [TASKS] if no tasks app is installed
     */
    fun currentAuthority(context: Context): String? =
        when (this) {
            CONTACTS -> ContactsContract.AUTHORITY
            EVENTS -> CalendarContract.AUTHORITY
            TASKS -> EntryPointAccessors.fromApplication<SyncDataTypeEntryPoint>(context)
                .tasksAppManager()
                .currentProvider()
                ?.authority
        }


    companion object {

        fun fromAuthority(authority: String): SyncDataType {
+67 −37
Original line number Diff line number Diff line
@@ -6,17 +6,19 @@ package at.bitfire.davdroid.sync.adapter

import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.content.SyncRequest
import android.os.Build
import android.os.Bundle
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.sync.SyncDataType
import dagger.Lazy
import dagger.hilt.android.qualifiers.ApplicationContext
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.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
@@ -31,6 +33,7 @@ import javax.inject.Inject
 * Sync requests from the Sync Adapter Framework are handled by [SyncAdapterService].
 */
class SyncFrameworkIntegration @Inject constructor(
    @ApplicationContext private val context: Context,
    private val localAddressBookStore: Lazy<LocalAddressBookStore>,
    private val logger: Logger
) {
@@ -178,55 +181,82 @@ class SyncFrameworkIntegration @Inject constructor(
    /**
     * Observe whether any of the given data types is currently pending for sync.
     *
     * Note: On Android 14+ finished syncs stay by default pending. This is why we
     * explicitly cancel the active sync in [SyncAdapterImpl] for Android 14+. Doing
     * so allows us to have a reliable "pending" flag again, which is used in this method.
     *
     * @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() }
    fun isSyncPending(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> {
        // Determine the pending state for each data type of the account as separate flows
        val pendingStateFlows: List<Flow<Boolean>> = dataTypes.mapNotNull { dataType ->
            // Map datatype to authority
            dataType.currentAuthority(context)?.let { authority ->
                // If checking contacts, we need to check all address book accounts instead of the single main account
                val accountsFlow: Flow<List<Account>> = when (dataType) {
                    SyncDataType.CONTACTS -> localAddressBookStore.get().getAddressBookAccountsFlow(account)
                    else -> flowOf(listOf(account))
                }

            // Use address book accounts if needed
            val accountsFlow = if (dataTypes.contains(SyncDataType.CONTACTS))
                localAddressBookStore.get().getAddressBookAccountsFlow(account)
            else
                flowOf(listOf(account))
                // Return the pending state flow for accounts with this authority
                anyPendingSyncFlow(accountsFlow, authority)
            }
        }

        // Combine the different per data type pending state flows into one
        return combine(pendingStateFlows) { pendingStates ->
            pendingStates.any { pending -> pending }
        }.distinctUntilChanged()
    }

            // Observe sync pending state for the given accounts and authorities
            accountsFlow.flatMapLatest { accounts ->
    /**
     * Maps the given accounts flow to a simple boolean flow telling us whether any of the accounts
     * has a pending sync for given authority.
     *
     * @param accountsFlow accounts to check sync status for
     * @param authority authority to check sync status for
     *
     * @return returns flow which emits *true* if any of the accounts has a sync pending for
     * the given authority and *false* otherwise
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    private fun anyPendingSyncFlow(
        accountsFlow: Flow<List<Account>>,
        authority: String
    ): Flow<Boolean> = accountsFlow.flatMapLatest { accounts ->
        // Observe sync pending state for the given accounts and data types
        callbackFlow {
            // Observe sync pending state
            val listener = ContentResolver.addStatusChangeListener(
                ContentResolver.SYNC_OBSERVER_TYPE_PENDING
            ) {
                        trySend(anyPendingSync(accounts, authorities))
                trySend(anyPendingSync(accounts, authority))
            }

            // Emit initial value
                    trySend(anyPendingSync(accounts, authorities))
            trySend(anyPendingSync(accounts, authority))

            // Clean up listener on close
            awaitClose { ContentResolver.removeStatusChangeListener(listener) }
        }
            }.distinctUntilChanged()
    }

    /**
     * Check if any of the given accounts and authorities have a sync pending.
     * Check if any of the given accounts have a sync pending for given authority.
     *
     * @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
     * @param authority authority to check sync status for
     *
     * @return *true* if any of the given accounts has a sync pending for given authority; *false* otherwise
     */
    private fun anyPendingSync(accounts: List<Account>, authorities: List<String>): Boolean =
    private fun anyPendingSync(accounts: List<Account>, authority: String): Boolean =
        accounts.any { account ->
            authorities.any { authority ->
                ContentResolver.isSyncPending(account, authority)
            ContentResolver.isSyncPending(account, authority).also { pending ->
                logger.finer("Sync pending($account, $authority) = $pending")
            }
        }