First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View 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)
}

View 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

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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"
}

View File

@@ -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)
}
}

View File

@@ -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) }
}
}

View File

@@ -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)
}
}

View File

@@ -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(")")
}
}
}

View File

@@ -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
}

View File

@@ -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
}