Compare commits

...

20 Commits

Author SHA1 Message Date
Sunik Kupfer
7f36e826d8 Cancel syncs for calendar, tasks, and contacts separately 2025-09-04 12:13:18 +02:00
Sunik Kupfer
3737d69397 Add FAB to cancel sync adapter syncs 2025-09-04 11:43:10 +02:00
Sunik Kupfer
2dbd5c02b6 Cancel by request and empty bundle 2025-09-04 11:09:23 +02:00
Sunik Kupfer
c12e9311f7 Stop always returning false for pending sync state of sync adapter framework 2025-09-04 10:58:40 +02:00
Sunik Kupfer
b663912feb Enable forever pending sync workaround by canceling sync adapter framework syncs on Android 14+ 2025-09-04 10:56:54 +02:00
Sunik Kupfer
3c484f253f Use cancelSync directly in migration 2025-09-04 10:53:50 +02:00
Sunik Kupfer
de7f8d2964 Cancel for all authorities and update kdoc 2025-09-04 10:53:50 +02:00
Sunik Kupfer
a79a39c25d Cancel only on Android 14+ 2025-09-04 10:53:50 +02:00
Sunik Kupfer
20675ed71b Update kdoc 2025-09-04 10:53:50 +02:00
Sunik Kupfer
881588f8e8 Don't infer authority from account type 2025-09-04 10:53:50 +02:00
Sunik Kupfer
0c31758880 Also cancel calendar syncs 2025-09-04 10:53:50 +02:00
Sunik Kupfer
9ffd59cd00 Updating log statement 2025-09-04 10:53:50 +02:00
Sunik Kupfer
c40b2b38bc Update kdoc 2025-09-04 10:53:50 +02:00
Sunik Kupfer
3025ea7491 Optimize imports 2025-09-04 10:53:50 +02:00
Sunik Kupfer
b84a812d7a Call cancelSync via integration 2025-09-04 10:53:50 +02:00
Sunik Kupfer
562afc5666 Add and update kdoc 2025-09-04 10:53:50 +02:00
Sunik Kupfer
8992859b63 Increase account settings current version 2025-09-04 10:53:50 +02:00
Sunik Kupfer
03013b5576 Add log statement 2025-09-04 10:53:50 +02:00
Sunik Kupfer
0028fc8722 Add application context annotation 2025-09-04 10:53:50 +02:00
Sunik Kupfer
1b4ebde896 Add AccountSettingsMigration21 to cancel pending address book syncs 2025-09-04 10:53:50 +02:00
7 changed files with 144 additions and 18 deletions

View File

