mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-17 03:07:53 -05:00
Compare commits
20 Commits
remove-tra
...
testing-sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f36e826d8 | ||
|
|
3737d69397 | ||
|
|
2dbd5c02b6 | ||
|
|
c12e9311f7 | ||
|
|
b663912feb | ||
|
|
3c484f253f | ||
|
|
de7f8d2964 | ||
|
|
a79a39c25d | ||
|
|
20675ed71b | ||
|
|
881588f8e8 | ||
|
|
0c31758880 | ||
|
|
9ffd59cd00 | ||
|
|
c40b2b38bc | ||
|
|
3025ea7491 | ||
|
|
b84a812d7a | ||
|
|
562afc5666 | ||
|
|
8992859b63 | ||
|
|
03013b5576 | ||
|
|
0028fc8722 | ||
|
|
1b4ebde896 |
@@ -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"
|
||||
|
||||
@@ -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].**
|
||||
*
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user