diff --git a/app/src/main/java/org/zotero/android/api/WebDavAuthNetworkInterceptor.kt b/app/src/main/java/org/zotero/android/api/WebDavAuthNetworkInterceptor.kt index 72722921..83ce5b20 100644 --- a/app/src/main/java/org/zotero/android/api/WebDavAuthNetworkInterceptor.kt +++ b/app/src/main/java/org/zotero/android/api/WebDavAuthNetworkInterceptor.kt @@ -7,7 +7,7 @@ import okhttp3.Response import org.zotero.android.webdav.WebDavSessionStorage import javax.inject.Inject -class WebDavAuthNetworkInterceptor @Inject constructor( +class WebDavBasicAuthNetworkInterceptor @Inject constructor( private val webDavSessionStorage: WebDavSessionStorage ) : Interceptor { diff --git a/app/src/main/java/org/zotero/android/api/module/ApiInterfacesModule.kt b/app/src/main/java/org/zotero/android/api/module/ApiInterfacesModule.kt index f00fcdd5..ee4d879c 100644 --- a/app/src/main/java/org/zotero/android/api/module/ApiInterfacesModule.kt +++ b/app/src/main/java/org/zotero/android/api/module/ApiInterfacesModule.kt @@ -7,11 +7,9 @@ import org.zotero.android.api.AccountApi import org.zotero.android.api.NoAuthenticationApi import org.zotero.android.api.NoRedirectApi import org.zotero.android.api.SyncApi -import org.zotero.android.api.WebDavApi import org.zotero.android.api.annotations.ForApiWithAuthentication import org.zotero.android.api.annotations.ForApiWithNoRedirects import org.zotero.android.api.annotations.ForBaseApi -import org.zotero.android.api.annotations.ForWebDav import retrofit2.Retrofit import javax.inject.Singleton @@ -40,9 +38,4 @@ object ApiInterfacesModule { return retrofit.create(NoAuthenticationApi::class.java) } - @Provides - @Singleton - fun provideWebDavApi(@ForWebDav retrofit: Retrofit): WebDavApi { - return retrofit.create(WebDavApi::class.java) - } } \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/api/module/WebDavModule.kt b/app/src/main/java/org/zotero/android/api/module/WebDavModule.kt deleted file mode 100644 index 142c4431..00000000 --- a/app/src/main/java/org/zotero/android/api/module/WebDavModule.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.zotero.android.api.module - -import dagger.Module -import dagger.Provides -import dagger.hilt.migration.DisableInstallInCheck -import okhttp3.ConnectionPool -import okhttp3.Dispatcher -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor.Level -import org.zotero.android.api.ClientInfoNetworkInterceptor -import org.zotero.android.api.HttpLoggingInterceptor -import org.zotero.android.api.WebDavAuthNetworkInterceptor -import org.zotero.android.api.annotations.ForWebDav -import org.zotero.android.ktx.setNetworkTimeout -import retrofit2.Retrofit -import java.util.concurrent.TimeUnit -import javax.inject.Singleton - - -@Module -@DisableInstallInCheck -object WebDavModule { - - @Provides - @Singleton - @ForWebDav - fun provideWebDavOkHttpClient( - webDavAuthNetworkInterceptor: WebDavAuthNetworkInterceptor, - clientInfoNetworkInterceptor: ClientInfoNetworkInterceptor, - ): OkHttpClient { - val connectionPool = ConnectionPool( - maxIdleConnections = 10, - keepAliveDuration = 5, - timeUnit = TimeUnit.MINUTES - ) - val dispatcher = Dispatcher() - dispatcher.maxRequests = 30 - dispatcher.maxRequestsPerHost = 30 - - return OkHttpClient.Builder() - .dispatcher(dispatcher) - .connectionPool(connectionPool) - .setNetworkTimeout(15L) - .addInterceptor(webDavAuthNetworkInterceptor) - .addInterceptor(HttpLoggingInterceptor.createInterceptor(Level.BODY)) - .addInterceptor(clientInfoNetworkInterceptor) - .build() - } - - @Provides - @ForWebDav - fun provideWebDavRetrofitBuilder(): Retrofit.Builder { - return Retrofit.Builder() - } - - @Provides - @Singleton - @ForWebDav - fun provideWebDavRetrofit( - @ForWebDav retrofitBuilder: Retrofit.Builder, - @ForWebDav okHttpClient: OkHttpClient - ): Retrofit { - return retrofitBuilder - .baseUrl("https://dummyurl.com") //no-op as all URLs for webdav are absolute - .client(okHttpClient) - .build() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/zotero/android/architecture/di/AppModule.kt b/app/src/main/java/org/zotero/android/architecture/di/AppModule.kt index 4e77c8e3..02f43ce4 100644 --- a/app/src/main/java/org/zotero/android/architecture/di/AppModule.kt +++ b/app/src/main/java/org/zotero/android/architecture/di/AppModule.kt @@ -20,7 +20,6 @@ import org.zotero.android.api.module.ApiWebSocketModule import org.zotero.android.api.module.ApiWithAuthenticationModule import org.zotero.android.api.module.BaseApiModule import org.zotero.android.api.module.GeneralModule -import org.zotero.android.api.module.WebDavModule import org.zotero.android.architecture.SdkInt import org.zotero.android.architecture.app.AppConfig import org.zotero.android.architecture.app.ApplicationIdProvider @@ -40,7 +39,6 @@ import javax.inject.Singleton ApiInterfacesModule::class, ApiWebSocketModule::class, ApiWithAuthenticationModule::class, - WebDavModule::class, ] ) internal class AppModule { diff --git a/app/src/main/java/org/zotero/android/screens/share/backgroundprocessor/BackgroundUploadProcessor.kt b/app/src/main/java/org/zotero/android/screens/share/backgroundprocessor/BackgroundUploadProcessor.kt index b7105a73..64a075c4 100644 --- a/app/src/main/java/org/zotero/android/screens/share/backgroundprocessor/BackgroundUploadProcessor.kt +++ b/app/src/main/java/org/zotero/android/screens/share/backgroundprocessor/BackgroundUploadProcessor.kt @@ -36,7 +36,6 @@ import javax.inject.Singleton @Singleton class BackgroundUploadProcessor @Inject constructor( private val noAuthenticationApi: NoAuthenticationApi, - private val webDavApi: WebDavApi, private val syncApi: SyncApi, private val schemaController: SchemaController, private val dbWrapperMain: DbWrapperMain, @@ -120,7 +119,7 @@ class BackgroundUploadProcessor @Inject constructor( val url = upload.remoteUrl val newUrl = "${url}${upload.key}.zip" val requestBody = createRequestBody(upload.fileUrl) - webDavApi.uploadAttachment(url = newUrl, body = requestBody) + webDavController.uploadAttachment(url = newUrl, body = requestBody) } } } diff --git a/app/src/main/java/org/zotero/android/webdav/WebDavController.kt b/app/src/main/java/org/zotero/android/webdav/WebDavController.kt index 012ae76d..dc4f5ffc 100644 --- a/app/src/main/java/org/zotero/android/webdav/WebDavController.kt +++ b/app/src/main/java/org/zotero/android/webdav/WebDavController.kt @@ -1,14 +1,25 @@ package org.zotero.android.webdav import android.webkit.MimeTypeMap +import com.burgstaller.okhttp.AuthenticationCacheInterceptor +import com.burgstaller.okhttp.digest.CachingAuthenticator +import com.burgstaller.okhttp.digest.Credentials +import com.burgstaller.okhttp.digest.DigestAuthenticator import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.ConnectionPool +import okhttp3.Dispatcher import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.OkHttpClient import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.ResponseBody +import okhttp3.logging.HttpLoggingInterceptor.Level +import org.zotero.android.api.ClientInfoNetworkInterceptor +import org.zotero.android.api.HttpLoggingInterceptor import org.zotero.android.api.WebDavApi +import org.zotero.android.api.WebDavBasicAuthNetworkInterceptor import org.zotero.android.api.network.CustomResult import org.zotero.android.api.network.safeApiCall import org.zotero.android.api.network.safeApiCallSync @@ -17,14 +28,19 @@ import org.zotero.android.database.objects.RCustomLibraryType import org.zotero.android.database.requests.StoreMtimeForAttachmentDbRequest import org.zotero.android.files.FileStore import org.zotero.android.helpers.Zipper +import org.zotero.android.ktx.setNetworkTimeout import org.zotero.android.sync.LibraryIdentifier import org.zotero.android.webdav.data.MetadataResult import org.zotero.android.webdav.data.WebDavDeletionResult import org.zotero.android.webdav.data.WebDavError import org.zotero.android.webdav.data.WebDavUploadResult +import retrofit2.Response +import retrofit2.Retrofit import timber.log.Timber import java.io.File import java.net.URL +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume @@ -34,8 +50,9 @@ import kotlin.coroutines.resume class WebDavController @Inject constructor( private val sessionStorage: WebDavSessionStorage, private val dbWrapperMain: DbWrapperMain, - private val webDavApi: WebDavApi, private val fileStore: FileStore, + private val webDavBasicAuthNetworkInterceptor: WebDavBasicAuthNetworkInterceptor, + private val clientInfoNetworkInterceptor: ClientInfoNetworkInterceptor, ) { private fun update(mtime: Long, key: String): CustomResult { @@ -181,7 +198,7 @@ class WebDavController @Inject constructor( private suspend fun checkIsDav(url: String): CustomResult { val networkResult = safeApiCall { - webDavApi.options(url) + provideWebDavApi().options(url) } if (networkResult !is CustomResult.GeneralSuccess.NetworkSuccess) { return networkResult as CustomResult.GeneralError @@ -204,7 +221,7 @@ class WebDavController @Inject constructor( createUrlResult as CustomResult.GeneralSuccess val url = createUrlResult.value!! val networkResult = safeApiCall { - webDavApi.mkcol(url) + provideWebDavApi().mkcol(url) } if (networkResult !is CustomResult.GeneralSuccess.NetworkSuccess) { @@ -244,7 +261,7 @@ class WebDavController @Inject constructor( val networkResult = safeApiCall { val body: RequestBody = bodyText.toRequestBody() - webDavApi.propfind(url = url, headers = headers, body = body) + provideWebDavApi().propfind(url = url, headers = headers, body = body) } if (networkResult !is CustomResult.GeneralSuccess.NetworkSuccess) { return networkResult as CustomResult.GeneralError @@ -258,7 +275,7 @@ class WebDavController @Inject constructor( private suspend fun checkWhetherReturns404ForMissingFile(url: String): CustomResult { val appendedUrl = "${url}nonexistent.prop" val networkResult = safeApiCall { - webDavApi.get(url = appendedUrl) + provideWebDavApi().get(url = appendedUrl) } if (networkResult is CustomResult.GeneralError.NetworkError) { if (networkResult.httpCode == 404) { @@ -295,7 +312,7 @@ class WebDavController @Inject constructor( private suspend fun webDavDeleteRequest(url: String): CustomResult { val networkResult = safeApiCall { - webDavApi.delete(url = url) + provideWebDavApi().delete(url = url) } if (networkResult is CustomResult.GeneralError.NetworkError) { return networkResult @@ -309,7 +326,7 @@ class WebDavController @Inject constructor( private suspend fun webDavDownloadRequest(url: String): CustomResult { val networkResult = safeApiCall { - webDavApi.get(url = url) + provideWebDavApi().get(url = url) } if (networkResult is CustomResult.GeneralError.NetworkError) { if (networkResult.httpCode == 404) { @@ -329,7 +346,7 @@ class WebDavController @Inject constructor( val networkResult = safeApiCall { val body: RequestBody = RequestBody.create("text/plain".toMediaType(), bodyText); - webDavApi.put(url = url, body = body) + provideWebDavApi().put(url = url, body = body) } if (networkResult is CustomResult.GeneralError.NetworkError) { @@ -373,7 +390,7 @@ class WebDavController @Inject constructor( checkServerIfNeededResult as CustomResult.GeneralSuccess val newUrl = "${checkServerIfNeededResult.value}$key.zip" return safeApiCall { - webDavApi.downloadFile(newUrl) + provideWebDavApi().downloadFile(newUrl) } } @@ -428,7 +445,7 @@ class WebDavController @Inject constructor( private suspend fun metadata(key: String, url: String): CustomResult?> { val newUrl = "${url}${key}.prop" val networkResult = safeApiCall { - webDavApi.get(url = newUrl) + provideWebDavApi().get(url = newUrl) } if (networkResult is CustomResult.GeneralError.NetworkError) { if (networkResult.httpCode == 404) { @@ -463,7 +480,7 @@ class WebDavController @Inject constructor( val newUrl = "${url}${key}.prop" val networkResult = safeApiCall { - webDavApi.delete(url = newUrl) + provideWebDavApi().delete(url = newUrl) } if (networkResult is CustomResult.GeneralError.NetworkError) { return networkResult @@ -603,7 +620,7 @@ class WebDavController @Inject constructor( val newUrl = "${url}${key}.prop" val networkResult = safeApiCall { - webDavApi.uploadProp(url = newUrl, body = data) + provideWebDavApi().uploadProp(url = newUrl, body = data) } if (networkResult is CustomResult.GeneralError.NetworkError) { @@ -620,7 +637,7 @@ class WebDavController @Inject constructor( val requestBody = createRequestBody(file) val networkResult = safeApiCall { - webDavApi.uploadAttachment(url = newUrl, body = requestBody) + provideWebDavApi().uploadAttachment(url = newUrl, body = requestBody) } if (networkResult is CustomResult.GeneralError.NetworkError) { @@ -708,7 +725,7 @@ class WebDavController @Inject constructor( val zipUrl = "${url}${key}.zip" val deleteZipResult = safeApiCallSync { - webDavApi.deleteSync(url = zipUrl) + provideWebDavApi().deleteSync(url = zipUrl) } if (deleteZipResult is CustomResult.GeneralError) { processResult(key, deleteZipResult) @@ -729,7 +746,7 @@ class WebDavController @Inject constructor( ) { val propUrl = "${url}${key}.prop" val deletePropResult = safeApiCallSync { - webDavApi.deleteSync(url = propUrl) + provideWebDavApi().deleteSync(url = propUrl) } if (deletePropResult is CustomResult.GeneralError) { processResult(key, deletePropResult) @@ -743,5 +760,51 @@ class WebDavController @Inject constructor( processResult(key, deletePropResult) } + suspend fun uploadAttachment(url: String, body: RequestBody): Response { + return provideWebDavApi().uploadAttachment(url, body) + } + + private fun provideWebDavOkHttpClient( + ): OkHttpClient { + val connectionPool = ConnectionPool( + maxIdleConnections = 10, + keepAliveDuration = 5, + timeUnit = TimeUnit.MINUTES + ) + val dispatcher = Dispatcher() + dispatcher.maxRequests = 30 + dispatcher.maxRequestsPerHost = 30 + + val authCache: Map = + ConcurrentHashMap() + val username = sessionStorage.username + val password = sessionStorage.password + + return OkHttpClient.Builder() + .dispatcher(dispatcher) + .connectionPool(connectionPool) + .setNetworkTimeout(15L) + .addInterceptor(webDavBasicAuthNetworkInterceptor) + .authenticator(DigestAuthenticator(Credentials(username, password))) + .addInterceptor(AuthenticationCacheInterceptor(authCache)) + .addInterceptor(clientInfoNetworkInterceptor) + .addInterceptor(HttpLoggingInterceptor.createInterceptor(Level.BODY)) + .build() + } + + private fun provideWebDavRetrofit( + ): Retrofit { + val okHttpClient = provideWebDavOkHttpClient() + val retrofitBuilder = Retrofit.Builder() + return retrofitBuilder + .baseUrl("https://dummyurl.com") //no-op as all URLs for webdav are absolute + .client(okHttpClient) + .build() + } + + private fun provideWebDavApi(): WebDavApi { + val retrofit = provideWebDavRetrofit() + return retrofit.create(WebDavApi::class.java) + } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/BuildConfig.kt b/buildSrc/src/main/kotlin/BuildConfig.kt index 03f93baa..a8a82908 100644 --- a/buildSrc/src/main/kotlin/BuildConfig.kt +++ b/buildSrc/src/main/kotlin/BuildConfig.kt @@ -4,7 +4,7 @@ object BuildConfig { const val compileSdkVersion = 34 const val targetSdk = 34 - val versionCode = 92 // Must be updated on every build + val versionCode = 94 // Must be updated on every build val version = Version( major = 1, minor = 0, diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 65fd9224..85e15b5a 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -75,6 +75,7 @@ object Libs { const val converterGson = "com.squareup.retrofit2:converter-gson:$version" const val converterScalars = "com.squareup.retrofit2:converter-scalars:$version" + const val digest = "io.github.rburgst:okhttp-digest:3.1.0" const val kotlinSerialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" } diff --git a/buildSrc/src/main/kotlin/dependencyplugins/NetworkPlugin.kt b/buildSrc/src/main/kotlin/dependencyplugins/NetworkPlugin.kt index fe15c3b0..c0c50b9e 100644 --- a/buildSrc/src/main/kotlin/dependencyplugins/NetworkPlugin.kt +++ b/buildSrc/src/main/kotlin/dependencyplugins/NetworkPlugin.kt @@ -16,6 +16,7 @@ private fun configure(project: Project) { add("implementation", Libs.Retrofit.core) add("implementation", Libs.Retrofit.converterGson) add("implementation", Libs.Retrofit.converterScalars) + add("implementation", Libs.Retrofit.digest) add("implementation", Libs.OkHttp.core) add("implementation", Libs.OkHttp.loggingInterceptor)