mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-28 08:41:30 -05:00
Compare commits
20 Commits
reuse-http
...
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 {
|
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_SETTINGS_VERSION = "version"
|
||||||
|
|
||||||
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
|
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import at.bitfire.davdroid.settings.AccountSettings
|
|||||||
interface AccountSettingsMigration {
|
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].**
|
* **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.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SyncResult
|
import android.content.SyncResult
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
@@ -59,6 +60,7 @@ class SyncAdapterImpl @Inject constructor(
|
|||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
private val syncConditionsFactory: SyncConditions.Factory,
|
private val syncConditionsFactory: SyncConditions.Factory,
|
||||||
|
private val syncFrameworkIntegration: SyncFrameworkIntegration,
|
||||||
private val syncWorkerManager: SyncWorkerManager
|
private val syncWorkerManager: SyncWorkerManager
|
||||||
): AbstractThreadedSyncAdapter(
|
): AbstractThreadedSyncAdapter(
|
||||||
/* context = */ context,
|
/* context = */ context,
|
||||||
@@ -117,11 +119,11 @@ class SyncAdapterImpl @Inject constructor(
|
|||||||
// Android 14+ does not handle pending sync state correctly.
|
// Android 14+ does not handle pending sync state correctly.
|
||||||
// As a defensive workaround, we can cancel specifically this still pending sync only
|
// As a defensive workaround, we can cancel specifically this still pending sync only
|
||||||
// See: https://github.com/bitfireAT/davx5-ose/issues/1458
|
// See: https://github.com/bitfireAT/davx5-ose/issues/1458
|
||||||
// if (Build.VERSION.SDK_INT >= 34) {
|
if (Build.VERSION.SDK_INT >= 34) {
|
||||||
// logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " +
|
logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " +
|
||||||
// "account=$accountOrAddressBookAccount authority=$authority upload=$upload")
|
"account=$accountOrAddressBookAccount authority=$authority upload=$upload")
|
||||||
// syncFrameworkIntegration.cancelSync(accountOrAddressBookAccount, authority, extras)
|
syncFrameworkIntegration.cancelSync(accountOrAddressBookAccount, authority, extras)
|
||||||
// }
|
}
|
||||||
|
|
||||||
/* Because we are not allowed to observe worker state on a background thread, we can not
|
/* 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
|
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+.
|
* Cancels the sync request in the Sync Adapter Framework by sync request. This
|
||||||
* This is a workaround for the bug that the sync framework does not handle pending syncs correctly
|
* is the defensive approach canceling only one specific sync request with matching
|
||||||
* on Android 14+ (API level 34+).
|
* sync extras.
|
||||||
*
|
|
||||||
* See: https://github.com/bitfireAT/davx5-ose/issues/1458
|
|
||||||
*
|
*
|
||||||
* @param account The account for which the sync request should be canceled.
|
* @param account The account for which the sync request should be canceled.
|
||||||
* @param authority The authority 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)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun isSyncPending(account: Account, dataTypes: Iterable<SyncDataType>): Flow<Boolean> {
|
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
|
// Determine the pending state for each data type of the account as separate flows
|
||||||
val pendingStateFlows: List<Flow<Boolean>> = dataTypes.mapNotNull { dataType ->
|
val pendingStateFlows: List<Flow<Boolean>> = dataTypes.mapNotNull { dataType ->
|
||||||
// Map datatype to authority
|
// Map datatype to authority
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
package at.bitfire.davdroid.ui
|
package at.bitfire.davdroid.ui
|
||||||
|
|
||||||
import android.accounts.Account
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
@@ -13,6 +15,7 @@ import android.net.ConnectivityManager
|
|||||||
import android.net.Network
|
import android.net.Network
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
|
import android.os.Build
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.provider.CalendarContract
|
import android.provider.CalendarContract
|
||||||
import android.provider.ContactsContract
|
import android.provider.ContactsContract
|
||||||
@@ -22,6 +25,7 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.WorkQuery
|
import androidx.work.WorkQuery
|
||||||
|
import at.bitfire.davdroid.R
|
||||||
import at.bitfire.davdroid.db.AppDatabase
|
import at.bitfire.davdroid.db.AppDatabase
|
||||||
import at.bitfire.davdroid.repository.AccountRepository
|
import at.bitfire.davdroid.repository.AccountRepository
|
||||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||||
@@ -296,4 +300,34 @@ class AccountsModel @AssistedInject constructor(
|
|||||||
false
|
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.AccountCircle
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.BatterySaver
|
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.DataSaverOn
|
||||||
import androidx.compose.material.icons.filled.Menu
|
import androidx.compose.material.icons.filled.Menu
|
||||||
import androidx.compose.material.icons.filled.NotificationsOff
|
import androidx.compose.material.icons.filled.NotificationsOff
|
||||||
@@ -111,6 +112,7 @@ fun AccountsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
AccountsScreen(
|
AccountsScreen(
|
||||||
|
cancelSyncAdapterSyncs = { model.cancelSyncAdapterSyncs() },
|
||||||
accountsDrawerHandler = accountsDrawerHandler,
|
accountsDrawerHandler = accountsDrawerHandler,
|
||||||
accounts = accounts,
|
accounts = accounts,
|
||||||
showSyncAll = showSyncAll,
|
showSyncAll = showSyncAll,
|
||||||
@@ -131,6 +133,7 @@ fun AccountsScreen(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AccountsScreen(
|
fun AccountsScreen(
|
||||||
|
cancelSyncAdapterSyncs: () -> Unit,
|
||||||
accountsDrawerHandler: AccountsDrawerHandler,
|
accountsDrawerHandler: AccountsDrawerHandler,
|
||||||
accounts: List<AccountsModel.AccountInfo>,
|
accounts: List<AccountsModel.AccountInfo>,
|
||||||
showSyncAll: Boolean = true,
|
showSyncAll: Boolean = true,
|
||||||
@@ -228,6 +231,17 @@ fun AccountsScreen(
|
|||||||
contentDescription = stringResource(R.string.accounts_sync_all)
|
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) }
|
snackbarHost = { SnackbarHost(snackbarHostState) }
|
||||||
@@ -321,6 +335,7 @@ fun AccountsScreen(
|
|||||||
@Preview
|
@Preview
|
||||||
fun AccountsScreen_Preview_Empty() {
|
fun AccountsScreen_Preview_Empty() {
|
||||||
AccountsScreen(
|
AccountsScreen(
|
||||||
|
cancelSyncAdapterSyncs = {},
|
||||||
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
||||||
@Composable
|
@Composable
|
||||||
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
||||||
@@ -337,6 +352,7 @@ fun AccountsScreen_Preview_Empty() {
|
|||||||
@Preview
|
@Preview
|
||||||
fun AccountsScreen_Preview_OneAccount() {
|
fun AccountsScreen_Preview_OneAccount() {
|
||||||
AccountsScreen(
|
AccountsScreen(
|
||||||
|
cancelSyncAdapterSyncs = {},
|
||||||
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
accountsDrawerHandler = object: AccountsDrawerHandler() {
|
||||||
@Composable
|
@Composable
|
||||||
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
override fun MenuEntries(snackbarHostState: SnackbarHostState) {
|
||||||
|
|||||||
Reference in New Issue
Block a user