forked from dsutanto/bChot-android
First Commit
This commit is contained in:
39
libraries/network/build.gradle.kts
Normal file
39
libraries/network/build.gradle.kts
Normal file
@@ -0,0 +1,39 @@
|
||||
import extension.setupDependencyInjection
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.network"
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupDependencyInjection()
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.di)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(platform(libs.network.okhttp.bom))
|
||||
implementation(libs.network.okhttp)
|
||||
implementation(libs.network.okhttp.logging)
|
||||
implementation(platform(libs.network.retrofit.bom))
|
||||
implementation(libs.network.retrofit)
|
||||
implementation(libs.network.retrofit.converter.serialization)
|
||||
implementation(libs.serialization.json)
|
||||
}
|
||||
9
libraries/network/consumer-rules.pro
Normal file
9
libraries/network/consumer-rules.pro
Normal file
@@ -0,0 +1,9 @@
|
||||
# From https://github.com/square/retrofit/issues/3751#issuecomment-1192043644
|
||||
# Keep generic signature of Call, Response (R8 full mode strips signatures from non-kept items).
|
||||
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
|
||||
-keep,allowobfuscation,allowshrinking class retrofit2.Response
|
||||
|
||||
# With R8 full mode generic signatures are stripped for classes that are not
|
||||
# kept. Suspend functions are wrapped in continuations where the type argument
|
||||
# is used.
|
||||
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.BindingContainer
|
||||
import dev.zacsweers.metro.ContributesTo
|
||||
import dev.zacsweers.metro.Provides
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.network.interceptors.DynamicHttpLoggingInterceptor
|
||||
import io.element.android.libraries.network.interceptors.FormattedJsonHttpLogger
|
||||
import io.element.android.libraries.network.interceptors.UserAgentInterceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@BindingContainer
|
||||
@ContributesTo(AppScope::class)
|
||||
object NetworkModule {
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesOkHttpClient(
|
||||
userAgentInterceptor: UserAgentInterceptor,
|
||||
dynamicHttpLoggingInterceptor: DynamicHttpLoggingInterceptor,
|
||||
): OkHttpClient = OkHttpClient.Builder().apply {
|
||||
connectTimeout(30, TimeUnit.SECONDS)
|
||||
readTimeout(60, TimeUnit.SECONDS)
|
||||
writeTimeout(60, TimeUnit.SECONDS)
|
||||
addInterceptor(userAgentInterceptor)
|
||||
addInterceptor(dynamicHttpLoggingInterceptor)
|
||||
}.build()
|
||||
|
||||
@Provides
|
||||
@SingleIn(AppScope::class)
|
||||
fun providesHttpLoggingInterceptor(): HttpLoggingInterceptor {
|
||||
val logger = FormattedJsonHttpLogger(HttpLoggingInterceptor.Level.BODY)
|
||||
return HttpLoggingInterceptor(logger)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.Provider
|
||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||
import io.element.android.libraries.core.uri.ensureTrailingSlash
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
@Inject
|
||||
class RetrofitFactory(
|
||||
private val okHttpClient: Provider<OkHttpClient>,
|
||||
private val json: Provider<JsonProvider>,
|
||||
) {
|
||||
fun create(baseUrl: String): Retrofit = Retrofit.Builder()
|
||||
.baseUrl(baseUrl.ensureTrailingSlash())
|
||||
.addConverterFactory(json()().asConverterFactory("application/json".toMediaType()))
|
||||
.callFactory { request -> okHttpClient().newCall(request) }
|
||||
.build()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.headers
|
||||
|
||||
@Suppress("ktlint:standard:property-naming")
|
||||
internal object HttpHeaders {
|
||||
const val Authorization = "Authorization"
|
||||
const val UserAgent = "User-Agent"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.interceptors
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.Inject
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor.Level
|
||||
|
||||
/**
|
||||
* HTTP logging interceptor that decides whether to display the HTTP logs or not based on the current log level.
|
||||
*/
|
||||
@Inject
|
||||
@SingleIn(AppScope::class)
|
||||
class DynamicHttpLoggingInterceptor(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val loggingInterceptor: HttpLoggingInterceptor,
|
||||
) : Interceptor by loggingInterceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
// This is called in a separate thread, so calling `runBlocking` here should be fine, it should be also instant after the value is cached
|
||||
val logLevel = runBlocking { appPreferencesStore.getTracingLogLevelFlow().first() }
|
||||
loggingInterceptor.level = if (logLevel >= LogLevel.DEBUG) Level.BODY else Level.NONE
|
||||
return loggingInterceptor.intercept(chain)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.interceptors
|
||||
|
||||
import io.element.android.libraries.core.extensions.ellipsize
|
||||
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.d(message.ellipsize(200_000))
|
||||
|
||||
// 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.length > 100_000) {
|
||||
Timber.d("Content is too long (${message.length} chars) to be formatted as JSON")
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.interceptors
|
||||
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.libraries.network.headers.HttpHeaders
|
||||
import io.element.android.libraries.network.useragent.UserAgentProvider
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
@Inject
|
||||
class UserAgentInterceptor(
|
||||
private val userAgentProvider: UserAgentProvider,
|
||||
) : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val newRequest = chain.request()
|
||||
.newBuilder()
|
||||
.header(HttpHeaders.UserAgent, userAgentProvider.provide())
|
||||
.build()
|
||||
return chain.proceed(newRequest)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.useragent
|
||||
|
||||
import android.os.Build
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.SingleIn
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.matrix.api.SdkMetadata
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultUserAgentProvider(
|
||||
private val buildMeta: BuildMeta,
|
||||
private val sdkMeta: SdkMetadata,
|
||||
) : UserAgentProvider {
|
||||
private val userAgent: String by lazy { buildUserAgent() }
|
||||
|
||||
override fun provide(): String = userAgent
|
||||
|
||||
/**
|
||||
* Create an user agent with the application version.
|
||||
* Ex: Element X/1.5.0 (Xiaomi Mi 9T; Android 11; RKQ1.200826.002; Sdk c344b155c)
|
||||
*/
|
||||
private fun buildUserAgent(): String {
|
||||
val appName = buildMeta.applicationName
|
||||
val appVersion = buildMeta.versionName
|
||||
val deviceManufacturer = Build.MANUFACTURER
|
||||
val deviceModel = Build.MODEL
|
||||
val androidVersion = Build.VERSION.RELEASE
|
||||
val deviceBuildId = Build.DISPLAY
|
||||
val matrixSdkVersion = sdkMeta.sdkGitSha
|
||||
|
||||
return buildString {
|
||||
append(appName)
|
||||
append("/")
|
||||
append(appVersion)
|
||||
append(" (")
|
||||
append(deviceManufacturer)
|
||||
append(" ")
|
||||
append(deviceModel)
|
||||
append("; ")
|
||||
append("Android ")
|
||||
append(androidVersion)
|
||||
append("; ")
|
||||
append(deviceBuildId)
|
||||
append("; ")
|
||||
append("Sdk ")
|
||||
append(matrixSdkVersion)
|
||||
append(")")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.useragent
|
||||
|
||||
class SimpleUserAgentProvider(
|
||||
private val userAgent: String = "User agent"
|
||||
) : UserAgentProvider {
|
||||
override fun provide(): String = userAgent
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.network.useragent
|
||||
|
||||
interface UserAgentProvider {
|
||||
fun provide(): String
|
||||
}
|
||||
Reference in New Issue
Block a user