diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a7d44ac510..effa886c4d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -219,6 +219,9 @@ dependencies { implementation(libs.androidx.startup) implementation(libs.coil) + implementation(platform(libs.network.okhttp.bom)) + implementation("com.squareup.okhttp3:logging-interceptor") + implementation(libs.dagger) kapt(libs.dagger.compiler) diff --git a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt index 2621045c83..c553f3f7f0 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppModule.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppModule.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.plus +import okhttp3.logging.HttpLoggingInterceptor import java.io.File import java.util.concurrent.Executors @@ -64,6 +65,7 @@ object AppModule { gitBranchName = "TODO", // BuildConfig.GIT_BRANCH_NAME, flavorDescription = "TODO", // BuildConfig.FLAVOR_DESCRIPTION, flavorShortDescription = "TODO", // BuildConfig.SHORT_FLAVOR_DESCRIPTION, + okHttpLoggingLevel = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC, ) @Provides diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1616b878e..68b8dbc8af 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -92,6 +92,11 @@ accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayo # Libraries squareup_seismic = "com.squareup:seismic:1.0.3" +# network +network_okhttp_bom = "com.squareup.okhttp3:okhttp-bom:4.10.0" +network_retrofit = "com.squareup.retrofit2:retrofit:2.9.0" +network_retrofit_converter_serialization = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" + # Test test_core = { module = "androidx.test:core", version.ref = "test_core" } test_corektx = { module = "androidx.test:core-ktx", version.ref = "test_core" } diff --git a/libraries/core/build.gradle.kts b/libraries/core/build.gradle.kts index ef4a882cb3..7a576b356b 100644 --- a/libraries/core/build.gradle.kts +++ b/libraries/core/build.gradle.kts @@ -1,4 +1,3 @@ - /* * Copyright (c) 2022 New Vector Ltd * @@ -29,4 +28,6 @@ java { dependencies { implementation(libs.coroutines.core) + implementation(platform(libs.network.okhttp.bom)) + implementation("com.squareup.okhttp3:logging-interceptor") } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt index 35deb30dc0..8fefe19919 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/meta/BuildMeta.kt @@ -16,15 +16,18 @@ package io.element.android.libraries.core.meta +import okhttp3.logging.HttpLoggingInterceptor + data class BuildMeta( - val isDebug: Boolean, - val applicationName: String, - val applicationId: String, - val lowPrivacyLoggingEnabled: Boolean, - val versionName: String, - val gitRevision: String, - val gitRevisionDate: String, - val gitBranchName: String, - val flavorDescription: String, - val flavorShortDescription: String, + val isDebug: Boolean, + val applicationName: String, + val applicationId: String, + val lowPrivacyLoggingEnabled: Boolean, + val versionName: String, + val gitRevision: String, + val gitRevisionDate: String, + val gitBranchName: String, + val flavorDescription: String, + val flavorShortDescription: String, + val okHttpLoggingLevel: HttpLoggingInterceptor.Level, ) diff --git a/libraries/network/build.gradle.kts b/libraries/network/build.gradle.kts new file mode 100644 index 0000000000..fb242d4f83 --- /dev/null +++ b/libraries/network/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.network" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(platform(libs.network.okhttp.bom)) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + + implementation(libs.network.retrofit) + implementation(libs.network.retrofit.converter.serialization) + implementation(libs.serialization.json) +} diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt new file mode 100644 index 0000000000..69c1d78369 --- /dev/null +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/NetworkModule.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.network + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import okhttp3.OkHttpClient +import okhttp3.Protocol +import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger +import java.util.concurrent.TimeUnit +import okhttp3.logging.HttpLoggingInterceptor + +@Module +@ContributesTo(AppScope::class) +object NetworkModule { + + @Provides + @JvmStatic + fun providesHttpLoggingInterceptor(buildMeta: BuildMeta): HttpLoggingInterceptor { + val logger = FormattedJsonHttpLogger(buildMeta.okHttpLoggingLevel) + val interceptor = HttpLoggingInterceptor(logger) + interceptor.level = buildMeta.okHttpLoggingLevel + return interceptor + } + + @Provides + @SingleIn(AppScope::class) + fun providesOkHttpClient( + httpLoggingInterceptor: HttpLoggingInterceptor, + ): OkHttpClient { + return OkHttpClient.Builder() + // workaround for #4669 + .protocols(listOf(Protocol.HTTP_1_1)) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .addInterceptor(httpLoggingInterceptor) + .build() + } +} diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt new file mode 100644 index 0000000000..cba09525f9 --- /dev/null +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/RetrofitFactory.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.network + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import io.element.android.libraries.core.uri.ensureTrailingSlash +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import javax.inject.Inject + +class RetrofitFactory @Inject constructor( + private val okHttpClient: OkHttpClient, +) { + fun create(baseUrl: String): Retrofit { + val contentType = "application/json".toMediaType() + return Retrofit.Builder() + .baseUrl(baseUrl.ensureTrailingSlash()) + .addConverterFactory(Json.asConverterFactory(contentType)) + .client(okHttpClient) + .build() + } +} diff --git a/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt new file mode 100644 index 0000000000..d6c3144c5c --- /dev/null +++ b/libraries/network/src/main/kotlin/io/element/android/libraries/network/interceptors/FormattedJsonHttpLogger.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.network.interceptors + +import okhttp3.logging.HttpLoggingInterceptor +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber + +internal class FormattedJsonHttpLogger( + private val level: HttpLoggingInterceptor.Level +) : HttpLoggingInterceptor.Logger { + + companion object { + private const val INDENT_SPACE = 2 + } + + /** + * Log the message and try to log it again as a JSON formatted string. + * Note: it can consume a lot of memory but it is only in DEBUG mode. + * + * @param message + */ + @Synchronized + override fun log(message: String) { + Timber.v(message) + + // Try to log formatted Json only if there is a chance that [message] contains Json. + // It can be only the case if we log the bodies of Http requests. + if (level != HttpLoggingInterceptor.Level.BODY) return + + if (message.startsWith("{")) { + // JSON Detected + try { + val o = JSONObject(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally this is not a JSON string... + Timber.e(e) + } + } else if (message.startsWith("[")) { + // JSON Array detected + try { + val o = JSONArray(message) + logJson(o.toString(INDENT_SPACE)) + } catch (e: JSONException) { + // Finally not JSON... + Timber.e(e) + } + } + // Else not a json string to log + } + + private fun logJson(formattedJson: String) { + formattedJson + .lines() + .dropLastWhile { it.isEmpty() } + .forEach { Timber.v(it) } + } +} diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 426be4fcdb..3d134bcab4 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -55,6 +55,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix:impl")) implementation(project(":libraries:matrixui")) + implementation(project(":libraries:network")) implementation(project(":libraries:core")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) diff --git a/settings.gradle.kts b/settings.gradle.kts index e28b354cf3..7a534a5778 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ include(":libraries:dateformatter:api") include(":libraries:dateformatter:impl") include(":libraries:dateformatter:test") include(":libraries:elementresources") +include(":libraries:network") include(":libraries:ui-strings") include(":libraries:testtags") include(":libraries:designsystem")