Choose real or fake (for tests) SyncAdapter over DI (#1608)

* Choose real or fake (for tests) SyncAdapter over DI

* Minor changes

* Rename SyncAdapterServicesTest.kt to RealSyncAdapterTest.kt

* Group sync adapter / sync framework classes into new package

* Cache SyncAdapter in SyncAdapterServices

* Add documentation to SyncAdapter interface and rename RealSyncAdapterTest to SyncAdapterImplTest
This commit is contained in:
Ricki Hirner
2025-07-24 12:19:54 +02:00
committed by GitHub
parent 288583bfad
commit dab948730e
16 changed files with 341 additions and 264 deletions

View File

@@ -10,7 +10,6 @@ import android.os.Build
import android.os.Bundle
import androidx.test.runner.AndroidJUnitRunner
import at.bitfire.davdroid.di.TestCoroutineDispatchersModule
import at.bitfire.davdroid.sync.SyncAdapterService
import at.bitfire.davdroid.test.BuildConfig
import at.bitfire.synctools.log.LogcatHandler
import dagger.hilt.android.testing.HiltTestApplication
@@ -36,9 +35,6 @@ class HiltTestRunner : AndroidJUnitRunner() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
throw AssertionError("MockK requires Android P [https://mockk.io/ANDROID.html]")
// disable sync adapters
SyncAdapterService.syncActive.set(false)
// set main dispatcher for tests (especially runTest)
TestCoroutineDispatchersModule.initMainDispatcher()
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.di
import at.bitfire.davdroid.sync.FakeSyncAdapter
import at.bitfire.davdroid.sync.adapter.SyncAdapter
import at.bitfire.davdroid.sync.adapter.SyncAdapterImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
@Module
@TestInstallIn(components = [SingletonComponent::class], replaces = [SyncAdapterImpl.RealSyncAdapterModule::class])
abstract class FakeSyncAdapterModule {
@Binds
abstract fun provide(impl: FakeSyncAdapter): SyncAdapter
}

View File

@@ -10,7 +10,7 @@ import android.content.Context
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.vcard4android.GroupMethod
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory

View File

@@ -0,0 +1,51 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import android.os.IBinder
import at.bitfire.davdroid.sync.adapter.SyncAdapter
import dagger.hilt.android.qualifiers.ApplicationContext
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
class FakeSyncAdapter @Inject constructor(
@ApplicationContext context: Context,
private val logger: Logger
): AbstractThreadedSyncAdapter(context, true), SyncAdapter {
init {
logger.info("FakeSyncAdapter created")
}
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
logger.log(
Level.INFO,
"onPerformSync(account=$account, extras=$extras, authority=$authority, syncResult=$syncResult)",
extras.keySet().map { key -> "extras[$key] = ${extras[key]}" }
)
// fake 5 sec sync
try {
Thread.sleep(5000)
} catch (_: InterruptedException) {
logger.info("onPerformSync($account) cancelled")
}
logger.info("onPerformSync($account) finished")
}
// SyncAdapter implementation and Hilt module
override fun getBinder(): IBinder = syncAdapterBinder
}

View File