@@ -354,7 +354,12 @@ class AccountSettings @AssistedInject constructor(
companion object {
const val CURRENT_VERSION = 20
/**
* Current (usually the newest) account settings version. It's used to
* determine whether a migration ([AccountSettingsMigration])
* should be performed.
*/
const val CURRENT_VERSION = 21
const val KEY_SETTINGS_VERSION = "version"
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"

View File

@@ -10,7 +10,8 @@ import at.bitfire.davdroid.settings.AccountSettings
interface AccountSettingsMigration {
/**
* Migrate the account settings from the old version to the new version.
* Migrate the account settings from the old version to the new version which
* is set in [AccountSettings.CURRENT_VERSION].
*
* **The new (target) version number is registered in the Hilt module as [Int] key of the multi-binding of [AccountSettings].**
*

View File

@@ -0,0 +1,76 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.settings.migration
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import at.bitfire.davdroid.R
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntKey
import dagger.multibindings.IntoMap
import java.util.logging.Logger
import javax.inject.Inject
/**
* On Android 14+ the pending sync state of the Sync Adapter Framework is not handled correctly.
* As a workaround we cancel incoming sync requests (clears pending flag) after enqueuing our own
* sync worker (work manager). With version 4.5.3 we started cancelling pending syncs for DAVx5
* accounts, but forgot to do that for address book accounts. With version 4.5.4 we also cancel
* those, but only when contact data of an address book has been edited.
*
* This migration cancels (once only) any possibly still wrongly pending address book and calendar
* (+tasks) account syncs.
*/
class AccountSettingsMigration21 @Inject constructor(
@ApplicationContext private val context: Context,
private val syncFrameworkIntegration: SyncFrameworkIntegration,
private val logger: Logger
): AccountSettingsMigration {
private val accountManager = AccountManager.get(context)
private val calendarAccountType = context.getString(R.string.account_type)
private val addressBookAccountType = context.getString(R.string.account_type_address_book)
override fun migrate(account: Account) {
if (Build.VERSION.SDK_INT >= 34) {
// Cancel any (after an update) possibly forever pending calendar (+tasks) account syncs
cancelSyncs(calendarAccountType, CalendarContract.AUTHORITY)
// Cancel any (after an update) possibly forever pending address book account syncs
cancelSyncs(addressBookAccountType, ContactsContract.AUTHORITY)
}
}
/**
* Cancels any (possibly forever pending) syncs for the accounts of given account type for all
* authorities.
*/
private fun cancelSyncs(accountType: String, authority: String) {
accountManager.getAccountsByType(accountType).forEach { account ->
logger.info("Android 14+: Canceling all (possibly forever pending) syncs for $account")
syncFrameworkIntegration.cancelSync(account, authority, Bundle())
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class AccountSettingsMigrationModule {
@Binds @IntoMap
@IntKey(21)
abstract fun provide(impl: AccountSettingsMigration21): AccountSettingsMigration
}
}

View File

@@ -11,6 +11,7 @@ import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.work.WorkInfo
@@ -59,6 +60,7 @@ class SyncAdapterImpl @Inject constructor(
@ApplicationContext context: Context,
private val logger: Logger,
private val syncConditionsFactory: SyncConditions.Factory,
private val syncFrameworkIntegration: SyncFrameworkIntegration,
private val syncWorkerManager: SyncWorkerManager
): AbstractThreadedSyncAdapter(
/* context = */ context,
@@ -117,11 +119,11 @@ class SyncAdapterImpl @Inject constructor(
// Android 14+ does not handle pending sync state correctly.
// As a defensive workaround, we can cancel specifically this still pending sync only
// See: https://github.com/bitfireAT/davx5-ose/issues/1458
// if (Build.VERSION.SDK_INT >= 34) {
// logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " +
// "account=$accountOrAddressBookAccount authority=$authority upload=$upload")
// syncFrameworkIntegration.cancelSync(accountOrAddressBookAccount, authority, extras)
// }
if (Build.VERSION.SDK_INT >= 34) {
logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " +
"account=$accountOrAddressBookAccount authority=$authority upload=$upload")
syncFrameworkIntegration.cancelSync(accountOrAddressBookAccount, authority, extras)
}
/* 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

View File

@@ -101,11 +101,9 @@ class SyncFrameworkIntegration @Inject constructor(
}
/**
* Cancels the sync request in the Sync Framework for Android 14+.
* This is a workaround for the bug that the sync framework does not handle pending syncs correctly
* on Android 14+ (API level 34+).
*
* See: https://github.com/bitfireAT/davx5-ose/issues/1458
* Cancels the sync request in the Sync Adapter Framework by sync request. This
* is the defensive approach canceling only one specific sync request with matching
* sync extras.
*
* @param account The account for which the sync request should be canceled.
* @param authority The authority for which the sync request should be canceled.
@@ -193,12 +191,6 @@ class SyncFrameworkIntegration @Inject constructor(
*/
@OptIn(ExperimentalCoroutinesApi::class)
fun isSyncPending(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> {
// Android 14+ does not handle pending sync state correctly.
// For now we simply always return false
// See also sync cancellation in [SyncAdapterImpl.onPerformSync]
if (Build.VERSION.SDK_INT >= 34)
return flowOf(false)
// 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

View File

@@ -5,6 +5,8 @@
package at.bitfire.davdroid.ui
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
@@ -13,6 +15,7 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.PowerManager
import android.provider.CalendarContract
import android.provider.ContactsContract
@@ -22,6 +25,7 @@ import androidx.lifecycle.ViewModel
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.repository.AccountRepository
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
@@ -296,4 +300,34 @@ class AccountsModel @AssistedInject constructor(
false
}
fun cancelSyncAdapterSyncs() {
if (Build.VERSION.SDK_INT >= 34) {
val calendarAccountType = context.getString(R.string.account_type)
val addressBookAccountType = context.getString(R.string.account_type_address_book)
// Cancel any (after an update) possibly forever pending calendar account syncs
cancelSyncs(calendarAccountType, SyncDataType.EVENTS.possibleAuthorities())
// Cancel any (after an update) possibly forever pending tasks account syncs
cancelSyncs(calendarAccountType, SyncDataType.TASKS.possibleAuthorities())
// Cancel any (after an update) possibly forever pending address book account syncs
cancelSyncs(addressBookAccountType, SyncDataType.CONTACTS.possibleAuthorities())
}
}
/**
* Cancels any (possibly forever pending) syncs for the accounts of given account type for all
* authorities.
*/
private fun cancelSyncs(accountType: String, authorities: List<String>) {
val accountManager = AccountManager.get(context)
accountManager.getAccountsByType(accountType).forEach { account ->
logger.info("Android 14+: Canceling all (possibly forever pending) syncs for $account")
for (authority in authorities)
ContentResolver.cancelSync(account, authority)
}
}
}
}

View File

@@ -23,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.BatterySaver
import androidx.compose.material.icons.filled.CancelScheduleSend
import androidx.compose.material.icons.filled.DataSaverOn
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.NotificationsOff
@@ -111,6 +112,7 @@ fun AccountsScreen(
}
AccountsScreen(
cancelSyncAdapterSyncs = { model.cancelSyncAdapterSyncs() },
accountsDrawerHandler = accountsDrawerHandler,
accounts = accounts,
showSyncAll = showSyncAll,
@@ -131,6 +133,7 @@ fun AccountsScreen(
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
@Composable
fun AccountsScreen(
cancelSyncAdapterSyncs: () -> Unit,
accountsDrawerHandler: AccountsDrawerHandler,
accounts: List<AccountsModel.AccountInfo>,
showSyncAll: Boolean = true,
@@ -228,6 +231,17 @@ fun AccountsScreen(
contentDescription = stringResource(R.string.accounts_sync_all)
)
}
FloatingActionButton(
onClick = cancelSyncAdapterSyncs,
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary,
modifier = Modifier.padding(top = 24.dp)
) {
Icon(
Icons.Default.CancelScheduleSend,
contentDescription = stringResource(R.string.accounts_sync_all)
)
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) }
@@ -321,6 +335,7 @@ fun AccountsScreen(
@Preview
fun AccountsScreen_Preview_Empty() {
AccountsScreen(
cancelSyncAdapterSyncs = {},
accountsDrawerHandler = object: AccountsDrawerHandler() {
@Composable
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
@@ -337,6 +352,7 @@ fun AccountsScreen_Preview_Empty() {
@Preview
fun AccountsScreen_Preview_OneAccount() {
AccountsScreen(
cancelSyncAdapterSyncs = {},
accountsDrawerHandler = object: AccountsDrawerHandler() {
@Composable
override fun MenuEntries(snackbarHostState: SnackbarHostState) {