diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt index 687c48b07..a1a9ebb5e 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/OkhttpClientTest.kt @@ -4,10 +4,7 @@ package at.bitfire.davdroid -import android.content.Context import at.bitfire.davdroid.network.HttpClient -import at.bitfire.davdroid.settings.SettingsManager -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import okhttp3.Request @@ -19,15 +16,11 @@ import javax.inject.Inject @HiltAndroidTest class OkhttpClientTest { - @get:Rule - val hiltRule = HiltAndroidRule(this) - @Inject - @ApplicationContext - lateinit var context: Context + lateinit var httpClientBuilder: HttpClient.Builder - @Inject - lateinit var settingsManager: SettingsManager + @get:Rule + val hiltRule = HiltAndroidRule(this) @Before fun inject() { @@ -37,12 +30,14 @@ class OkhttpClientTest { @Test fun testIcloudWithSettings() { - val client = HttpClient.Builder(context).build() - client.okHttpClient.newCall(Request.Builder() + httpClientBuilder.build().use { client -> + client.okHttpClient + .newCall(Request.Builder() .get() .url("https://icloud.com") .build()) .execute() + } } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt index 356143d5a..56166c88c 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/db/CollectionTest.kt @@ -4,14 +4,11 @@ package at.bitfire.davdroid.db -import android.content.Context import android.security.NetworkSecurityPolicy import androidx.test.filters.SmallTest import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.property.webdav.ResourceType import at.bitfire.davdroid.network.HttpClient -import at.bitfire.davdroid.settings.SettingsManager -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import okhttp3.HttpUrl.Companion.toHttpUrl @@ -31,15 +28,11 @@ import javax.inject.Inject @HiltAndroidTest class CollectionTest { - @get:Rule - val hiltRule = HiltAndroidRule(this) - @Inject - @ApplicationContext - lateinit var context: Context + lateinit var httpClientBuilder: HttpClient.Builder - @Inject - lateinit var settingsManager: SettingsManager + @get:Rule + val hiltRule = HiltAndroidRule(this) private lateinit var httpClient: HttpClient private val server = MockWebServer() @@ -48,7 +41,7 @@ class CollectionTest { fun setup() { hiltRule.inject() - httpClient = HttpClient.Builder(context).build() + httpClient = httpClientBuilder.build() Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) } @@ -211,4 +204,4 @@ class CollectionTest { assertEquals("https://example.com/1.ics".toHttpUrl(), info.source) } -} +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt index 2dc7ecf7b..1c1209904 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/network/HttpClientTest.kt @@ -4,9 +4,7 @@ package at.bitfire.davdroid.network -import android.content.Context import android.security.NetworkSecurityPolicy -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import okhttp3.Request @@ -25,21 +23,20 @@ import javax.inject.Inject @HiltAndroidTest class HttpClientTest { - lateinit var server: MockWebServer - lateinit var httpClient: HttpClient - @get:Rule var hiltRule = HiltAndroidRule(this) @Inject - @ApplicationContext - lateinit var context: Context + lateinit var httpClientBuilder: HttpClient.Builder + + lateinit var httpClient: HttpClient + lateinit var server: MockWebServer @Before fun setUp() { hiltRule.inject() - httpClient = HttpClient.Builder(context).build() + httpClient = httpClientBuilder.build() server = MockWebServer() server.start(30000) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt index 7d8ae9f8a..9e32f6faf 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavCollectionRepositoryTest.kt @@ -4,16 +4,14 @@ package at.bitfire.davdroid.repository -import android.content.Context import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service -import at.bitfire.davdroid.settings.AccountSettings -import dagger.Lazy -import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValueIntoSet import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import io.mockk.mockk +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule import io.mockk.verify import kotlinx.coroutines.runBlocking import okhttp3.HttpUrl.Companion.toHttpUrl @@ -26,28 +24,36 @@ import javax.inject.Inject @HiltAndroidTest class DavCollectionRepositoryTest { - @get:Rule - var hiltRule = HiltAndroidRule(this) - - @Inject - lateinit var accountSettingsFactory: AccountSettings.Factory - @Inject - @ApplicationContext - lateinit var context: Context + lateinit var collectionRepository: DavCollectionRepository @Inject lateinit var db: AppDatabase + @BindValueIntoSet + @MockK(relaxed = true) + lateinit var testObserver: DavCollectionRepository.OnChangeListener + @Inject lateinit var serviceRepository: DavServiceRepository + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockkKRule = MockKRule(this) + var service: Service? = null @Before fun setUp() { hiltRule.inject() - service = createTestService(Service.TYPE_CARDDAV)!! + + // insert test service + val serviceId = serviceRepository.insertOrReplace( + Service(id=0, accountName="test", type=Service.TYPE_CARDDAV, principal = null) + ) + service = serviceRepository.get(serviceId)!! } @After @@ -67,18 +73,6 @@ class DavCollectionRepositoryTest { forceReadOnly = false, ) ) - val testObserver = mockk(relaxed = true) - val collectionRepository = DavCollectionRepository( - accountSettingsFactory, - context, - db, - object : Lazy> { - override fun get(): Set { - return mutableSetOf(testObserver) - } - }, - serviceRepository - ) assert(db.collectionDao().get(collectionId)?.forceReadOnly == false) verify(exactly = 0) { @@ -91,13 +85,4 @@ class DavCollectionRepositoryTest { } } - - // Test helpers and dependencies - - private fun createTestService(serviceType: String) : Service? { - val service = Service(id=0, accountName="test", type=serviceType, principal = null) - val serviceId = serviceRepository.insertOrReplace(service) - return serviceRepository.get(serviceId) - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt index a39177f37..7594058b0 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/repository/DavHomeSetRepositoryTest.kt @@ -18,26 +18,30 @@ import javax.inject.Inject @HiltAndroidTest class DavHomeSetRepositoryTest { - @get:Rule - var hiltRule = HiltAndroidRule(this) - @Inject lateinit var repository: DavHomeSetRepository @Inject lateinit var serviceRepository: DavServiceRepository + @get:Rule + var hiltRule = HiltAndroidRule(this) + + var serviceId: Long = 0 + @Before fun setUp() { hiltRule.inject() + + serviceId = serviceRepository.insertOrReplace( + Service(id=0, accountName="test", type= Service.TYPE_CALDAV, principal = null) + ) } @Test fun testInsertOrUpdate() { // should insert new row or update (upsert) existing row - without changing its key! - val serviceId = createTestService() - val entry1 = HomeSet(id=0, serviceId=serviceId, personal=true, url="https://example.com/1".toHttpUrl()) val insertId1 = repository.insertOrUpdateByUrl(entry1) assertEquals(1L, insertId1) @@ -57,8 +61,6 @@ class DavHomeSetRepositoryTest { @Test fun testDelete() { // should delete row with given primary key (id) - val serviceId = createTestService() - val entry1 = HomeSet(id=1, serviceId=serviceId, personal=true, url= "https://example.com/1".toHttpUrl()) val insertId1 = repository.insertOrUpdateByUrl(entry1) @@ -69,10 +71,4 @@ class DavHomeSetRepositoryTest { assertEquals(null, repository.getById(1L)) } - - private fun createTestService() : Long { - val service = Service(id=0, accountName="test", type= Service.TYPE_CALDAV, principal = null) - return serviceRepository.insertOrReplace(service) - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt index 97d2a6e25..53dbeb300 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresherTest.kt @@ -4,7 +4,6 @@ package at.bitfire.davdroid.servicedetection -import android.content.Context import android.security.NetworkSecurityPolicy import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Collection @@ -14,11 +13,12 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager -import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import io.mockk.every -import io.mockk.mockkObject +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -37,50 +37,62 @@ import javax.inject.Inject @HiltAndroidTest class CollectionListRefresherTest { - @Inject @ApplicationContext - lateinit var context: Context - @Inject lateinit var db: AppDatabase + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + @Inject lateinit var logger: Logger @Inject lateinit var refresherFactory: CollectionListRefresher.Factory - @Inject + @BindValue + @MockK(relaxed = true) lateinit var settings: SettingsManager @get:Rule - var hiltRule = HiltAndroidRule(this) + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) - private val mockServer = MockWebServer() private lateinit var client: HttpClient + private lateinit var mockServer: MockWebServer + private lateinit var service: Service @Before - fun setup() { + fun setUp() { hiltRule.inject() // Start mock web server - mockServer.dispatcher = TestDispatcher(logger) - mockServer.start() - - client = HttpClient.Builder(context).build() + mockServer = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } + // build HTTP client + client = httpClientBuilder.build() Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + // insert test service + val serviceId = db.serviceDao().insertOrReplace( + Service(id = 0, accountName = "test", type = Service.TYPE_CARDDAV, principal = null) + ) + service = db.serviceDao().get(serviceId)!! } @After - fun teardown() { + fun tearDown() { + client.close() mockServer.shutdown() - db.close() } @Test fun testDiscoverHomesets() { - val service = createTestService(Service.TYPE_CARDDAV)!! val baseUrl = mockServer.url(PATH_CARDDAV + SUBPATH_PRINCIPAL) // Query home sets @@ -110,8 +122,6 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_addsNewCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // save homeset in DB val homesetId = db.homeSetDao().insert( HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) @@ -138,8 +148,6 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_updatesExistingCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // save "old" collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( @@ -175,8 +183,6 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_preservesCollectionFlags() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // save "old" collection in DB - with set flags val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( @@ -216,8 +222,6 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_marksRemovedCollectionsAsHomeless() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // save homeset in DB - which is empty (zero address books) on the serverside val homesetId = db.homeSetDao().insert( HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_EMPTY")) @@ -244,8 +248,6 @@ class CollectionListRefresherTest { @Test fun refreshHomesetsAndTheirCollections_addsOwnerUrls() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // save a homeset in DB val homesetId = db.homeSetDao().insert( HomeSet(id=0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) @@ -283,8 +285,6 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_updatesExistingCollection() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place homeless collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( @@ -318,8 +318,6 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_deletesInaccessibleCollections() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place homeless collection in DB - it is also inaccessible val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( @@ -341,8 +339,6 @@ class CollectionListRefresherTest { @Test fun refreshHomelessCollections_addsOwnerUrls() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place homeless collection in DB val collectionId = db.collectionDao().insertOrUpdateByUrl( Collection( @@ -375,8 +371,6 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_inaccessiblePrincipal() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place principal without display name in db val principalId = db.principalDao().insert( Principal( @@ -410,8 +404,6 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_updatesPrincipal() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place principal without display name in db val principalId = db.principalDao().insert( Principal( @@ -445,8 +437,6 @@ class CollectionListRefresherTest { @Test fun refreshPrincipals_deletesPrincipalsWithoutCollections() { - val service = createTestService(Service.TYPE_CARDDAV)!! - // place principal without collections in DB db.principalDao().insert( Principal( @@ -468,159 +458,161 @@ class CollectionListRefresherTest { @Test fun shouldPreselect_none() { - val service = createTestService(Service.TYPE_CARDDAV)!! - - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" - - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") - ) - val homesets = listOf( - HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_NONE + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertFalse(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) } @Test fun shouldPreselect_all() { - val service = createTestService(Service.TYPE_CARDDAV)!! - - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" - - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") - ) - val homesets = listOf( - HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertTrue(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertTrue(refresher.shouldPreselect(collection, homesets)) } @Test fun shouldPreselect_all_blacklisted() { - val service = createTestService(Service.TYPE_CARDDAV)!! - val url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") + val url = mockServer.url("/addressbook-homeset/addressbook/") - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns url.toString() + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_ALL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns url.toString() - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = url - ) - val homesets = listOf( - HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = url + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertFalse(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) } @Test fun shouldPreselect_personal_notPersonal() { - val service = createTestService(Service.TYPE_CARDDAV)!! - - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" - - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") - ) - val homesets = listOf( - HomeSet(0, service.id, false, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = false, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertFalse(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) } @Test fun shouldPreselect_personal_isPersonal() { - val service = createTestService(Service.TYPE_CARDDAV)!! - - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" - - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") - ) - val homesets = listOf( - HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns "" + + val collection = Collection( + 0, + service.id, + 0, + type = Collection.TYPE_ADDRESSBOOK, + url = mockServer.url("/addressbook-homeset/addressbook/") + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertTrue(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertTrue(refresher.shouldPreselect(collection, homesets)) } @Test fun shouldPreselect_personal_isPersonalButBlacklisted() { - val service = createTestService(Service.TYPE_CARDDAV)!! - val collectionUrl = mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK/") + val collectionUrl = mockServer.url("/addressbook-homeset/addressbook/") - mockkObject(settings) { - every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL - every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns collectionUrl.toString() + every { settings.getIntOrNull(Settings.PRESELECT_COLLECTIONS) } returns Settings.PRESELECT_COLLECTIONS_PERSONAL + every { settings.getString(Settings.PRESELECT_COLLECTIONS_EXCLUDED) } returns collectionUrl.toString() - val collection = Collection( - 0, - service.id, - 0, - type = Collection.TYPE_ADDRESSBOOK, - url = collectionUrl - ) - val homesets = listOf( - HomeSet(0, service.id, true, mockServer.url("$PATH_CARDDAV$SUBPATH_ADDRESSBOOK_HOMESET_PERSONAL")) + val collection = Collection( + id = 0, + serviceId = service.id, + homeSetId = 0, + type = Collection.TYPE_ADDRESSBOOK, + url = collectionUrl + ) + val homesets = listOf( + HomeSet( + id = 0, + serviceId = service.id, + personal = true, + url = mockServer.url("/addressbook-homeset/") ) + ) - val refresher = refresherFactory.create(service, client.okHttpClient) - assertFalse(refresher.shouldPreselect(collection, homesets)) - } + val refresher = refresherFactory.create(service, client.okHttpClient) + assertFalse(refresher.shouldPreselect(collection, homesets)) } - // Test helpers and dependencies - - private fun createTestService(serviceType: String) : Service? { - val service = Service(id=0, accountName="test", type=serviceType, principal = null) - val serviceId = db.serviceDao().insertOrReplace(service) - return db.serviceDao().get(serviceId) - } companion object { + private const val PATH_CALDAV = "/caldav" private const val PATH_CARDDAV = "/carddav" @@ -633,6 +625,7 @@ class CollectionListRefresherTest { private const val SUBPATH_ADDRESSBOOK_HOMESET_EMPTY = "/addressbooks-homeset-empty" private const val SUBPATH_ADDRESSBOOK = "/addressbooks/my-contacts" private const val SUBPATH_ADDRESSBOOK_INACCESSIBLE = "/addressbooks/inaccessible-contacts" + } class TestDispatcher( @@ -751,4 +744,5 @@ class CollectionListRefresherTest { } } -} + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt index 65b7d8221..b533d2552 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinderTest.kt @@ -4,17 +4,13 @@ package at.bitfire.davdroid.servicedetection -import android.content.Context import android.security.NetworkSecurityPolicy -import androidx.test.filters.SmallTest import at.bitfire.dav4jvm.DavResource import at.bitfire.dav4jvm.property.carddav.AddressbookHomeSet import at.bitfire.dav4jvm.property.webdav.ResourceType import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.servicedetection.DavResourceFinder.Configuration.ServiceInfo -import at.bitfire.davdroid.settings.SettingsManager -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import okhttp3.mockwebserver.Dispatcher @@ -53,8 +49,7 @@ class DavResourceFinderTest { val hiltRule = HiltAndroidRule(this) @Inject - @ApplicationContext - lateinit var context: Context + lateinit var httpClientBuilder: HttpClient.Builder @Inject lateinit var logger: Logger @@ -62,40 +57,37 @@ class DavResourceFinderTest { @Inject lateinit var resourceFinderFactory: DavResourceFinder.Factory - @Inject - lateinit var settingsManager: SettingsManager - - private val server = MockWebServer() - - private lateinit var finder: DavResourceFinder + private lateinit var server: MockWebServer private lateinit var client: HttpClient + private lateinit var finder: DavResourceFinder @Before - fun setup() { + fun setUp() { hiltRule.inject() - server.dispatcher = TestDispatcher(logger) - server.start() + server = MockWebServer().apply { + dispatcher = TestDispatcher(logger) + start() + } - val baseURI = URI.create("/") val credentials = Credentials("mock", "12345") - - finder = resourceFinderFactory.create(baseURI, credentials) - client = HttpClient.Builder(context) - .addAuthentication(null, credentials) + client = httpClientBuilder + .authenticate(host = null, credentials = credentials) .build() - Assume.assumeTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) + + val baseURI = URI.create("/") + finder = resourceFinderFactory.create(baseURI, credentials) } @After - fun teardown() { + fun tearDown() { + client.close() server.shutdown() } @Test - @SmallTest fun testRememberIfAddressBookOrHomeset() { // recognize home set var info = ServiceInfo() diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt index d9d9e8174..0d61e717c 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/SyncManagerTest.kt @@ -20,14 +20,13 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.repository.DavSyncStatsRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.sync.account.TestAccount -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.components.SingletonComponent import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.junit4.MockKRule import io.mockk.mockk import okhttp3.Protocol import okhttp3.internal.http.StatusLine @@ -46,32 +45,33 @@ import javax.inject.Inject @HiltAndroidTest class SyncManagerTest { - @Module - @InstallIn(SingletonComponent::class) - object SyncManagerTestModule { - @Provides - fun davSyncStatsRepository(): DavSyncStatsRepository = mockk(relaxed = true) - } - - - @get:Rule - val hiltRule = HiltAndroidRule(this) - @Inject lateinit var accountSettingsFactory: AccountSettings.Factory - @Inject - @ApplicationContext + @Inject @ApplicationContext lateinit var context: Context + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + @Inject lateinit var syncManagerFactory: TestSyncManager.Factory + @BindValue + @MockK(relaxed = true) + lateinit var syncStatsRepository: DavSyncStatsRepository + @Inject lateinit var workerFactory: HiltWorkerFactory - lateinit var account: Account - private val server = MockWebServer() + @get:Rule + val hiltRule = HiltAndroidRule(this) + + @get:Rule + val mockKRule = MockKRule(this) + + private lateinit var account: Account + private lateinit var server: MockWebServer @Before fun setUp() { @@ -80,7 +80,9 @@ class SyncManagerTest { account = TestAccount.create() - server.start() + server = MockWebServer().apply { + start() + } } @After @@ -513,10 +515,9 @@ class SyncManagerTest { } ) = syncManagerFactory.create( account, - accountSettingsFactory.create(account), arrayOf(), "TestAuthority", - HttpClient.Builder(context).build(), + httpClientBuilder.build(), syncResult, localCollection, collection diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt index 3856b8b09..04ab90783 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/TestSyncManager.kt @@ -25,7 +25,6 @@ import org.junit.Assert.assertEquals class TestSyncManager @AssistedInject constructor( @Assisted account: Account, - @Assisted accountSettings: AccountSettings, @Assisted extras: Array, @Assisted authority: String, @Assisted httpClient: HttpClient, @@ -34,7 +33,6 @@ class TestSyncManager @AssistedInject constructor( @Assisted collection: Collection ): SyncManager( account, - accountSettings, httpClient, extras, authority, @@ -47,7 +45,6 @@ class TestSyncManager @AssistedInject constructor( interface Factory { fun create( account: Account, - accountSettings: AccountSettings, extras: Array, authority: String, httpClient: HttpClient, diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProviderTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProviderTest.kt index b797642fe..498bd74c3 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProviderTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProviderTest.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.webdav import android.content.Context import android.security.NetworkSecurityPolicy -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.WebDavDocument import at.bitfire.davdroid.db.WebDavMount @@ -14,6 +13,7 @@ import at.bitfire.davdroid.network.HttpClient import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.junit4.MockKRule import okhttp3.CookieJar import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse @@ -25,6 +25,7 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test +import java.util.logging.Logger import javax.inject.Inject @HiltAndroidTest @@ -34,60 +35,68 @@ class DavDocumentsProviderTest { private const val PATH_WEBDAV_ROOT = "/webdav" } - @get:Rule - val hiltRule = HiltAndroidRule(this) + @Inject @ApplicationContext + lateinit var context: Context @Inject - @ApplicationContext - lateinit var context: Context + lateinit var credentialsStore: CredentialsStore + + @Inject + lateinit var davDocumentsActorFactory: DavDocumentsProvider.DavDocumentsActor.Factory + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + @Inject lateinit var db: AppDatabase @Inject - lateinit var logger: java.util.logging.Logger - - @Before - fun setUp() { - hiltRule.inject() - } + lateinit var testDispatcher: TestDispatcher + @get:Rule + val hiltRule = HiltAndroidRule(this) - private var mockServer = MockWebServer() + @get:Rule + val mockkRule = MockKRule(this) + private lateinit var server: MockWebServer private lateinit var client: HttpClient - @Before - fun mockServerSetup() { - // Start mock web server - mockServer.dispatcher = TestDispatcher(logger) - mockServer.start() + fun setUp() { + hiltRule.inject() + + server = MockWebServer().apply { + dispatcher = testDispatcher + start() + } - client = HttpClient.Builder(context).build() + client = httpClientBuilder.build() // mock server delivers HTTP without encryption assertTrue(NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted) } @After - fun cleanUp() { - mockServer.shutdown() - db.close() + fun tearDown() { + client.close() + server.shutdown() } @Test fun testDoQueryChildren_insert() { // Create parent and root in database - val id = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT))) + val id = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT))) val webDavMount = db.webDavMountDao().getById(id) val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount) - val cookieStore = mutableMapOf() // Query - DavDocumentsProvider.DavDocumentsActor(context, db, logger, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority)) - .queryChildren(parent) + val actor = davDocumentsActorFactory.create( + cookieStore = mutableMapOf(), + credentialsStore = credentialsStore + ) + actor.queryChildren(parent) // Assert new children were inserted into db assertEquals(3, db.webDavDocumentDao().getChildren(parent.id).size) @@ -99,10 +108,9 @@ class DavDocumentsProviderTest { @Test fun testDoQueryChildren_update() { // Create parent and root in database - val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT))) + val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT))) val webDavMount = db.webDavMountDao().getById(mountId) val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount) - val cookieStore = mutableMapOf() assertEquals("Cat food storage", db.webDavDocumentDao().get(parent.id)!!.displayName) // Create a folder @@ -120,8 +128,11 @@ class DavDocumentsProviderTest { assertEquals("My Books", db.webDavDocumentDao().get(folderId)!!.displayName) // Query - should update the parent displayname and folder name - DavDocumentsProvider.DavDocumentsActor(context, db, logger, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority)) - .queryChildren(parent) + val actor = davDocumentsActorFactory.create( + cookieStore = mutableMapOf(), + credentialsStore = credentialsStore + ) + actor.queryChildren(parent) // Assert parent and children were updated in database assertEquals("Cats WebDAV", db.webDavDocumentDao().get(parent.id)!!.displayName) @@ -133,10 +144,9 @@ class DavDocumentsProviderTest { @Test fun testDoQueryChildren_delete() { // Create parent and root in database - val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT))) + val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT))) val webDavMount = db.webDavMountDao().getById(mountId) val parent = db.webDavDocumentDao().getOrCreateRoot(webDavMount) - val cookieStore = mutableMapOf() // Create a folder val folderId = db.webDavDocumentDao().insert( @@ -145,22 +155,24 @@ class DavDocumentsProviderTest { assertEquals("deleteme", db.webDavDocumentDao().get(folderId)!!.name) // Query - discovers serverside deletion - DavDocumentsProvider.DavDocumentsActor(context, db, logger, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority)) - .queryChildren(parent) + val actor = davDocumentsActorFactory.create( + cookieStore = mutableMapOf(), + credentialsStore = credentialsStore + ) + actor.queryChildren(parent) // Assert folder got deleted assertEquals(null, db.webDavDocumentDao().get(folderId)) } @Test - fun testDoQueryChildren_updateTwoParentsSimultaneous() { + fun testDoQueryChildren_updateTwoDirectoriesSimultaneously() { // Create root in database - val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", mockServer.url(PATH_WEBDAV_ROOT))) + val mountId = db.webDavMountDao().insert(WebDavMount(0, "Cat food storage", server.url(PATH_WEBDAV_ROOT))) val webDavMount = db.webDavMountDao().getById(mountId) val root = db.webDavDocumentDao().getOrCreateRoot(webDavMount) - val cookieStore = mutableMapOf() - // Create two parents + // Create two directories val parent1Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent1", true)) val parent2Id = db.webDavDocumentDao().insert(WebDavDocument(0, mountId, root.id, "parent2", true)) val parent1 = db.webDavDocumentDao().get(parent1Id)!! @@ -169,10 +181,12 @@ class DavDocumentsProviderTest { assertEquals("parent2", parent2.name) // Query - find children of two nodes simultaneously - DavDocumentsProvider.DavDocumentsActor(context, db, logger, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority)) - .queryChildren(parent1) - DavDocumentsProvider.DavDocumentsActor(context, db, logger, cookieStore, CredentialsStore(context), context.getString(R.string.webdav_authority)) - .queryChildren(parent2) + val actor = davDocumentsActorFactory.create( + cookieStore = mutableMapOf(), + credentialsStore = credentialsStore + ) + actor.queryChildren(parent1) + actor.queryChildren(parent2) // Assert the two folders names have changed assertEquals("childOne.txt", db.webDavDocumentDao().getChildren(parent1Id)[0].name) @@ -182,8 +196,8 @@ class DavDocumentsProviderTest { // mock server - class TestDispatcher( - private val logger: java.util.logging.Logger + class TestDispatcher @Inject constructor( + private val logger: Logger ): Dispatcher() { data class Resource( @@ -192,10 +206,10 @@ class DavDocumentsProviderTest { ) override fun dispatch(request: RecordedRequest): MockResponse { + logger.info("Request: $request") val requestPath = request.path!!.trimEnd('/') if (request.method.equals("PROPFIND", true)) { - val propsMap = mutableMapOf( PATH_WEBDAV_ROOT to arrayOf( Resource("", @@ -239,7 +253,6 @@ class DavDocumentsProviderTest { responses + "" - logger.info("Query path: $requestPath") logger.info("Response: $multistatus") return MockResponse() .setResponseCode(207) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt new file mode 100644 index 000000000..0859849f6 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/ClientCertKeyManager.kt @@ -0,0 +1,47 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.content.Context +import android.security.KeyChain +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import dagger.hilt.android.qualifiers.ApplicationContext +import java.net.Socket +import java.security.Principal +import javax.net.ssl.X509ExtendedKeyManager + +/** + * KeyManager that provides a client certificate and private key from the Android KeyChain. + * + * @throws IllegalArgumentException if the alias doesn't exist or is not accessible + */ +class ClientCertKeyManager @AssistedInject constructor( + @Assisted private val alias: String, + @ApplicationContext private val context: Context +): X509ExtendedKeyManager() { + + @AssistedFactory + interface Factory { + fun create(alias: String): ClientCertKeyManager + } + + val certs = KeyChain.getCertificateChain(context, alias) ?: throw IllegalArgumentException("Alias doesn't exist or not accessible: $alias") + val key = KeyChain.getPrivateKey(context, alias) ?: throw IllegalArgumentException("Alias doesn't exist or not accessible: $alias") + + override fun getServerAliases(p0: String?, p1: Array?): Array? = null + override fun chooseServerAlias(p0: String?, p1: Array?, p2: Socket?) = null + + override fun getClientAliases(p0: String?, p1: Array?) = arrayOf(alias) + override fun chooseClientAlias(p0: Array?, p1: Array?, p2: Socket?) = alias + + override fun getCertificateChain(forAlias: String?) = + certs.takeIf { forAlias == alias } + + override fun getPrivateKey(forAlias: String?) = + key.takeIf { forAlias == alias } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt index 951fa4b1f..f1ff9fabf 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/HttpClient.kt @@ -4,333 +4,280 @@ package at.bitfire.davdroid.network +import android.accounts.Account import android.content.Context -import android.os.Build -import android.security.KeyChain import at.bitfire.cert4android.CustomCertManager import at.bitfire.dav4jvm.BasicDigestAuthHandler import at.bitfire.dav4jvm.UrlUtils -import at.bitfire.davdroid.BuildConfig import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import dagger.hilt.EntryPoint -import dagger.hilt.InstallIn -import dagger.hilt.android.EntryPointAccessors -import dagger.hilt.components.SingletonComponent -import java.io.File -import java.net.InetSocketAddress -import java.net.Proxy -import java.net.Socket -import java.security.KeyStore -import java.security.Principal -import java.util.Locale -import java.util.concurrent.TimeUnit -import java.util.logging.Level -import java.util.logging.Logger -import javax.net.ssl.KeyManager -import javax.net.ssl.SSLContext -import javax.net.ssl.TrustManagerFactory -import javax.net.ssl.X509ExtendedKeyManager -import javax.net.ssl.X509TrustManager +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationService +import okhttp3.Authenticator import okhttp3.Cache import okhttp3.ConnectionSpec import okhttp3.CookieJar import okhttp3.Interceptor -import okhttp3.OkHttp import okhttp3.OkHttpClient import okhttp3.Protocol -import okhttp3.Response import okhttp3.brotli.BrotliInterceptor import okhttp3.internal.tls.OkHostnameVerifier import okhttp3.logging.HttpLoggingInterceptor +import java.io.File +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit +import java.util.logging.Level +import java.util.logging.Logger +import javax.inject.Inject +import javax.inject.Provider +import javax.net.ssl.KeyManager +import javax.net.ssl.SSLContext -class HttpClient @AssistedInject constructor( - @Assisted val okHttpClient: OkHttpClient, - @Assisted private var authService: AuthorizationService? = null, - val settingsManager: SettingsManager +class HttpClient( + val okHttpClient: OkHttpClient, + private val authorizationService: AuthorizationService? = null ): AutoCloseable { - companion object { - /** max. size of disk cache (10 MB) */ - const val DISK_CACHE_MAX_SIZE: Long = 10*1024*1024 - - /** Base Builder to build all clients from. Use rarely; [OkHttpClient]s should - * be reused as much as possible. */ - fun baseBuilder() = - OkHttpClient.Builder() - // Set timeouts. According to [AbstractThreadedSyncAdapter], when there is no network - // traffic within a minute, a sync will be cancelled. - .connectTimeout(15, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .readTimeout(120, TimeUnit.SECONDS) - .pingInterval( - 45, - TimeUnit.SECONDS - ) // avoid cancellation because of missing traffic; only works for HTTP/2 - - // keep TLS 1.0 and 1.1 for now; remove when major browsers have dropped it (probably 2020) - .connectionSpecs( - listOf( - ConnectionSpec.CLEARTEXT, - ConnectionSpec.COMPATIBLE_TLS - ) - ) - - // don't allow redirects by default, because it would break PROPFIND handling - .followRedirects(false) - - // add User-Agent to every request - .addInterceptor(UserAgentInterceptor) - } - - @AssistedFactory - interface Factory { - fun create(okHttpClient: OkHttpClient, authService: AuthorizationService?): HttpClient - } - - override fun close() { - authService?.dispose() + authorizationService?.dispose() okHttpClient.cache?.close() } - class Builder( - val context: Context, - accountSettings: AccountSettings? = null, - val logger: Logger = Logger.getGlobal(), - val loggerLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY + // builder + + /** + * Builder for the [HttpClient]. + * + * **Attention:** If the builder is injected, it shouldn't be used from multiple locations to generate different clients because then + * there's only one [Builder] object and setting properties from one location would influence the others. + * + * To generate multiple clients, inject and use `Provider` instead. + */ + class Builder @Inject constructor( + private val accountSettingsFactory: AccountSettings.Factory, + private val authorizationServiceProvider: Provider, + @ApplicationContext private val context: Context, + defaultLogger: Logger, + private val keyManagerFactory: ClientCertKeyManager.Factory, + private val settingsManager: SettingsManager ) { - @EntryPoint - @InstallIn(SingletonComponent::class) - interface HttpClientBuilderEntryPoint { - fun authorizationService(): AuthorizationService - fun httpClientFactory(): Factory - fun settingsManager(): SettingsManager - } - - private val entryPoint = EntryPointAccessors.fromApplication(context) + // property setters/getters - fun interface CertManagerProducer { - fun certManager(): CustomCertManager + private var logger: Logger = defaultLogger + fun setLogger(logger: Logger): Builder { + this.logger = logger + return this } - private var appInForeground: MutableStateFlow? = - MutableStateFlow(false) - private var authService: AuthorizationService? = null - private var certManagerProducer: CertManagerProducer? = null - private var certificateAlias: String? = null - private var offerCompression: Boolean = false - - // default cookie store for non-persistent cookies (some services like Horde use cookies for session tracking) - private var cookieStore: CookieJar? = MemoryCookieStore() - - private val orig = baseBuilder() - - init { - // add network logging, if requested - if (logger.isLoggable(Level.FINEST)) { - val loggingInterceptor = HttpLoggingInterceptor { message -> logger.finest(message) } - loggingInterceptor.level = loggerLevel - orig.addNetworkInterceptor(loggingInterceptor) - } - - val settings = entryPoint.settingsManager() - - // custom proxy support - try { - val proxyTypeValue = settings.getInt(Settings.PROXY_TYPE) - if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) { - // we set our own proxy - val address by lazy { // lazy because not required for PROXY_TYPE_NONE - InetSocketAddress( - settings.getString(Settings.PROXY_HOST), - settings.getInt(Settings.PROXY_PORT) - ) - } - val proxy = - when (proxyTypeValue) { - Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY - Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address) - Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address) - else -> throw IllegalArgumentException("Invalid proxy type") - } - orig.proxy(proxy) - logger.log(Level.INFO, "Using proxy setting", proxy) - } - } catch (e: Exception) { - logger.log(Level.SEVERE, "Can't set proxy, ignoring", e) - } - - customCertManager { - // by default, use a CustomCertManager that respects the "distrust system certificates" setting - val trustSystemCerts = !settings.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES) - CustomCertManager(context, trustSystemCerts, appInForeground) - } - - // use account settings for authentication and cookies - if (accountSettings != null) - addAuthentication(null, accountSettings.credentials(), authStateCallback = { authState: AuthState -> - accountSettings.credentials(Credentials(authState = authState)) - }) + private var loggerInterceptorLevel: HttpLoggingInterceptor.Level = HttpLoggingInterceptor.Level.BODY + fun loggerInterceptorLevel(level: HttpLoggingInterceptor.Level): Builder { + loggerInterceptorLevel = level + return this } - constructor(context: Context, host: String?, credentials: Credentials?) : this(context) { - if (credentials != null) - addAuthentication(host, credentials) + // default cookie store for non-persistent cookies (some services like Horde use cookies for session tracking) + private var cookieStore: CookieJar = MemoryCookieStore() + fun setCookieStore(cookieStore: CookieJar): Builder { + this.cookieStore = cookieStore + return this } - fun addAuthentication(host: String?, credentials: Credentials, insecurePreemptive: Boolean = false, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { - if (credentials.username != null && credentials.password != null) { - val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.username, credentials.password, insecurePreemptive) - orig.addNetworkInterceptor(authHandler) - .authenticator(authHandler) + private var authenticationInterceptor: Interceptor? = null + private var authenticator: Authenticator? = null + private var authorizationService: AuthorizationService? = null + private var certificateAlias: String? = null + fun authenticate(host: String?, credentials: Credentials, authStateCallback: BearerAuthInterceptor.AuthStateUpdateCallback? = null): Builder { + if (credentials.authState != null) { + // OAuth + val authService = authorizationServiceProvider.get() + authenticationInterceptor = BearerAuthInterceptor.fromAuthState(authService, credentials.authState, authStateCallback) + authorizationService = authService + + } else if (credentials.username != null && credentials.password != null) { + // basic/digest auth + val authHandler = BasicDigestAuthHandler(UrlUtils.hostToDomain(host), credentials.username, credentials.password, insecurePreemptive = true) + authenticationInterceptor = authHandler + authenticator = authHandler } + // client certificate if (credentials.certificateAlias != null) certificateAlias = credentials.certificateAlias - credentials.authState?.let { authState -> - val newAuthService = entryPoint.authorizationService() - authService = newAuthService - BearerAuthInterceptor.fromAuthState(newAuthService, authState, authStateCallback)?.let { bearerAuthInterceptor -> - orig.addNetworkInterceptor(bearerAuthInterceptor) - } - } - return this - } - - fun allowCompression(allow: Boolean): Builder { - offerCompression = allow - return this - } - - fun cookieStore(store: CookieJar?): Builder { - cookieStore = store return this } + private var followRedirects = false fun followRedirects(follow: Boolean): Builder { - orig.followRedirects(follow) + followRedirects = follow return this } - fun customCertManager(producer: CertManagerProducer) { - certManagerProducer = producer - } - fun setForeground(foreground: Boolean): Builder { + private var appInForeground: MutableStateFlow? = MutableStateFlow(false) + fun inForeground(foreground: Boolean): Builder { appInForeground?.value = foreground return this } - fun withDiskCache(): Builder { + private var cache: Cache? = null + @Suppress("unused") + fun withDiskCache(maxSize: Long = 10*1024*1024): Builder { for (dir in arrayOf(context.externalCacheDir, context.cacheDir).filterNotNull()) { if (dir.exists() && dir.canWrite()) { val cacheDir = File(dir, "HttpClient") cacheDir.mkdir() logger.fine("Using disk cache: $cacheDir") - orig.cache(Cache(cacheDir, DISK_CACHE_MAX_SIZE)) + cache = Cache(cacheDir, maxSize) break } } return this } - fun build(): HttpClient { - cookieStore?.let { - orig.cookieJar(it) - } - if (offerCompression) - // offer Brotli and gzip compression - orig.addInterceptor(BrotliInterceptor) - - var keyManager: KeyManager? = null - certificateAlias?.let { alias -> - // get provider certificate and private key - val certs = KeyChain.getCertificateChain(context, alias) ?: return@let - val key = KeyChain.getPrivateKey(context, alias) ?: return@let - logger?.fine("Using provider certificate $alias for authentication (chain length: ${certs.size})") + // convenience builders from other classes + + /** + * Takes authentication (basic/digest or OAuth and client certificate) from a given account. + * + * @param account the account to take authentication from + * @param onlyHost if set: only authenticate for this host name + */ + fun fromAccount(account: Account, onlyHost: String? = null): Builder { + val accountSettings = accountSettingsFactory.create(account) + authenticate( + host = onlyHost, + credentials = accountSettings.credentials(), + authStateCallback = { authState: AuthState -> + accountSettings.credentials(Credentials(authState = authState)) + } + ) + return this + } - // create KeyManager - keyManager = object : X509ExtendedKeyManager() { - override fun getServerAliases(p0: String?, p1: Array?): Array? = null - override fun chooseServerAlias(p0: String?, p1: Array?, p2: Socket?) = null - override fun getClientAliases(p0: String?, p1: Array?) = - arrayOf(alias) + // actual builder - override fun chooseClientAlias(p0: Array?, p1: Array?, p2: Socket?) = - alias + fun build(): HttpClient { + val okBuilder = OkHttpClient.Builder() + // Set timeouts. According to [AbstractThreadedSyncAdapter], when there is no network + // traffic within a minute, a sync will be cancelled. + .connectTimeout(15, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(120, TimeUnit.SECONDS) + .pingInterval(45, TimeUnit.SECONDS) // avoid cancellation because of missing traffic; only works for HTTP/2 - override fun getCertificateChain(forAlias: String?) = - certs.takeIf { forAlias == alias } + // don't allow redirects by default because it would break PROPFIND handling + .followRedirects(false) - override fun getPrivateKey(forAlias: String?) = - key.takeIf { forAlias == alias } - } + // add User-Agent to every request + .addInterceptor(UserAgentInterceptor) - // HTTP/2 doesn't support client certificates (yet) - // see https://tools.ietf.org/html/draft-ietf-httpbis-http2-secondary-certs-04 - orig.protocols(listOf(Protocol.HTTP_1_1)) - } + // connection-private cookie store + .cookieJar(cookieStore) - if (certManagerProducer != null || keyManager != null) { - val manager = certManagerProducer?.certManager() + // allow cleartext and TLS 1.2+ + .connectionSpecs(listOf( + ConnectionSpec.CLEARTEXT, + ConnectionSpec.MODERN_TLS + )) - val trustManager = manager ?: /* fall back to system default trust manager */ - TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) - .let { factory -> - factory.init(null as KeyStore?) - factory.trustManagers.first() as X509TrustManager - } + // offer Brotli and gzip compression (can be disabled per request with `Accept-Encoding: identity`) + .addInterceptor(BrotliInterceptor) - val hostnameVerifier = - if (manager != null) - manager.HostnameVerifier(OkHostnameVerifier) - else - OkHostnameVerifier - - val sslContext = SSLContext.getInstance("TLS") - sslContext.init( - if (keyManager != null) arrayOf(keyManager) else null, - arrayOf(trustManager), - null) - orig.sslSocketFactory(sslContext.socketFactory, trustManager) - orig.hostnameVerifier(hostnameVerifier) - } + // add cache, if requested + .cache(cache) - return entryPoint.httpClientFactory().create(orig.build(), authService = authService) - } + // app-wide custom proxy support + buildProxy(okBuilder) - } + // add authentication + buildAuthentication(okBuilder) + // add network logging, if requested + if (logger.isLoggable(Level.FINEST)) { + val loggingInterceptor = HttpLoggingInterceptor { message -> logger.finest(message) } + loggingInterceptor.level = loggerInterceptorLevel + okBuilder.addNetworkInterceptor(loggingInterceptor) + } - object UserAgentInterceptor: Interceptor { + return HttpClient( + okHttpClient = okBuilder.build(), + authorizationService = authorizationService + ) + } - val userAgent = "DAVx5/${BuildConfig.VERSION_NAME} (dav4jvm; " + - "okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}" + private fun buildAuthentication(okBuilder: OkHttpClient.Builder) { + // basic/digest auth and OAuth + authenticationInterceptor?.let { okBuilder.addInterceptor(it) } + authenticator?.let { okBuilder.authenticator(it) } + + // client certificate + val keyManager: KeyManager? = certificateAlias?.let { alias -> + try { + val manager = keyManagerFactory.create(alias) + logger.fine("Using certificate $alias for authentication") + + // HTTP/2 doesn't support client certificates (yet) + // see https://tools.ietf.org/html/draft-ietf-httpbis-http2-secondary-certs-04 + okBuilder.protocols(listOf(Protocol.HTTP_1_1)) + + manager + } catch (e: IllegalArgumentException) { + logger.log(Level.SEVERE, "Couldn't create KeyManager for certificate $alias", e) + null + } + } - init { - Logger.getGlobal().info("Will set User-Agent: $userAgent") + // cert4android integration + val certManager = CustomCertManager( + context = context, + trustSystemCerts = !settingsManager.getBoolean(Settings.DISTRUST_SYSTEM_CERTIFICATES), + appInForeground = appInForeground + ) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + /* km = */ if (keyManager != null) arrayOf(keyManager) else null, + /* tm = */ arrayOf(certManager), + /* random = */ null + ) + okBuilder + .sslSocketFactory(sslContext.socketFactory, certManager) + .hostnameVerifier(certManager.HostnameVerifier(OkHostnameVerifier)) } - override fun intercept(chain: Interceptor.Chain): Response { - val locale = Locale.getDefault() - val request = chain.request().newBuilder() - .header("User-Agent", userAgent) - .header("Accept-Language", "${locale.language}-${locale.country}, ${locale.language};q=0.7, *;q=0.5") - .build() - return chain.proceed(request) + private fun buildProxy(okBuilder: OkHttpClient.Builder) { + try { + val proxyTypeValue = settingsManager.getInt(Settings.PROXY_TYPE) + if (proxyTypeValue != Settings.PROXY_TYPE_SYSTEM) { + // we set our own proxy + val address by lazy { // lazy because not required for PROXY_TYPE_NONE + InetSocketAddress( + settingsManager.getString(Settings.PROXY_HOST), + settingsManager.getInt(Settings.PROXY_PORT) + ) + } + val proxy = + when (proxyTypeValue) { + Settings.PROXY_TYPE_NONE -> Proxy.NO_PROXY + Settings.PROXY_TYPE_HTTP -> Proxy(Proxy.Type.HTTP, address) + Settings.PROXY_TYPE_SOCKS -> Proxy(Proxy.Type.SOCKS, address) + else -> throw IllegalArgumentException("Invalid proxy type") + } + okBuilder.proxy(proxy) + logger.log(Level.INFO, "Using proxy setting", proxy) + } + } catch (e: Exception) { + logger.log(Level.SEVERE, "Can't set proxy, ignoring", e) + } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt index 8186b6953..543dbc938 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/NextcloudLoginFlow.kt @@ -4,7 +4,6 @@ package at.bitfire.davdroid.network -import android.content.Context import at.bitfire.dav4jvm.exception.DavException import at.bitfire.dav4jvm.exception.HttpException import at.bitfire.davdroid.db.Credentials @@ -24,14 +23,15 @@ import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject import java.net.HttpURLConnection import java.net.URI +import javax.inject.Inject /** * Implements Nextcloud Login Flow v2. * * See https://docs.nextcloud.com/server/latest/developer_manual/client_apis/LoginFlow/index.html#login-flow-v2 */ -class NextcloudLoginFlow( - context: Context +class NextcloudLoginFlow @Inject constructor( + httpClientBuilder: HttpClient.Builder ): AutoCloseable { companion object { @@ -42,8 +42,8 @@ class NextcloudLoginFlow( const val DAV_PATH = "remote.php/dav" } - val httpClient = HttpClient.Builder(context) - .setForeground(true) + val httpClient = httpClientBuilder + .inForeground(true) .build() override fun close() { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt index e8c10421a..ef23a4852 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/OAuthModule.kt @@ -26,8 +26,9 @@ object OAuthModule { .setConnectionBuilder { uri -> val url = URL(uri.toString()) (url.openConnection() as HttpURLConnection).apply { - setRequestProperty("User-Agent", HttpClient.UserAgentInterceptor.userAgent) + setRequestProperty("User-Agent", UserAgentInterceptor.userAgent) } }.build() ) + } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt b/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt new file mode 100644 index 000000000..88b979175 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/network/UserAgentInterceptor.kt @@ -0,0 +1,33 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.network + +import android.os.Build +import at.bitfire.davdroid.BuildConfig +import okhttp3.Interceptor +import okhttp3.OkHttp +import okhttp3.Response +import java.util.Locale +import java.util.logging.Logger + +object UserAgentInterceptor: Interceptor { + + val userAgent = "DAVx5/${BuildConfig.VERSION_NAME} (dav4jvm; " + + "okhttp/${OkHttp.VERSION}) Android/${Build.VERSION.RELEASE}" + + init { + Logger.getGlobal().info("Will set User-Agent: $userAgent") + } + + override fun intercept(chain: Interceptor.Chain): Response { + val locale = Locale.getDefault() + val request = chain.request().newBuilder() + .header("User-Agent", userAgent) + .header("Accept-Language", "${locale.language}-${locale.country}, ${locale.language};q=0.7, *;q=0.5") + .build() + return chain.proceed(request) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt index f30f3d0a1..06d311a83 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushRegistrationWorker.kt @@ -23,7 +23,6 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.repository.PreferenceRepository -import at.bitfire.davdroid.settings.AccountSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.runInterruptible @@ -36,6 +35,7 @@ import java.time.Duration import java.time.Instant import java.util.logging.Level import java.util.logging.Logger +import javax.inject.Provider /** * Worker that registers push for all collections that support it. @@ -47,8 +47,8 @@ import java.util.logging.Logger class PushRegistrationWorker @AssistedInject constructor( @Assisted context: Context, @Assisted workerParameters: WorkerParameters, - private val accountSettingsFactory: AccountSettings.Factory, private val collectionRepository: DavCollectionRepository, + private val httpClientBuilder: Provider, private val logger: Logger, private val preferenceRepository: PreferenceRepository, private val serviceRepository: DavServiceRepository @@ -68,13 +68,12 @@ class PushRegistrationWorker @AssistedInject constructor( } private suspend fun registerPushSubscription(collection: Collection, account: Account, endpoint: String) { - val settings = accountSettingsFactory.create(account) - - runInterruptible { - HttpClient.Builder(applicationContext, settings) - .setForeground(true) - .build() - .use { client -> + httpClientBuilder.get() + .fromAccount(account) + .inForeground(true) + .build() + .use { client -> + runInterruptible { val httpClient = client.okHttpClient // requested expiration time: 3 days @@ -116,7 +115,7 @@ class PushRegistrationWorker @AssistedInject constructor( logger.warning("Couldn't register push for ${collection.url}: $response") } } - } + } } private suspend fun registerSyncable() { @@ -150,14 +149,13 @@ class PushRegistrationWorker @AssistedInject constructor( } private suspend fun unregisterPushSubscription(collection: Collection, account: Account, url: HttpUrl) { - val settings = accountSettingsFactory.create(account) - - runInterruptible { - HttpClient.Builder(applicationContext, settings) - .setForeground(true) - .build() - .use { client -> - val httpClient = client.okHttpClient + httpClientBuilder.get() + .fromAccount(account) + .inForeground(true) + .build() + .use { httpClient -> + runInterruptible { + val httpClient = httpClient.okHttpClient try { DavResource(httpClient, url).delete { @@ -174,7 +172,7 @@ class PushRegistrationWorker @AssistedInject constructor( expires = null ) } - } + } } private suspend fun unregisterNotSyncable() { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt index ea78ad57b..7e565e563 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/DavCollectionRepository.kt @@ -27,7 +27,6 @@ import at.bitfire.davdroid.db.CollectionType import at.bitfire.davdroid.db.HomeSet import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils import at.bitfire.ical4android.util.DateUtils import dagger.Lazy @@ -38,7 +37,6 @@ import dagger.hilt.components.SingletonComponent import dagger.multibindings.Multibinds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible -import kotlinx.coroutines.withContext import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Component import net.fortuna.ical4j.model.ComponentList @@ -48,6 +46,7 @@ import java.io.StringWriter import java.util.Collections import java.util.UUID import javax.inject.Inject +import javax.inject.Provider /** * Repository for managing collections. @@ -55,10 +54,10 @@ import javax.inject.Inject * Implements an observer pattern that can be used to listen for changes of collections. */ class DavCollectionRepository @Inject constructor( - private val accountSettingsFactory: AccountSettings.Factory, @ApplicationContext private val context: Context, private val db: AppDatabase, defaultListeners: Lazy>, + private val httpClientBuilder: Provider, private val serviceRepository: DavServiceRepository ) { @@ -177,15 +176,15 @@ class DavCollectionRepository @Inject constructor( val service = serviceRepository.get(collection.serviceId) ?: throw IllegalArgumentException("Service not found") val account = Account(service.accountName, context.getString(R.string.account_type)) - HttpClient.Builder(context, accountSettingsFactory.create(account)) - .setForeground(true) - .build().use { httpClient -> - withContext(Dispatchers.IO) { - runInterruptible { - DavResource(httpClient.okHttpClient, collection.url).delete() { - // success, otherwise an exception would have been thrown → delete locally, too - delete(collection) - } + httpClientBuilder.get() + .fromAccount(account) + .inForeground(true) + .build() + .use { httpClient -> + runInterruptible(Dispatchers.IO) { + DavResource(httpClient.okHttpClient, collection.url).delete { + // success, otherwise an exception would have been thrown → delete locally, too + delete(collection) } } } @@ -291,17 +290,17 @@ class DavCollectionRepository @Inject constructor( // helpers private suspend fun createOnServer(account: Account, url: HttpUrl, method: String, xmlBody: String) { - HttpClient.Builder(context, accountSettingsFactory.create(account)) - .setForeground(true) - .build().use { httpClient -> - withContext(Dispatchers.IO) { - runInterruptible { - DavResource(httpClient.okHttpClient, url).mkCol( - xmlBody = xmlBody, - method = method - ) { - // success, otherwise an exception would have been thrown - } + httpClientBuilder.get() + .fromAccount(account) + .inForeground(true) + .build() + .use { httpClient -> + runInterruptible(Dispatchers.IO) { + DavResource(httpClient.okHttpClient, url).mkCol( + xmlBody = xmlBody, + method = method + ) { + // success, otherwise an exception would have been thrown } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresher.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresher.kt index 9e26f8dd1..07d9edec7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresher.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/CollectionListRefresher.kt @@ -386,8 +386,8 @@ class CollectionListRefresher @AssistedInject constructor( * Before a collection is pre-selected, we check whether its URL matches the regexp in * [Settings.PRESELECT_COLLECTIONS_EXCLUDED], in which case *false* is returned. * - * @param collection the collection to check - * @param homeSets list of home-sets (to check whether collection is in a personal home-set) + * @param collection the collection to check + * @param homeSets list of personal home-sets * @return *true* if the collection should be preselected for synchronization; *false* otherwise */ internal fun shouldPreselect(collection: Collection, homeSets: Iterable): Boolean { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt index 7b5a77119..58a79678f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/DavResourceFinder.kt @@ -48,9 +48,8 @@ import java.util.logging.Level import java.util.logging.Logger /** - * Does initial resource detection (straight after app install). Called after user has supplied url in - * app setup process [at.bitfire.davdroid.ui.setup.DetectConfigurationFragment]. - * It uses the (user given) base URL to find + * Does initial resource detection when an account is added. It uses the (user given) base URL to find + * * - services (CalDAV and/or CardDAV), * - principal, * - homeset/collections (multistatus responses are handled through dav4jvm). @@ -63,7 +62,8 @@ class DavResourceFinder @AssistedInject constructor( @Assisted private val baseURI: URI, @Assisted private val credentials: Credentials? = null, @ApplicationContext val context: Context, - private val dnsRecordResolver: DnsRecordResolver + private val dnsRecordResolver: DnsRecordResolver, + httpClientBuilder: HttpClient.Builder ): AutoCloseable { @AssistedFactory @@ -83,13 +83,17 @@ class DavResourceFinder @AssistedInject constructor( private var encountered401 = false - private val httpClient: HttpClient = HttpClient.Builder(context, logger = log).let { - credentials?.let { credentials -> - it.addAuthentication(null, credentials) - } - it.setForeground(true) - it.build() - } + private val httpClient = httpClientBuilder + .inForeground(true) + .setLogger(log) + .apply { + if (credentials != null) + authenticate( + host = null, + credentials = credentials + ) + } + .build() override fun close() { httpClient.close() @@ -136,7 +140,7 @@ class DavResourceFinder @AssistedInject constructor( log.log(Level.INFO, "CalDAV service detection failed", e) processException(e) } - } catch(e: Exception) { + } catch(_: Exception) { // we have been interrupted; reset results so that an error message will be shown cardDavConfig = null calDavConfig = null diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt index 02b40fe16..8be4e46a6 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt @@ -26,7 +26,6 @@ import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.repository.DavServiceRepository -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationRegistry import at.bitfire.davdroid.ui.account.AccountSettingsActivity @@ -60,8 +59,8 @@ import java.util.logging.Logger class RefreshCollectionsWorker @AssistedInject constructor( @Assisted appContext: Context, @Assisted workerParams: WorkerParameters, - private val accountSettingsFactory: AccountSettings.Factory, private val collectionListRefresherFactory: CollectionListRefresher.Factory, + private val httpClientBuilder: HttpClient.Builder, private val logger: Logger, private val notificationRegistry: NotificationRegistry, serviceRepository: DavServiceRepository @@ -147,11 +146,13 @@ class RefreshCollectionsWorker @AssistedInject constructor( .cancel(serviceId.toString(), NotificationRegistry.NOTIFY_REFRESH_COLLECTIONS) // create authenticating OkHttpClient (credentials taken from account settings) - runInterruptible { - HttpClient.Builder(applicationContext, accountSettingsFactory.create(account)) - .setForeground(true) - .build().use { client -> - val httpClient = client.okHttpClient + httpClientBuilder + .fromAccount(account) + .inForeground(true) + .build() + .use { httpClient -> + runInterruptible { + val httpClient = httpClient.okHttpClient val refresher = collectionListRefresherFactory.create(service, httpClient) // refresh home set list (from principal url) @@ -169,7 +170,7 @@ class RefreshCollectionsWorker @AssistedInject constructor( // Lastly, refresh the principals (collection owners) refresher.refreshPrincipals() } - } + } } catch(e: InvalidAccountException) { logger.log(Level.SEVERE, "Invalid account", e) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt index aabeff7d6..4067d6b57 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AddressBookSyncer.kt @@ -5,6 +5,7 @@ package at.bitfire.davdroid.sync import android.accounts.Account +import android.accounts.AccountManager import android.content.ContentProviderClient import android.provider.ContactsContract import at.bitfire.davdroid.db.Collection @@ -12,6 +13,7 @@ import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.resource.LocalAddressBookStore +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.sync.account.setAndVerifyUserData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +28,7 @@ class AddressBookSyncer @AssistedInject constructor( @Assisted extras: Array, @Assisted syncResult: SyncResult, addressBookStore: LocalAddressBookStore, + private val accountSettingsFactory: AccountSettings.Factory, private val contactsSyncManagerFactory: ContactsSyncManager.Factory ): Syncer(account, extras, syncResult) { @@ -78,11 +81,12 @@ class AddressBookSyncer @AssistedInject constructor( collection: Collection ) { try { - val accountSettings = accountSettingsFactory.create(account) - // handle group method change + val accountSettings = accountSettingsFactory.create(account) val groupMethod = accountSettings.getGroupMethod().name - accountSettings.accountManager.getUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod -> + + val accountManager = AccountManager.get(context) + accountManager.getUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod -> if (previousGroupMethod != groupMethod) { logger.info("Group method changed, deleting all local contacts/groups") @@ -94,9 +98,9 @@ class AddressBookSyncer @AssistedInject constructor( addressBook.syncState = null } } - accountSettings.accountManager.setAndVerifyUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD, groupMethod) + accountManager.setAndVerifyUserData(addressBook.addressBookAccount, PREVIOUS_GROUP_METHOD, groupMethod) - val syncManager = contactsSyncManagerFactory.contactsSyncManager(account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook, collection) + val syncManager = contactsSyncManagerFactory.contactsSyncManager(account, httpClient.value, extras, authority, syncResult, provider, addressBook, collection) syncManager.performSync() } catch(e: Exception) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt index de2a50992..1260e8ee4 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncManager.kt @@ -50,16 +50,15 @@ import java.util.logging.Level */ class CalendarSyncManager @AssistedInject constructor( @Assisted account: Account, - @Assisted accountSettings: AccountSettings, @Assisted extras: Array, @Assisted httpClient: HttpClient, @Assisted authority: String, @Assisted syncResult: SyncResult, @Assisted localCalendar: LocalCalendar, - @Assisted collection: Collection + @Assisted collection: Collection, + private val accountSettingsFactory: AccountSettings.Factory ): SyncManager( account, - accountSettings, httpClient, extras, authority, @@ -72,7 +71,6 @@ class CalendarSyncManager @AssistedInject constructor( interface Factory { fun calendarSyncManager( account: Account, - accountSettings: AccountSettings, extras: Array, httpClient: HttpClient, authority: String, @@ -82,6 +80,9 @@ class CalendarSyncManager @AssistedInject constructor( ): CalendarSyncManager } + private val accountSettings = accountSettingsFactory.create(account) + + override fun prepare(): Boolean { davCollection = DavCalendar(httpClient.okHttpClient, collection.url) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt index e481f49fc..78008108a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/CalendarSyncer.kt @@ -11,6 +11,7 @@ import at.bitfire.davdroid.db.Collection import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.resource.LocalCalendar import at.bitfire.davdroid.resource.LocalCalendarStore +import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.ical4android.AndroidCalendar import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -24,6 +25,7 @@ class CalendarSyncer @AssistedInject constructor( @Assisted extras: Array, @Assisted syncResult: SyncResult, calendarStore: LocalCalendarStore, + private val accountSettingsFactory: AccountSettings.Factory, private val calendarSyncManagerFactory: CalendarSyncManager.Factory ): Syncer(account, extras, syncResult) { @@ -42,6 +44,7 @@ class CalendarSyncer @AssistedInject constructor( override fun prepare(provider: ContentProviderClient): Boolean { // Update colors + val accountSettings = accountSettingsFactory.create(account) if (accountSettings.getEventColors()) AndroidCalendar.insertColors(provider, account) else @@ -57,7 +60,6 @@ class CalendarSyncer @AssistedInject constructor( val syncManager = calendarSyncManagerFactory.calendarSyncManager( account, - accountSettings, extras, httpClient.value, authority, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt index f7c840bcc..5f0107167 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/ContactsSyncManager.kt @@ -95,7 +95,6 @@ import kotlin.jvm.optionals.getOrNull */ class ContactsSyncManager @AssistedInject constructor( @Assisted account: Account, - @Assisted accountSettings: AccountSettings, @Assisted httpClient: HttpClient, @Assisted extras: Array, @Assisted authority: String, @@ -103,10 +102,11 @@ class ContactsSyncManager @AssistedInject constructor( @Assisted val provider: ContentProviderClient, @Assisted localAddressBook: LocalAddressBook, @Assisted collection: Collection, - val dirtyVerifier: Optional + val dirtyVerifier: Optional, + accountSettingsFactory: AccountSettings.Factory, + private val httpClientBuilder: HttpClient.Builder ): SyncManager( account, - accountSettings, httpClient, extras, authority, @@ -119,7 +119,6 @@ class ContactsSyncManager @AssistedInject constructor( interface Factory { fun contactsSyncManager( account: Account, - accountSettings: AccountSettings, httpClient: HttpClient, extras: Array, authority: String, @@ -134,6 +133,8 @@ class ContactsSyncManager @AssistedInject constructor( infix fun Set.disjunct(other: Set) = (this - other) union (other - this) } + private val accountSettings = accountSettingsFactory.create(account) + private var hasVCard4 = false private var hasJCard = false private val groupStrategy = when (accountSettings.getGroupMethod()) { @@ -434,7 +435,7 @@ class ContactsSyncManager @AssistedInject constructor( // downloader helper class private inner class ResourceDownloader( - val baseUrl: HttpUrl + val baseUrl: HttpUrl ): Contact.Downloader { override fun download(url: String, accepts: String): ByteArray? { @@ -445,25 +446,26 @@ class ContactsSyncManager @AssistedInject constructor( } // authenticate only against a certain host, and only upon request - val client = HttpClient.Builder(context, baseUrl.host, accountSettings.credentials()) + httpClientBuilder + .fromAccount(account, onlyHost = baseUrl.host) .followRedirects(true) // allow redirects .build() + .use { httpClient -> + try { + val response = httpClient.okHttpClient.newCall(Request.Builder() + .get() + .url(httpUrl) + .build()).execute() + + if (response.isSuccessful) + return response.body?.bytes() + else + logger.warning("Couldn't download external resource") + } catch(e: IOException) { + logger.log(Level.SEVERE, "Couldn't download external resource", e) + } + } - try { - val response = client.okHttpClient.newCall(Request.Builder() - .get() - .url(httpUrl) - .build()).execute() - - if (response.isSuccessful) - return response.body?.bytes() - else - logger.warning("Couldn't download external resource") - } catch(e: IOException) { - logger.log(Level.SEVERE, "Couldn't download external resource", e) - } finally { - client.close() - } return null } } @@ -471,4 +473,4 @@ class ContactsSyncManager @AssistedInject constructor( override fun notifyInvalidResourceTitle(): String = context.getString(R.string.sync_invalid_contact) -} +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt index ead3ae8df..a6ea58693 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncManager.kt @@ -22,7 +22,6 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalJtxCollection import at.bitfire.davdroid.resource.LocalJtxICalObject import at.bitfire.davdroid.resource.LocalResource -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils.lastSegment import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.ical4android.JtxICalObject @@ -39,7 +38,6 @@ import java.util.logging.Level class JtxSyncManager @AssistedInject constructor( @Assisted account: Account, - @Assisted accountSettings: AccountSettings, @Assisted extras: Array, @Assisted httpClient: HttpClient, @Assisted authority: String, @@ -48,7 +46,6 @@ class JtxSyncManager @AssistedInject constructor( @Assisted collection: Collection ): SyncManager( account, - accountSettings, httpClient, extras, authority, @@ -61,7 +58,6 @@ class JtxSyncManager @AssistedInject constructor( interface Factory { fun jtxSyncManager( account: Account, - accountSettings: AccountSettings, extras: Array, httpClient: HttpClient, authority: String, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt index d798a19d1..21a66ce48 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/JtxSyncer.kt @@ -72,7 +72,6 @@ class JtxSyncer @AssistedInject constructor( val syncManager = jtxSyncManagerFactory.jtxSyncManager( account, - accountSettings, extras, httpClient.value, authority, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt index 78527b8f5..951067b9d 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt @@ -48,7 +48,6 @@ import at.bitfire.davdroid.resource.LocalContact import at.bitfire.davdroid.resource.LocalEvent import at.bitfire.davdroid.resource.LocalResource import at.bitfire.davdroid.resource.LocalTask -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.DebugInfoActivity import at.bitfire.davdroid.ui.NotificationRegistry import at.bitfire.davdroid.ui.account.AccountSettingsActivity @@ -84,7 +83,6 @@ import javax.net.ssl.SSLHandshakeException * @param RemoteType type of remote collection * * @param account account to synchronize - * @param accountSettings account settings of account to synchronize * @param httpClient HTTP client to use for network requests, already authenticated with credentials from [account] * @param extras additional sync parameters * @param authority authority of the content provider the collection shall be synchronized with @@ -94,7 +92,6 @@ import javax.net.ssl.SSLHandshakeException */ abstract class SyncManager, out CollectionType: LocalCollection, RemoteType: DavCollection>( val account: Account, - val accountSettings: AccountSettings, val httpClient: HttpClient, val extras: Array, val authority: String, @@ -388,7 +385,7 @@ abstract class SyncManager, out CollectionType: L try { remote.delete(ifETag = lastETag, ifScheduleTag = lastScheduleTag) {} numDeleted++ - } catch (e: HttpException) { + } catch (_: HttpException) { logger.warning("Couldn't delete $fileName from server; ignoring (may be downloaded again)") } } @@ -587,7 +584,7 @@ abstract class SyncManager, out CollectionType: L toDownload += url if (toDownload.size >= MAX_MULTIGET_RESOURCES || url == null) { - while (toDownload.size > 0) { + while (toDownload.isNotEmpty()) { val bunch = LinkedList() toDownload.drainTo(bunch, MAX_MULTIGET_RESOURCES) launch { @@ -676,7 +673,7 @@ abstract class SyncManager, out CollectionType: L } var syncToken: SyncToken? = null - report.filterIsInstance(SyncToken::class.java).firstOrNull()?.let { + report.filterIsInstance().firstOrNull()?.let { syncToken = it } if (syncToken == null) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt index 801d955df..ccb55fce1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt @@ -17,7 +17,6 @@ import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalCollection import at.bitfire.davdroid.resource.LocalDataStore -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.NotificationRegistry import dagger.hilt.android.qualifiers.ApplicationContext import okhttp3.HttpUrl @@ -63,16 +62,15 @@ abstract class Syncer, CollectionType: abstract val dataStore: StoreType - @Inject - lateinit var accountSettingsFactory: AccountSettings.Factory - - @Inject - @ApplicationContext + @Inject @ApplicationContext lateinit var context: Context @Inject lateinit var collectionRepository: DavCollectionRepository + @Inject + lateinit var httpClientBuilder: HttpClient.Builder + @Inject lateinit var logger: Logger @@ -86,8 +84,9 @@ abstract class Syncer, CollectionType: @ServiceType abstract val serviceType: String - val accountSettings by lazy { accountSettingsFactory.create(account) } - val httpClient = lazy { HttpClient.Builder(context, accountSettings).build() } + val httpClient = lazy { + httpClientBuilder.fromAccount(account).build() + } /** * Creates, updates and/or deletes local collections (calendars, address books, etc) according to @@ -226,9 +225,6 @@ abstract class Syncer, CollectionType: /** * Get the local database collections which are sync-enabled (should by synchronized). * - * [Syncer] will remove collections which are returned by [getLocalCollections], but not by - * this method, and add collections which are returned by this method, but not by [getLocalCollections]. - * * @param serviceId The CalDAV or CardDAV service (account) to be synchronized * @return Database collections to be synchronized */ diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt index 76fee733e..3bf516b6a 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TaskSyncer.kt @@ -73,7 +73,6 @@ class TaskSyncer @AssistedInject constructor( val syncManager = tasksSyncManagerFactory.tasksSyncManager( account, - accountSettings, httpClient.value, extras, authority, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt index 7f49e94cf..494a622d8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksSyncManager.kt @@ -22,7 +22,6 @@ import at.bitfire.davdroid.network.HttpClient import at.bitfire.davdroid.resource.LocalResource import at.bitfire.davdroid.resource.LocalTask import at.bitfire.davdroid.resource.LocalTaskList -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.util.DavUtils.lastSegment import at.bitfire.ical4android.InvalidCalendarException import at.bitfire.ical4android.Task @@ -42,7 +41,6 @@ import java.util.logging.Level */ class TasksSyncManager @AssistedInject constructor( @Assisted account: Account, - @Assisted accountSettings: AccountSettings, @Assisted httpClient: HttpClient, @Assisted extras: Array, @Assisted authority: String, @@ -51,7 +49,6 @@ class TasksSyncManager @AssistedInject constructor( @Assisted collection: Collection ): SyncManager( account, - accountSettings, httpClient, extras, authority, @@ -64,7 +61,6 @@ class TasksSyncManager @AssistedInject constructor( interface Factory { fun tasksSyncManager( account: Account, - accountSettings: AccountSettings, httpClient: HttpClient, extras: Array, authority: String, diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt index 684a61cbb..bee507252 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/setup/NextcloudLoginModel.kt @@ -26,8 +26,8 @@ import java.util.logging.Logger class NextcloudLoginModel @AssistedInject constructor( @Assisted val initialLoginInfo: LoginInfo, @ApplicationContext val context: Context, - private val logger: Logger - //val state: SavedStateHandle + private val logger: Logger, + private val loginFlow: NextcloudLoginFlow ): ViewModel() { @AssistedFactory @@ -90,8 +90,6 @@ class NextcloudLoginModel @AssistedInject constructor( uiState = uiState.copy(baseUrl = baseUrl) } - val loginFlow = NextcloudLoginFlow(context) - // Login flow state /*private var pollUrl: HttpUrl? get() = state.get(STATE_POLL_URL)?.toHttpUrlOrNull() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt index ea562d338..8fa8ba225 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/DavDocumentsProvider.kt @@ -49,10 +49,14 @@ import at.bitfire.davdroid.network.MemoryCookieStore import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity import at.bitfire.davdroid.webdav.DavDocumentsProvider.DavDocumentsActor import at.bitfire.davdroid.webdav.cache.ThumbnailCache +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import dagger.hilt.EntryPoint import dagger.hilt.EntryPoints import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -73,6 +77,7 @@ import java.net.HttpURLConnection import java.util.concurrent.ConcurrentHashMap import java.util.logging.Level import java.util.logging.Logger +import javax.inject.Provider /** * Provides functionality on WebDav documents. @@ -85,6 +90,7 @@ class DavDocumentsProvider: DocumentsProvider() { @InstallIn(SingletonComponent::class) interface DavDocumentsProviderEntryPoint { fun appDatabase(): AppDatabase + fun davDocumentsActorFactory(): DavDocumentsActor.Factory fun logger(): Logger fun randomAccessCallbackWrapperFactory(): RandomAccessCallbackWrapper.Factory fun streamingFileDescriptorFactory(): StreamingFileDescriptor.Factory @@ -135,8 +141,6 @@ class DavDocumentsProvider: DocumentsProvider() { private val mountDao by lazy { db.webDavMountDao() } private val documentDao by lazy { db.webDavDocumentDao() } - private val credentialsStore by lazy { webdavEntryPoint.credentialsStore() } - private val cookieStore by lazy { mutableMapOf() } private val thumbnailCache by lazy { webdavEntryPoint.thumbnailCache() } private val connectivityManager by lazy { ourContext.getSystemService()!! } @@ -149,7 +153,9 @@ class DavDocumentsProvider: DocumentsProvider() { */ private val runningQueryChildren = ConcurrentHashMap() - private val actor by lazy { DavDocumentsActor(ourContext, db, logger, cookieStore, credentialsStore, authority) } + private val credentialsStore by lazy { webdavEntryPoint.credentialsStore() } + private val cookieStore by lazy { mutableMapOf() } + private val actor by lazy { globalEntryPoint.davDocumentsActorFactory().create(cookieStore, credentialsStore) } override fun onCreate() = true @@ -623,14 +629,21 @@ class DavDocumentsProvider: DocumentsProvider() { * to make the methods of [DavDocumentsProvider] more easily testable. * [DavDocumentsProvider]s methods should do nothing more, but to call [DavDocumentsActor]s methods. */ - class DavDocumentsActor( - private val context: Context, + class DavDocumentsActor @AssistedInject constructor( + @Assisted private val cookieStores: MutableMap, + @Assisted private val credentialsStore: CredentialsStore, + @ApplicationContext private val context: Context, private val db: AppDatabase, - private val logger: Logger, - private val cookieStore: MutableMap, - private val credentialsStore: CredentialsStore, - private val authority: String + private val httpClientBuilder: Provider, + private val logger: Logger ) { + + @AssistedFactory + interface Factory { + fun create(cookieStore: MutableMap, credentialsStore: CredentialsStore): DavDocumentsActor + } + + private val authority = context.getString(R.string.webdav_authority) private val documentDao = db.webDavDocumentDao() /** @@ -711,15 +724,14 @@ class DavDocumentsProvider: DocumentsProvider() { * @param logBody whether to log the body of HTTP requests (disable for potentially large files) */ internal fun httpClient(mountId: Long, logBody: Boolean = true): HttpClient { - val builder = HttpClient.Builder( - context = context, - loggerLevel = if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS - ).cookieStore( - cookieStore.getOrPut(mountId) { MemoryCookieStore() } - ) + val builder = httpClientBuilder.get() + .loggerInterceptorLevel(if (logBody) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.HEADERS) + .setCookieStore( + cookieStores.getOrPut(mountId) { MemoryCookieStore() } + ) credentialsStore.getCredentials(mountId)?.let { credentials -> - builder.addAuthentication(null, credentials, true) + builder.authenticate(host = null, credentials = credentials) } return builder.build() diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt index 19b198516..2fcd4bb27 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/RandomAccessCallbackWrapper.kt @@ -12,7 +12,6 @@ import android.os.HandlerThread import android.os.ProxyFileDescriptorCallback import androidx.annotation.RequiresApi import at.bitfire.davdroid.network.HttpClient -import at.bitfire.davdroid.webdav.RandomAccessCallbackWrapper.Companion.TIMEOUT_INTERVAL import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt index ea79a05bf..82661b3e8 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/webdav/WebDavMountRepository.kt @@ -20,10 +20,12 @@ import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.withContext import okhttp3.HttpUrl import javax.inject.Inject +import javax.inject.Provider class WebDavMountRepository @Inject constructor( @ApplicationContext val context: Context, - val db: AppDatabase + val db: AppDatabase, + private val httpClientBuilder: Provider ) { private val mountDao = db.webDavMountDao() @@ -111,22 +113,27 @@ class WebDavMountRepository @Inject constructor( url: HttpUrl, credentials: Credentials? ): Boolean = withContext(Dispatchers.IO) { - var supported = false - val validVersions = arrayOf("1", "2", "3") - HttpClient.Builder(context, null, credentials) - .setForeground(true) - .build() - .use { client -> - val dav = DavResource(client.okHttpClient, url) - runInterruptible { - dav.options { davCapabilities, _ -> - if (davCapabilities.any { it in validVersions }) - supported = true - } + val builder = httpClientBuilder.get() + .inForeground(true) + + if (credentials != null) + builder.authenticate( + host = null, + credentials = credentials + ) + + var supported = false + builder.build().use { httpClient -> + val dav = DavResource(httpClient.okHttpClient, url) + runInterruptible { + dav.options { davCapabilities, _ -> + if (davCapabilities.any { it in validVersions }) + supported = true } } + } supported }