Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 946c4500 authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

SyncAdapter: Hilt error handling (#1299)

* SyncAdapter: Hilt error handling

* HiltTestRunner: enforce Android P requirement for MockK

* Update comment
parent 969d92d0
Loading
Loading
Loading
Loading
+10 −0
Original line number Diff line number Diff line
@@ -6,12 +6,22 @@ package at.bitfire.davdroid

import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.test.runner.AndroidJUnitRunner
import dagger.hilt.android.testing.HiltTestApplication

@Suppress("unused")
class HiltTestRunner : AndroidJUnitRunner() {

    override fun newApplication(cl: ClassLoader, name: String, context: Context): Application =
        super.newApplication(cl, HiltTestApplication::class.java.name, context)

    override fun onCreate(arguments: Bundle?) {
        super.onCreate(arguments)

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P)
            throw AssertionError("MockK requires Android P [https://mockk.io/ANDROID.html]")
    }

}
 No newline at end of file
+42 −18
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ 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.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.repository.DavCollectionRepository
@@ -25,8 +26,10 @@ import at.bitfire.davdroid.resource.LocalAddressBook.Companion.USER_DATA_COLLECT
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.sync.worker.BaseSyncWorker
import at.bitfire.davdroid.sync.worker.SyncWorkerManager
import dagger.hilt.android.AndroidEntryPoint
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
@@ -37,15 +40,46 @@ import kotlinx.coroutines.withTimeout
import java.util.logging.Level
import java.util.logging.Logger
import javax.inject.Inject
import javax.inject.Provider

abstract class SyncAdapterService: Service() {

    @Inject
    lateinit var syncAdapter: Provider<SyncAdapter>
    /**
     * 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 {
        return syncAdapter.get().syncAdapterBinder
        try {
            // create sync adapter via Hilt
            val entryPoint = EntryPointAccessors.fromApplication<EntryPoint>(this)
            val syncAdapter = entryPoint.syncAdapter()
            return syncAdapter.syncAdapterBinder

        } catch (e: IllegalStateException) {
            if (BuildConfig.DEBUG) {
                // only for debug builds: handle "Hilt not initialized" exception
                val logger = Logger.getLogger(this@SyncAdapterService::class.java.name)
                logger.log(Level.WARNING, "SyncAdapterService.onBind() was called without Hilt initialization. Ignoring", e)

                val fakeAdapter = object: AbstractThreadedSyncAdapter(this, false) {
                    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())
                    }
                }
                return fakeAdapter.syncAdapterBinder
            } else
                // re-throw in production builds
                throw e
        }
    }

    /**
@@ -68,9 +102,9 @@ abstract class SyncAdapterService: Service() {
        private val syncConditionsFactory: SyncConditions.Factory,
        private val syncWorkerManager: SyncWorkerManager
    ): AbstractThreadedSyncAdapter(
        context,
        true    // isSyncable shouldn't be -1 because DAVx5 (SyncFrameworkIntegration) sets it to 0 or 1.
                // However, if it is -1 by accident, set it to 1 to avoid endless sync loops.
        /* 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.
    ) {

        /**
@@ -174,18 +208,8 @@ abstract class SyncAdapterService: Service() {
}

// exported sync adapter services; we need a separate class for each authority

@AndroidEntryPoint
class CalendarsSyncAdapterService: SyncAdapterService()

@AndroidEntryPoint
class ContactsSyncAdapterService: SyncAdapterService()

@AndroidEntryPoint
class JtxSyncAdapterService: SyncAdapterService()

@AndroidEntryPoint
class OpenTasksSyncAdapterService: SyncAdapterService()

@AndroidEntryPoint
class TasksOrgSyncAdapterService: SyncAdapterService()
 No newline at end of file
+12 −1
Original line number Diff line number Diff line
@@ -3,4 +3,15 @@
    android:accountType="@string/account_type"
    android:userVisible="false"
    android:supportsUploading="true"
    android:allowParallelSyncs="true" />
 No newline at end of file
    android:allowParallelSyncs="true"
    android:isAlwaysSyncable="true" />

<!-- Note:
isAlwaysSyncable = false   Sets isSyncable(contentAuthority) of new Account(type=accountType) to -1
                           → causes a sync with SYNC_EXTRAS_INITIALIZE to be scheduled
                           → that sync is expected to finally set isSyncable to 0 or 1.
isAlwaysSyncable = true    Sets isSyncable(contentAuthority) of new Account(type=accountType) to 1
                           → causes a normal sync (without SYNC_EXTRAS_INITIALIZE) to be scheduled.

There's no possibility to automatically set isSyncable to 0 at account creation.
-->
 No newline at end of file
+4 −1
Original line number Diff line number Diff line
@@ -3,4 +3,7 @@
    android:accountType="@string/account_type_address_book"
    android:userVisible="false"
    android:supportsUploading="true"
    android:allowParallelSyncs="true" />
 No newline at end of file
    android:allowParallelSyncs="true"
    android:isAlwaysSyncable="true" />

<!-- See sync_calendars.xml for an explanation of isAlwaysSyncable. -->
 No newline at end of file
+4 −1
Original line number Diff line number Diff line
@@ -3,4 +3,7 @@
    android:accountType="@string/account_type"
    android:userVisible="false"
    android:supportsUploading="true"
    android:allowParallelSyncs="true" />
 No newline at end of file
    android:allowParallelSyncs="true"
    android:isAlwaysSyncable="true" />

<!-- See sync_calendars.xml for an explanation of isAlwaysSyncable. -->
 No newline at end of file
Loading