Compare commits

..

4 Commits

Author SHA1 Message Date
Arnau Mora
46e8c4522b Updated name, package and color
Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
2024-10-25 17:47:23 +02:00
Sunik Kupfer
d00353ba9c Replace android sync framework result class with our own (#1094)
* Use our own SyncResult data class

* Minor comment changes

---------

Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2024-10-25 14:13:43 +02:00
Ricki Hirner
dc0d4f371a More compatible processing of multiget responses (#1099)
* Ignore multi-get responses without calendar/contact data

* Add comment
2024-10-25 12:44:54 +02:00
Arnau Mora
3d198f5454 LocalAddressBook: rename account to addressbookAccount (#1095)
* Upgraded vcard4android

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Replaced all usages of addressBookAccount

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>

* Minor changes

---------

Signed-off-by: Arnau Mora Gras <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
2024-10-22 14:07:53 +02:00
22 changed files with 177 additions and 88 deletions

View File

@@ -82,6 +82,9 @@ android {
signingConfig = signingConfigs.findByName("bitfire")
}
getByName("debug") {
applicationIdSuffix = ".debug"
}
}
lint {

View File

@@ -19,6 +19,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import ezvcard.property.Telephone
import java.util.LinkedList
import javax.inject.Inject
import org.junit.After
import org.junit.AfterClass
import org.junit.Assert.assertEquals
@@ -29,8 +31,6 @@ import org.junit.BeforeClass
import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import java.util.LinkedList
import javax.inject.Inject
@HiltAndroidTest
class LocalAddressBookTest {
@@ -84,7 +84,7 @@ class LocalAddressBookTest {
// rename address book
val newName = "New Name"
addressBook.renameAccount(newName)
assertEquals(Account(newName, LocalTestAddressBook.ACCOUNT.type), addressBook.account)
assertEquals(Account(newName, LocalTestAddressBook.ACCOUNT.type), addressBook.addressBookAccount)
// check whether contact is still here (including data rows) and not dirty
val result = addressBook.findContactById(id)
@@ -113,7 +113,7 @@ class LocalAddressBookTest {
// rename address book
val newName = "New Name"
addressBook.renameAccount(newName)
assertEquals(Account(newName, LocalTestAddressBook.ACCOUNT.type), addressBook.account)
assertEquals(Account(newName, LocalTestAddressBook.ACCOUNT.type), addressBook.addressBookAccount)
// check whether group is still here and not dirty
val result = addressBook.findGroupById(id)

View File

@@ -25,12 +25,12 @@ import java.util.logging.Logger
class LocalTestAddressBook @AssistedInject constructor(
@Assisted provider: ContentProviderClient,
@Assisted override val groupMethod: GroupMethod,
@ApplicationContext context: Context,
accountSettingsFactory: AccountSettings.Factory,
collectionRepository: DavCollectionRepository,
@ApplicationContext context: Context,
logger: Logger,
serviceRepository: DavServiceRepository
): LocalAddressBook(ACCOUNT, provider, context, accountSettingsFactory, collectionRepository, logger, serviceRepository) {
): LocalAddressBook(ACCOUNT, provider, accountSettingsFactory, collectionRepository, context, logger, serviceRepository) {
@AssistedFactory
interface Factory {

View File

@@ -6,7 +6,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.Context
import android.content.SyncResult
import android.util.Log
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.work.HiltWorkerFactory

View File

@@ -6,7 +6,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.SyncResult
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.sync.account.TestAccountAuthenticator
import dagger.assisted.Assisted

View File

@@ -5,7 +5,6 @@
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.SyncResult
import at.bitfire.dav4jvm.DavCollection
import at.bitfire.dav4jvm.MultiResponseCallback
import at.bitfire.dav4jvm.Response

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#E07C25</color>
<color name="primaryLightColor">#E5A371</color>
<color name="primaryDarkColor">#7C3E07</color>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">DAVx⁵ Debug</string>
</resources>

View File

@@ -50,19 +50,22 @@ import java.util.logging.Logger
* account and there is no such thing as "address books". So, DAVx5 creates a "DAVx5
* address book" account for every CardDAV address book.
*
* @param addressBookAccount Address book account (not: DAVx5 account) storing the actual android address book
* @param _addressBookAccount Address book account (not: DAVx5 account) storing the actual Android
* contacts. This is the initial value of [addressBookAccount]. However when the address book is renamed,
* the new name will only be available in [addressBookAccount], so usually that one should be used.
*
* @param provider Content provider needed to access and modify the address book
*/
@OpenForTesting
open class LocalAddressBook @AssistedInject constructor(
@Assisted addressBookAccount: Account,
@Assisted _addressBookAccount: Account,
@Assisted provider: ContentProviderClient,
@ApplicationContext val context: Context,
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
@ApplicationContext val context: Context,
private val logger: Logger,
private val serviceRepository: DavServiceRepository
): AndroidAddressBook<LocalContact, LocalGroup>(addressBookAccount, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
): AndroidAddressBook<LocalContact, LocalGroup>(_addressBookAccount, provider, LocalContact.Factory, LocalGroup.Factory), LocalCollection<LocalAddress> {
@AssistedFactory
interface Factory {
@@ -71,10 +74,10 @@ open class LocalAddressBook @AssistedInject constructor(
override val tag: String
get() = "contacts-${account.name}"
get() = "contacts-${addressBookAccount.name}"
override val title
get() = account.name
get() = addressBookAccount.name
/**
* Whether contact groups ([LocalGroup]) are included in query results
@@ -85,16 +88,16 @@ open class LocalAddressBook @AssistedInject constructor(
*/
open val groupMethod: GroupMethod by lazy {
val manager = AccountManager.get(context)
val associatedAccount = manager.getUserData(/* address book account */ account, USER_DATA_COLLECTION_ID)?.toLongOrNull()?.let { collectionId ->
val account = manager.getUserData(addressBookAccount, USER_DATA_COLLECTION_ID)?.toLongOrNull()?.let { collectionId ->
collectionRepository.get(collectionId)?.let { collection ->
serviceRepository.get(collection.serviceId)?.let { service ->
Account(service.accountName, context.getString(R.string.account_type))
}
}
}
if (associatedAccount == null)
if (account == null)
throw IllegalArgumentException("Collection of address book account $addressBookAccount does not have an account")
val accountSettings = accountSettingsFactory.create(associatedAccount)
val accountSettings = accountSettingsFactory.create(account)
accountSettings.getGroupMethod()
}
private val includeGroups
@@ -102,13 +105,13 @@ open class LocalAddressBook @AssistedInject constructor(
@Deprecated("Local collection should be identified by ID, not by URL")
override var collectionUrl: String
get() = AccountManager.get(context).getUserData(account, USER_DATA_URL)
get() = AccountManager.get(context).getUserData(addressBookAccount, USER_DATA_URL)
?: throw IllegalStateException("Address book has no URL")
set(url) = AccountManager.get(context).setAndVerifyUserData(account, USER_DATA_URL, url)
set(url) = AccountManager.get(context).setAndVerifyUserData(addressBookAccount, USER_DATA_URL, url)
override var readOnly: Boolean
get() = AccountManager.get(context).getUserData(account, USER_DATA_READ_ONLY) != null
set(readOnly) = AccountManager.get(context).setAndVerifyUserData(account, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
get() = AccountManager.get(context).getUserData(addressBookAccount, USER_DATA_READ_ONLY) != null
set(readOnly) = AccountManager.get(context).setAndVerifyUserData(addressBookAccount, USER_DATA_READ_ONLY, if (readOnly) "1" else null)
override var lastSyncState: SyncState?
get() = syncState?.let { SyncState.fromString(String(it)) }
@@ -153,18 +156,18 @@ open class LocalAddressBook @AssistedInject constructor(
* `null`: don't change the existing value
*/
fun update(info: Collection, forceReadOnly: Boolean? = null) {
logger.log(Level.INFO, "Updating local address book $account with collection $info")
logger.log(Level.INFO, "Updating local address book $addressBookAccount with collection $info")
val accountManager = AccountManager.get(context)
// Update the account name
val newAccountName = accountName(context, info)
if (account.name != newAccountName)
if (addressBookAccount.name != newAccountName)
// rename, move contacts/groups and update [AndroidAddressBook.]account
renameAccount(newAccountName)
// Update the account user data
accountManager.setAndVerifyUserData(account, USER_DATA_COLLECTION_ID, info.id.toString())
accountManager.setAndVerifyUserData(account, USER_DATA_URL, info.url.toString())
accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_COLLECTION_ID, info.id.toString())
accountManager.setAndVerifyUserData(addressBookAccount, USER_DATA_URL, info.url.toString())
// Update force read only
if (forceReadOnly != null) {
@@ -200,18 +203,18 @@ open class LocalAddressBook @AssistedInject constructor(
* Renames an address book account and moves the contacts and groups (without making them dirty).
* Does not keep user data of the old account, so these have to be set again.
*
* On success, [account] will be updated to the new account name.
* On success, [addressBookAccount] will be updated to the new account name.
*
* _Note:_ Previously, we had used [AccountManager.renameAccount], but then the contacts can't be moved because there's never
* a moment when both accounts are available.
*
* @param newName the new account name (account type is taken from [account])
* @param newName the new account name (account type is taken from [addressBookAccount])
*
* @return whether the account was renamed successfully
*/
@VisibleForTesting
internal fun renameAccount(newName: String): Boolean {
val oldAccount = account
val oldAccount = addressBookAccount
logger.info("Renaming address book from \"${oldAccount.name}\" to \"$newName\"")
// create new account
@@ -234,7 +237,7 @@ open class LocalAddressBook @AssistedInject constructor(
batch.commit()
// update AndroidAddressBook.account
account = newAccount
addressBookAccount = newAccount
// delete old account
val accountManager = AccountManager.get(context)
@@ -245,7 +248,7 @@ open class LocalAddressBook @AssistedInject constructor(
override fun deleteCollection(): Boolean {
val accountManager = AccountManager.get(context)
return accountManager.removeAccountExplicitly(account)
return accountManager.removeAccountExplicitly(addressBookAccount)
}
@@ -259,15 +262,15 @@ open class LocalAddressBook @AssistedInject constructor(
*/
fun updateSyncFrameworkSettings() {
// Enable sync-ability
if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) != 1)
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
if (ContentResolver.getIsSyncable(addressBookAccount, ContactsContract.AUTHORITY) != 1)
ContentResolver.setIsSyncable(addressBookAccount, ContactsContract.AUTHORITY, 1)
// Enable content trigger
if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY))
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
if (!ContentResolver.getSyncAutomatically(addressBookAccount, ContactsContract.AUTHORITY))
ContentResolver.setSyncAutomatically(addressBookAccount, ContactsContract.AUTHORITY, true)
// Remove periodic syncs (setSyncAutomatically also creates periodic syncs, which we don't want)
for (periodicSync in ContentResolver.getPeriodicSyncs(account, ContactsContract.AUTHORITY))
for (periodicSync in ContentResolver.getPeriodicSyncs(addressBookAccount, ContactsContract.AUTHORITY))
ContentResolver.removePeriodicSync(periodicSync.account, periodicSync.authority, periodicSync.extras)
}

View File

@@ -6,7 +6,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.SyncResult
import android.provider.ContactsContract
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
@@ -74,7 +73,7 @@ class AddressBookSyncer @AssistedInject constructor(
}
override fun syncCollection(provider: ContentProviderClient, localCollection: LocalAddressBook, remoteCollection: Collection) {
logger.info("Synchronizing address book: ${localCollection.account.name}")
logger.info("Synchronizing address book: ${localCollection.addressBookAccount.name}")
syncAddressBook(
account = account,
addressBook = localCollection,
@@ -110,7 +109,7 @@ class AddressBookSyncer @AssistedInject constructor(
// handle group method change
val groupMethod = accountSettings.getGroupMethod().name
accountSettings.accountManager.getUserData(addressBook.account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
accountSettings.accountManager.getUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
if (previousGroupMethod != groupMethod) {
logger.info("Group method changed, deleting all local contacts/groups")
@@ -122,7 +121,7 @@ class AddressBookSyncer @AssistedInject constructor(
addressBook.syncState = null
}
}
accountSettings.accountManager.setAndVerifyUserData(addressBook.account, PREVIOUS_GROUP_METHOD, groupMethod)
accountSettings.accountManager.setAndVerifyUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD, groupMethod)
val syncManager = contactsSyncManagerFactory.contactsSyncManager(account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook, collection)
syncManager.performSync()

View File

@@ -5,7 +5,6 @@
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.SyncResult
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.MultiResponseCallback
@@ -196,20 +195,35 @@ class CalendarSyncManager @AssistedInject constructor(
logger.info("Downloading ${bunch.size} iCalendars: $bunch")
SyncException.wrapWithRemoteResource(collection.url) {
davCollection.multiget(bunch) { response, _ ->
SyncException.wrapWithRemoteResource(response.href) wrapResponse@ {
/*
* Real-world servers may return:
*
* - unrelated resources
* - the collection itself
* - the requested resources, but with a different collection URL (for instance, `/cal/1.ics` instead of `/shared-cal/1.ics`).
*
* So we:
*
* - ignore unsuccessful responses,
* - ignore responses without requested calendar data (should also ignore collections and hopefully unrelated resources), and
* - take the last segment of the href as the file name and assume that it's in the requested collection.
*/
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Received non-successful multiget response for ${response.href}")
return@wrapResponse
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val iCal = response[CalendarData::class.java]?.iCalendar
if (iCal == null) {
logger.warning("Ignoring multi-get response without calendar-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
?: throw DavException("Received multi-get response without ETag")
val scheduleTag = response[ScheduleTag::class.java]?.scheduleTag
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without calendar data")
processVEvent(
response.href.lastSegment,
eTag,

View File

@@ -7,7 +7,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.SyncResult
import android.provider.CalendarContract
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service

View File

@@ -7,7 +7,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.SyncResult
import android.os.Build
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavAddressBook
@@ -320,24 +319,27 @@ class ContactsSyncManager @AssistedInject constructor(
}
}
davCollection.multiget(bunch, contentType, version) { response, _ ->
// See CalendarSyncManager for more information about the multi-get response
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Received non-successful multiget response for ${response.href}")
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val card = response[AddressData::class.java]?.card
if (card == null) {
logger.warning("Ignoring multi-get response without address-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
?: throw DavException("Received multi-get response without ETag")
var isJCard = hasJCard // assume that server has sent what we have requested (we ask for jCard only when the server advertises it)
response[GetContentType::class.java]?.type?.let { type ->
isJCard = type.sameTypeAs(DavUtils.MEDIA_TYPE_JCARD)
}
val addressData = response[AddressData::class.java]
val card = addressData?.card
?: throw DavException("Received multi-get response without address data")
processCard(
response.href.lastSegment,
eTag,

View File

@@ -5,7 +5,6 @@
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.SyncResult
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.MultiResponseCallback
@@ -123,19 +122,22 @@ class JtxSyncManager @AssistedInject constructor(
// multiple iCalendars, use calendar-multi-get
SyncException.wrapWithRemoteResource(collection.url) {
davCollection.multiget(bunch) { response, _ ->
// See CalendarSyncManager for more information about the multi-get response
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Received non-successful multiget response for ${response.href}")
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val iCal = response[CalendarData::class.java]?.iCalendar
if (iCal == null) {
logger.warning("Ignoring multi-get response without calendar-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")
processICalObject(response.href.lastSegment, eTag, StringReader(iCal))
}
}

View File

@@ -8,7 +8,6 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.SyncResult
import android.os.Build
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
@@ -54,7 +53,7 @@ class JtxSyncer @AssistedInject constructor(
TaskProvider.checkVersion(context, TaskProvider.ProviderName.JtxBoard)
} catch (e: TaskProvider.ProviderTooOldException) {
tasksAppManager.get().notifyProviderTooOld(e)
syncResult.databaseError = true
syncResult.contentProviderError = true
return false // Don't sync
}

View File

@@ -9,7 +9,6 @@ import android.app.PendingIntent
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.net.Uri
import android.os.DeadObjectException
import android.os.RemoteException
@@ -333,7 +332,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
logger.log(Level.WARNING, "Got 503 Service unavailable, trying again later", e)
// determine when to retry
syncResult.delayUntil = getDelayUntil(e.retryAfter).epochSecond
syncResult.stats.numIoExceptions++ // Indicate a soft error occurred
syncResult.stats.numServiceUnavailableExceptions++ // Indicate a soft error occurred
}
// all others
@@ -788,19 +787,19 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
is HttpException, is DavException -> {
logger.log(Level.SEVERE, "HTTP/DAV exception", e)
message = context.getString(R.string.sync_error_http_dav, e.localizedMessage)
syncResult.stats.numParseExceptions++ // numIoExceptions would indicate a soft error
syncResult.stats.numHttpExceptions++
}
is CalendarStorageException, is ContactsStorageException, is RemoteException -> {
logger.log(Level.SEVERE, "Couldn't access local storage", e)
message = context.getString(R.string.sync_error_local_storage, e.localizedMessage)
syncResult.databaseError = true
syncResult.localStorageError = true
}
else -> {
logger.log(Level.SEVERE, "Unclassified sync error", e)
message = e.localizedMessage ?: e::class.java.simpleName
syncResult.stats.numParseExceptions++
syncResult.stats.numUnclassifiedErrors++
}
}

View File

@@ -0,0 +1,62 @@
package at.bitfire.davdroid.sync
/**
* This class represents the results of a sync operation from [Syncer].
*
* Used by [at.bitfire.davdroid.sync.worker.BaseSyncWorker] to determine whether or not there will be retries etc.
*/
data class SyncResult(
var contentProviderError: Boolean = false,
var localStorageError: Boolean = false,
var delayUntil: Long = 0,
val stats: SyncStats = SyncStats()
) {
/**
* Whether a hard error occurred.
*/
fun hasHardError(): Boolean =
contentProviderError
|| localStorageError
|| stats.numAuthExceptions > 0
|| stats.numHttpExceptions > 0
|| stats.numUnclassifiedErrors > 0
/**
* Whether a soft error occurred.
*/
fun hasSoftError(): Boolean =
stats.numDeadObjectExceptions > 0
|| stats.numIoExceptions > 0
|| stats.numServiceUnavailableExceptions > 0
/**
* Whether a hard or a soft error occurred.
*/
fun hasError(): Boolean =
hasHardError() || hasSoftError()
/**
* Holds statistics about the sync operation. Used to determine retries. Also useful for
* debugging and customer support when logged.
*/
data class SyncStats(
// Stats
var numDeletes: Long = 0,
var numEntries: Long = 0,
var numInserts: Long = 0,
var numSkippedEntries: Long = 0,
var numUpdates: Long = 0,
// Hard errors
var numAuthExceptions: Long = 0,
var numHttpExceptions: Long = 0,
var numUnclassifiedErrors: Long = 0,
// Soft errors
var numDeadObjectExceptions: Long = 0,
var numIoExceptions: Long = 0,
var numServiceUnavailableExceptions: Long = 0
)
}

View File

@@ -7,7 +7,6 @@ package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.DeadObjectException
import androidx.annotation.VisibleForTesting
import at.bitfire.davdroid.InvalidAccountException
@@ -282,7 +281,7 @@ abstract class Syncer<CollectionType: LocalCollection<*>>(
- the content provider is not available at all, for instance because the respective
system app, like "calendar storage" is disabled */
logger.warning("Couldn't connect to content provider of authority $authority")
syncResult.stats.numParseExceptions++ // hard sync error
syncResult.contentProviderError = true
return // Don't continue without provider
}
@@ -297,14 +296,14 @@ abstract class Syncer<CollectionType: LocalCollection<*>>(
/* May happen when the remote process dies or (since Android 14) when IPC (for instance with the calendar provider)
is suddenly forbidden because our sync process was demoted from a "service process" to a "cached process". */
logger.log(Level.WARNING, "Received DeadObjectException, treating as soft error", e)
syncResult.stats.numIoExceptions++
syncResult.stats.numDeadObjectExceptions++
} catch (e: InvalidAccountException) {
logger.log(Level.WARNING, "Account was removed during synchronization", e)
} catch (e: Exception) {
logger.log(Level.SEVERE, "Couldn't sync $authority", e)
syncResult.stats.numParseExceptions++ // Hard sync error
syncResult.stats.numUnclassifiedErrors++ // Hard sync error
} finally {
if (httpClient.isInitialized())

View File

@@ -8,7 +8,6 @@ import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.SyncResult
import android.os.Build
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Service
@@ -52,7 +51,7 @@ class TaskSyncer @AssistedInject constructor(
TaskProvider.checkVersion(context, providerName)
} catch (e: TaskProvider.ProviderTooOldException) {
tasksAppManager.get().notifyProviderTooOld(e)
syncResult.databaseError = true
syncResult.contentProviderError = true
return false // Don't sync
}

View File

@@ -5,7 +5,6 @@
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.SyncResult
import android.text.format.Formatter
import at.bitfire.dav4jvm.DavCalendar
import at.bitfire.dav4jvm.MultiResponseCallback
@@ -123,18 +122,21 @@ class TasksSyncManager @AssistedInject constructor(
// multiple iCalendars, use calendar-multi-get
SyncException.wrapWithRemoteResource(collection.url) {
davCollection.multiget(bunch) { response, _ ->
// See CalendarSyncManager for more information about the multi-get response
SyncException.wrapWithRemoteResource(response.href) wrapResource@ {
if (!response.isSuccess()) {
logger.warning("Received non-successful multiget response for ${response.href}")
logger.warning("Ignoring non-successful multi-get response for ${response.href}")
return@wrapResource
}
val iCal = response[CalendarData::class.java]?.iCalendar
if (iCal == null) {
logger.warning("Ignoring multi-get response without calendar-data")
return@wrapResource
}
val eTag = response[GetETag::class.java]?.eTag
?: throw DavException("Received multi-get response without ETag")
val calendarData = response[CalendarData::class.java]
val iCal = calendarData?.iCalendar
?: throw DavException("Received multi-get response without task data")
?: throw DavException("Received multi-get response without ETag")
processVTodo(response.href.lastSegment, eTag, StringReader(iCal))
}

View File

@@ -7,7 +7,6 @@ package at.bitfire.davdroid.sync.worker
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Build
import android.provider.CalendarContract
import androidx.annotation.IntDef
@@ -27,20 +26,21 @@ import at.bitfire.davdroid.sync.AddressBookSyncer
import at.bitfire.davdroid.sync.CalendarSyncer
import at.bitfire.davdroid.sync.JtxSyncer
import at.bitfire.davdroid.sync.SyncConditions
import at.bitfire.davdroid.sync.SyncResult
import at.bitfire.davdroid.sync.SyncUtils
import at.bitfire.davdroid.sync.Syncer
import at.bitfire.davdroid.sync.TaskSyncer
import at.bitfire.davdroid.ui.NotificationRegistry
import at.bitfire.ical4android.TaskProvider
import java.util.Collections
import java.util.logging.Logger
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import java.util.Collections
import java.util.logging.Logger
import javax.inject.Inject
abstract class BaseSyncWorker(
context: Context,

View File

@@ -21,7 +21,7 @@ androidx-work = "2.9.1"
bitfire-cert4android = "f1cc9b9ca3"
bitfire-dav4jvm = "fbd95a5f5a"
bitfire-ical4android = "b75f33972a"
bitfire-vcard4android = "5439c1f63c"
bitfire-vcard4android = "13840ade05"
compose-accompanist = "0.36.0"
compose-bom = "2024.10.00"
dnsjava = "3.6.0"