@@ -15,6 +15,7 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import at.bitfire.davdroid.TestUtils
import at.bitfire.davdroid.sync.account.TestAccount
import at.bitfire.davdroid.sync.adapter.SyncAdapterImpl
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.testing.BindValue
@@ -44,7 +45,7 @@ import javax.inject.Provider
import kotlin.coroutines.cancellation.CancellationException
@HiltAndroidTest
class SyncAdapterServicesTest {
class SyncAdapterImplTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@@ -56,7 +57,7 @@ class SyncAdapterServicesTest {
lateinit var context: Context
@Inject
lateinit var syncAdapterProvider: Provider<SyncAdapterService.SyncAdapter>
lateinit var syncAdapterImplProvider: Provider<SyncAdapterImpl>
@BindValue @MockK
lateinit var syncWorkerManager: SyncWorkerManager
@@ -90,7 +91,7 @@ class SyncAdapterServicesTest {
@Test
fun testSyncAdapter_onPerformSync_cancellation() = runTest {
val workManager = WorkManager.getInstance(context)
val syncAdapter = syncAdapterProvider.get()
val syncAdapter = syncAdapterImplProvider.get()
mockkObject(workManager) {
// don't actually create a worker
@@ -114,7 +115,7 @@ class SyncAdapterServicesTest {
@Test
fun testSyncAdapter_onPerformSync_returnsAfterTimeout() {
val workManager = WorkManager.getInstance(context)
val syncAdapter = syncAdapterProvider.get()
val syncAdapter = syncAdapterImplProvider.get()
mockkObject(workManager) {
// don't actually create a worker
@@ -135,7 +136,7 @@ class SyncAdapterServicesTest {
@Test
fun testSyncAdapter_onPerformSync_runsInTime() {
val workManager = WorkManager.getInstance(context)
val syncAdapter = syncAdapterProvider.get()
val syncAdapter = syncAdapterImplProvider.get()
mockkObject(workManager) {
// don't actually create a worker
@@ -154,4 +155,4 @@ class SyncAdapterServicesTest {
}
}
}
}

View File

@@ -186,7 +186,7 @@
android:resource="@xml/account_authenticator"/>
</service>
<service
android:name=".sync.CalendarsSyncAdapterService"
android:name=".sync.adapter.CalendarsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
@@ -197,7 +197,7 @@
android:resource="@xml/sync_calendars"/>
</service>
<service
android:name=".sync.JtxSyncAdapterService"
android:name=".sync.adapter.JtxSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
@@ -208,7 +208,7 @@
android:resource="@xml/sync_notes"/>
</service>
<service
android:name=".sync.OpenTasksSyncAdapterService"
android:name=".sync.adapter.OpenTasksSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
@@ -219,7 +219,7 @@
android:resource="@xml/sync_opentasks"/>
</service>
<service
android:name=".sync.TasksOrgSyncAdapterService"
android:name=".sync.adapter.TasksOrgSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
@@ -254,7 +254,7 @@
android:resource="@xml/account_authenticator_address_book"/>
</service>
<service
android:name=".sync.ContactsSyncAdapterService"
android:name=".sync.adapter.ContactsSyncAdapterService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>

View File

@@ -23,9 +23,9 @@ import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_READ_ON
import at.bitfire.davdroid.resource.workaround.ContactDirtyVerifier
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.account.SystemAccountUtils
import at.bitfire.davdroid.sync.account.setAndVerifyUserData
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.synctools.storage.BatchOperation
import at.bitfire.synctools.storage.ContactsBatchOperation
import at.bitfire.vcard4android.AndroidAddressBook

View File

@@ -11,6 +11,7 @@ import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import kotlinx.coroutines.runBlocking
import javax.inject.Inject

View File

@@ -1,240 +0,0 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
import android.accounts.Account
import android.accounts.AccountManager
import android.app.Service
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECTION_ID
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.account.InvalidAccountException
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.util.concurrent.atomic.AtomicBoolean
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
abstract class SyncAdapterService: Service() {
/**
* We don't use @AndroidEntryPoint / @Inject because it's unavoidable that instrumented tests sometimes accidentally / asynchronously
* create a [SyncAdapterService] instance before Hilt is initialized during the tests.
*/
@dagger.hilt.EntryPoint
@InstallIn(SingletonComponent::class)
interface EntryPoint {
fun syncAdapter(): SyncAdapter
}
override fun onBind(intent: Intent?): IBinder {
if (BuildConfig.DEBUG && !syncActive.get()) {
// only for debug builds/testing: syncActive flag
val logger = Logger.getLogger(SyncAdapterService::class.java.name)
logger.warning("SyncAdapterService.onBind() was called but syncActive = false. Returning fake sync adapter")
val fakeAdapter = object: AbstractThreadedSyncAdapter(this, true) {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val message = StringBuilder()
message.append("FakeSyncAdapter onPerformSync(account=$account, extras=$extras, authority=$authority, syncResult=$syncResult)")
for (key in extras.keySet())
message.append("\n\textras[$key] = ${extras[key]}")
logger.warning(message.toString())
// fake 5 sec sync
try {
Thread.sleep(5000)
} catch (_: InterruptedException) {
logger.warning("FakeSyncAdapter onPerformSync($account) cancelled")
}
logger.warning("FakeSyncAdapter onPerformSync($account) finished")
}
}
return fakeAdapter.syncAdapterBinder
}
// create sync adapter via Hilt
val entryPoint = EntryPointAccessors.fromApplication<EntryPoint>(this)
val syncAdapter = entryPoint.syncAdapter()
return syncAdapter.syncAdapterBinder
}
companion object {
/**
* Flag to indicate whether the sync adapter should be active. When it is `false`, synchronization will not be run
* (only intended for tests).
*/
val syncActive = AtomicBoolean(true)
}
/**
* Entry point for the Sync Adapter Framework.
*
* Handles incoming sync requests from the Sync Adapter Framework.
*
* Although we do not use the sync adapter for syncing anymore, we keep this sole
* adapter to provide exported services, which allow android system components and calendar,
* contacts or task apps to sync via DAVx5.
*
* All Sync Adapter Framework related interaction should happen inside [SyncFrameworkIntegration].
*/
class SyncAdapter @Inject constructor(
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
private val serviceRepository: DavServiceRepository,
@ApplicationContext context: Context,
private val logger: Logger,
private val syncConditionsFactory: SyncConditions.Factory,
private val syncFrameworkIntegration: SyncFrameworkIntegration,
private val syncWorkerManager: SyncWorkerManager
): AbstractThreadedSyncAdapter(
/* context = */ context,
/* autoInitialize = */ true // Sets isSyncable=1 when isSyncable=-1 and SYNC_EXTRAS_INITIALIZE is set.
// Doesn't matter for us because we have android:isAlwaysSyncable="true" for all sync adapters.
) {
/**
* Scope used to wait until the synchronization is finished. Will be cancelled when the sync framework
* requests cancellation.
*/
private val waitScope = CoroutineScope(Dispatchers.Default)
override fun onPerformSync(accountOrAddressBookAccount: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
// We have to pass this old SyncFramework extra for an Android 7 workaround
val upload = extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD)
logger.info("Sync request via sync framework for $accountOrAddressBookAccount $authority (upload=$upload)")
// If we should sync an address book account - find the account storing the settings
val account = if (accountOrAddressBookAccount.type == context.getString(R.string.account_type_address_book))
AccountManager.get(context)
.getUserData(accountOrAddressBookAccount, USER_DATA_COLLECTION_ID)
?.toLongOrNull()
?.let { collectionId ->
collectionRepository.get(collectionId)?.let { collection ->
serviceRepository.getBlocking(collection.serviceId)?.let { service ->
Account(service.accountName, context.getString(R.string.account_type))
}
}
}
else
accountOrAddressBookAccount
if (account == null) {
logger.warning("Address book account $accountOrAddressBookAccount doesn't have an associated collection")
return
}
// Check sync conditions
val accountSettings = try {
accountSettingsFactory.create(account)
} catch (e: InvalidAccountException) {
logger.log(Level.WARNING, "Account doesn't exist anymore", e)
return
}
val syncConditions = syncConditionsFactory.create(accountSettings)
// Should we run the sync at all?
if (!syncConditions.wifiConditionsMet()) {
logger.info("Sync conditions not met. Aborting sync framework initiated sync")
return
}
logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it")
val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.fromAuthority(authority), fromUpload = upload)
// 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=$account authority=$authority upload=$upload")
syncFrameworkIntegration.cancelSync(account, 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
has finished. */
val workManager = WorkManager.getInstance(context)
try {
val waitJob = waitScope.launch {
// wait for finished worker state
workManager.getWorkInfosForUniqueWorkFlow(workerName).collect { infoList ->
for (info in infoList)
if (info.state.isFinished) {
if (info.state == WorkInfo.State.FAILED) {
if (info.outputData.getBoolean(BaseSyncWorker.OUTPUT_TOO_MANY_RETRIES, false))
syncResult.tooManyRetries = true
else
syncResult.databaseError = true
}
cancel("$workerName has finished")
}
}
}
runBlocking {
withTimeout(10 * 60 * 1000) { // block max. 10 minutes
waitJob.join() // wait until worker has finished
}
}
} catch (_: CancellationException) {
// waiting for work was cancelled, either by timeout or because the worker has finished
logger.fine("Not waiting for OneTimeSyncWorker anymore.")
}
logger.log(Level.INFO, "Returning to sync framework.", syncResult)
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
logger.log(Level.WARNING, "Security exception for $account/$authority")
}
override fun onSyncCanceled() {
logger.info("Sync adapter requested cancellation won't cancel sync, but also won't block sync framework anymore")
// unblock sync framework
waitScope.cancel()
}
override fun onSyncCanceled(thread: Thread) = onSyncCanceled()
}
}
// exported sync adapter services; we need a separate class for each authority
class CalendarsSyncAdapterService: SyncAdapterService()
class ContactsSyncAdapterService: SyncAdapterService()
class JtxSyncAdapterService: SyncAdapterService()
class OpenTasksSyncAdapterService: SyncAdapterService()
class TasksOrgSyncAdapterService: SyncAdapterService()

View File

@@ -0,0 +1,19 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync.adapter
import android.os.IBinder
/**
* Interface for an Android sync adapter, as created by [SyncAdapterService].
*
* Sync adapters are bound services that communicate over IPC, so the only method is
* [getBinder], which returns the sync adapter binder.
*/
interface SyncAdapter {
fun getBinder(): IBinder
}

View File

@@ -0,0 +1,188 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync.adapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.AbstractThreadedSyncAdapter
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
import androidx.work.WorkManager
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavCollectionRepository
import at.bitfire.davdroid.repository.DavServiceRepository
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.SyncConditions
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.account.InvalidAccountException
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
/**
* Entry point for the Sync Adapter Framework.
*
* Handles incoming sync requests from the Sync Adapter Framework.
*
* Although we do not use the sync adapter for syncing anymore, we keep this sole
* adapter to provide exported services, which allow android system components and calendar,
* contacts or task apps to sync via DAVx5.
*
* All Sync Adapter Framework related interaction should happen inside [SyncFrameworkIntegration].
*/
class SyncAdapterImpl @Inject constructor(
private val accountSettingsFactory: AccountSettings.Factory,
private val collectionRepository: DavCollectionRepository,
private val serviceRepository: DavServiceRepository,
@ApplicationContext context: Context,
private val logger: Logger,
private val syncConditionsFactory: SyncConditions.Factory,
private val syncFrameworkIntegration: SyncFrameworkIntegration,
private val syncWorkerManager: SyncWorkerManager
): AbstractThreadedSyncAdapter(
/* context = */ context,
/* autoInitialize = */ true // Sets isSyncable=1 when isSyncable=-1 and SYNC_EXTRAS_INITIALIZE is set.
// Doesn't matter for us because we have android:isAlwaysSyncable="true" for all sync adapters.
), SyncAdapter {
/**
* Scope used to wait until the synchronization is finished. Will be cancelled when the sync framework
* requests cancellation.
*/
private val waitScope = CoroutineScope(Dispatchers.Default)
override fun onPerformSync(accountOrAddressBookAccount: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
// We have to pass this old SyncFramework extra for an Android 7 workaround
val upload = extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD)
logger.info("Sync request via sync framework for $accountOrAddressBookAccount $authority (upload=$upload)")
// If we should sync an address book account - find the account storing the settings
val account = if (accountOrAddressBookAccount.type == context.getString(R.string.account_type_address_book))
AccountManager.get(context)
.getUserData(accountOrAddressBookAccount, LocalAddressBook.USER_DATA_COLLECTION_ID)
?.toLongOrNull()
?.let { collectionId ->
collectionRepository.get(collectionId)?.let { collection ->
serviceRepository.getBlocking(collection.serviceId)?.let { service ->
Account(service.accountName, context.getString(R.string.account_type))
}
}
}
else
accountOrAddressBookAccount
if (account == null) {
logger.warning("Address book account $accountOrAddressBookAccount doesn't have an associated collection")
return
}
// Check sync conditions
val accountSettings = try {
accountSettingsFactory.create(account)
} catch (e: InvalidAccountException) {
logger.log(Level.WARNING, "Account doesn't exist anymore", e)
return
}
val syncConditions = syncConditionsFactory.create(accountSettings)
// Should we run the sync at all?
if (!syncConditions.wifiConditionsMet()) {
logger.info("Sync conditions not met. Aborting sync framework initiated sync")
return
}
logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it")
val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.Companion.fromAuthority(authority), fromUpload = upload)
// 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=$account authority=$authority upload=$upload")
syncFrameworkIntegration.cancelSync(account, 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
has finished. */
val workManager = WorkManager.getInstance(context)
try {
val waitJob = waitScope.launch {
// wait for finished worker state
workManager.getWorkInfosForUniqueWorkFlow(workerName).collect { infoList ->
for (info in infoList)
if (info.state.isFinished) {
if (info.state == WorkInfo.State.FAILED) {
if (info.outputData.getBoolean(BaseSyncWorker.OUTPUT_TOO_MANY_RETRIES, false))
syncResult.tooManyRetries = true
else
syncResult.databaseError = true
}
cancel("$workerName has finished")
}
}
}
runBlocking {
withTimeout(10 * 60 * 1000) { // block max. 10 minutes
waitJob.join() // wait until worker has finished
}
}
} catch (_: CancellationException) {
// waiting for work was cancelled, either by timeout or because the worker has finished
logger.fine("Not waiting for OneTimeSyncWorker anymore.")
}
logger.log(Level.INFO, "Returning to sync framework.", syncResult)
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
logger.log(Level.WARNING, "Security exception for $account/$authority")
}
override fun onSyncCanceled() {
logger.info("Sync adapter requested cancellation won't cancel sync, but also won't block sync framework anymore")
// unblock sync framework
waitScope.cancel()
}
override fun onSyncCanceled(thread: Thread) = onSyncCanceled()
// SyncAdapter implementation and Hilt module
override fun getBinder(): IBinder = syncAdapterBinder
@Module
@InstallIn(SingletonComponent::class)
abstract class RealSyncAdapterModule {
@Binds
abstract fun provide(impl: SyncAdapterImpl): SyncAdapter
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync.adapter
import android.app.Service
import android.content.Intent
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
abstract class SyncAdapterService: Service() {
/**
* We don't use @AndroidEntryPoint / @Inject because it's unavoidable that instrumented tests sometimes accidentally / asynchronously
* create a [SyncAdapterService] instance before Hilt is initialized by the HiltTestRunner.
*/
@EntryPoint
@InstallIn(SingletonComponent::class)
interface SyncAdapterServicesEntryPoint {
fun syncAdapter(): SyncAdapter
}
// create syncAdapter on demand and cache it
val syncAdapter by lazy {
val entryPoint = EntryPointAccessors.fromApplication<SyncAdapterServicesEntryPoint>(this)
entryPoint.syncAdapter()
}
override fun onBind(intent: Intent?) = syncAdapter.getBinder()
}
// exported sync adapter services; we need a separate class for each authority
class CalendarsSyncAdapterService: SyncAdapterService()
class ContactsSyncAdapterService: SyncAdapterService()
class JtxSyncAdapterService: SyncAdapterService()
class OpenTasksSyncAdapterService: SyncAdapterService()
class TasksOrgSyncAdapterService: SyncAdapterService()

View File

@@ -2,16 +2,16 @@
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
*/
package at.bitfire.davdroid.sync
package at.bitfire.davdroid.sync.adapter
import android.accounts.Account
import android.content.ContentResolver
import android.content.SyncRequest
import android.os.Build
import android.os.Bundle
import android.provider.CalendarContract
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.resource.LocalAddressBookStore
import at.bitfire.davdroid.sync.SyncDataType
import dagger.Lazy
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
@@ -132,7 +132,7 @@ class SyncFrameworkIntegration @Inject constructor(
*
* @param account account to enable/disable content change sync triggers for
* @param enable *true* enables automatic sync; *false* disables it
* @param authority sync authority (like [CalendarContract.AUTHORITY])
* @param authority sync authority (like [android.provider.CalendarContract.AUTHORITY])
* @return whether the content triggered sync was enabled successfully
*/
@WorkerThread
@@ -163,7 +163,7 @@ class SyncFrameworkIntegration @Inject constructor(
*
* @param account account to enable/disable content change sync triggers for
* @param enable *true* enables automatic sync; *false* disables it
* @param authority sync authority (like [CalendarContract.AUTHORITY])
* @param authority sync authority (like [android.provider.CalendarContract.AUTHORITY])
* @return whether the content triggered sync was enabled successfully
*/
private fun setContentTrigger(account: Account, authority: String, enable: Boolean): Boolean =

View File

@@ -28,7 +28,7 @@ import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager

View File

@@ -41,8 +41,8 @@ import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.account.InvalidAccountException
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.ical4android.TaskProvider
import at.techbee.jtx.JtxContract

View File

@@ -10,7 +10,7 @@ import androidx.work.WorkInfo
import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.sync.SyncDataType
import at.bitfire.davdroid.sync.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.adapter.SyncFrameworkIntegration
import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.android.qualifiers.ApplicationContext