mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-02-23 18:36:17 -05:00
Compare commits
4 Commits
v4.4.3.2-o
...
debug-buil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46e8c4522b | ||
|
|
d00353ba9c | ||
|
|
dc0d4f371a | ||
|
|
3d198f5454 |
@@ -82,6 +82,9 @@ android {
|
||||
|
||||
signingConfig = signingConfigs.findByName("bitfire")
|
||||
}
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
6
app/src/debug/res/values/colors.xml
Normal file
6
app/src/debug/res/values/colors.xml
Normal 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>
|
||||
4
app/src/debug/res/values/strings.xml
Normal file
4
app/src/debug/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">DAVx⁵ Debug</string>
|
||||
</resources>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
62
app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt
Normal file
62
app/src/main/kotlin/at/bitfire/davdroid/sync/SyncResult.kt
Normal 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
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user