From 731e619613d7adc99fb8980611074882ad06f76b Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 30 Apr 2026 13:51:06 +0200 Subject: [PATCH 1/2] perf: Avoid initializing HttpClient on the main thread --- .../commonMain/kotlin/AuthenticatorFacade.kt | 2 ++ .../internal/network/ApiClientProvider.kt | 11 +++++++++- .../internal/requests/AuthenticatorRequest.kt | 20 +++++++++---------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt index ca624a23..e18b45a1 100644 --- a/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt +++ b/multiplatform-lib/src/commonMain/kotlin/AuthenticatorFacade.kt @@ -77,6 +77,7 @@ abstract class AuthenticatorFacade internal constructor() { val webAuthnRepository = WebAuthnRepository( authenticatorRequest = AuthenticatorRequest( httpClient = ApiClientProvider( + scope = scope, userAgent = userAgent, routes = routes, crashReport = crashReport, @@ -119,6 +120,7 @@ abstract class AuthenticatorFacade internal constructor() { val webAuthnRepository = WebAuthnRepository( authenticatorRequest = AuthenticatorRequest( httpClient = ApiClientProvider( + scope = scope, userAgent = userAgent, routes = routes, crashReport = crashReport, diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/network/ApiClientProvider.kt b/multiplatform-lib/src/commonMain/kotlin/internal/network/ApiClientProvider.kt index b5962fb7..51995cb9 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/network/ApiClientProvider.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/network/ApiClientProvider.kt @@ -42,12 +42,18 @@ import io.ktor.http.contentLength import io.ktor.http.contentType import io.ktor.serialization.kotlinx.json.json import io.ktor.utils.io.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.async import kotlinx.io.IOException import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds internal class ApiClientProvider( + scope: CoroutineScope, private val userAgent: String, private val routes: ApiRoutes, private val crashReport: CrashReportInterface? = null, @@ -70,7 +76,10 @@ internal class ApiClientProvider( useAlternativeNames = false } - val httpClient = HttpClient(getHttpClientEngine()) { + val httpClient: suspend () -> HttpClient = { httpClientAsync.await() } + private val httpClientAsync = scope.async(Dispatchers.IO, start = CoroutineStart.LAZY) { createHttpClient() } + + private fun createHttpClient() = HttpClient(getHttpClientEngine()) { install(UserAgent) { agent = userAgent } diff --git a/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt b/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt index 70d2caa3..55a17cb7 100644 --- a/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt +++ b/multiplatform-lib/src/commonMain/kotlin/internal/requests/AuthenticatorRequest.kt @@ -37,7 +37,7 @@ import io.ktor.client.request.post import io.ktor.client.request.setBody internal class AuthenticatorRequest( - private val httpClient: HttpClient, + private val httpClient: suspend () -> HttpClient, private val routes: ApiRoutes, ) { @@ -45,7 +45,7 @@ internal class AuthenticatorRequest( * Retrieves options (including a challenge) prior to registering a public key credential with [registerPasskey]. */ suspend fun getPasskeysOptions(token: String): SuccessfulApiResponse { - return httpClient.get(routes.passkeysOptions()) { + return httpClient().get(routes.passkeysOptions()) { addAuthenticationHeader(token) }.decode() } @@ -55,7 +55,7 @@ internal class AuthenticatorRequest( * after [getPasskeysOptions] is done. */ suspend fun registerPasskey(token: String, registerPasskey: RegisterPasskey) { - httpClient.post(routes.registerPasskey()) { + httpClient().post(routes.registerPasskey()) { addAuthenticationHeader(token) setBody(registerPasskey) } @@ -65,7 +65,7 @@ internal class AuthenticatorRequest( * Retrieves the backend-generated challenge, prior to authenticating with [verify]. */ suspend fun challenge(clientId: String): SuccessfulApiResponse { - return httpClient.post(routes.challenge()) { + return httpClient().post(routes.challenge()) { setBody(mapOf("client_id" to clientId)) }.decode() } @@ -78,7 +78,7 @@ internal class AuthenticatorRequest( * @return An [AuthResult] that includes an access token. */ suspend fun verify(verifyAuthenticationData: VerifyAuthenticationData): SuccessfulApiResponse { - return httpClient.post(routes.verify()) { + return httpClient().post(routes.verify()) { setBody(verifyAuthenticationData) }.decode() } @@ -90,7 +90,7 @@ internal class AuthenticatorRequest( * @param passkeyId The id of the passkey to delete. */ suspend fun deletePasskey(token: String, passkeyId: String) { - httpClient.delete(routes.delete(passkeyId)) { + httpClient().delete(routes.delete(passkeyId)) { addAuthenticationHeader(token) } } @@ -102,7 +102,7 @@ internal class AuthenticatorRequest( * @param userId The id of the user. */ suspend fun getMigrationOptions(deviceId: String, userId: Long): SuccessfulApiResponse { - return httpClient.post(routes.migrationsOptions()) { + return httpClient().post(routes.migrationsOptions()) { setBody(mapOf("device" to deviceId, "id" to userId.toString())) }.decode() } @@ -122,7 +122,7 @@ internal class AuthenticatorRequest( sessionId: String, otpPayload: OtpPayload, ): SuccessfulApiResponse { - return httpClient.post(routes.verifyMigration(sessionId)) { + return httpClient().post(routes.verifyMigration(sessionId)) { setBody(otpPayload) }.decode() } @@ -134,7 +134,7 @@ internal class AuthenticatorRequest( * @param deviceId ID of the device. */ suspend fun completeMigration(token: String, sessionId: String, deviceId: String) { - httpClient.delete(routes.finishMigration(sessionId)) { + httpClient().delete(routes.finishMigration(sessionId)) { addAuthenticationHeader(token) setBody(mapOf("device" to deviceId)) } @@ -145,7 +145,7 @@ internal class AuthenticatorRequest( ): SuccessfulApiResponse { val url = "${routes.userProfile()}&with=security" - return httpClient.get(url) { + return httpClient().get(url) { addAuthenticationHeader(token) }.decode() } From 8ecec02760ac630bee107e7f6b4545291a9dfa0f Mon Sep 17 00:00:00 2001 From: Louis CAD Date: Thu, 30 Apr 2026 15:17:08 +0200 Subject: [PATCH 2/2] chore: Workaround StrictMode warning for app settings db --- app/src/main/kotlin/com/infomaniak/auth/MainApplication.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/kotlin/com/infomaniak/auth/MainApplication.kt b/app/src/main/kotlin/com/infomaniak/auth/MainApplication.kt index 02150020..7cc6c531 100644 --- a/app/src/main/kotlin/com/infomaniak/auth/MainApplication.kt +++ b/app/src/main/kotlin/com/infomaniak/auth/MainApplication.kt @@ -24,6 +24,7 @@ import androidx.annotation.RequiresApi import androidx.hilt.work.HiltWorkerFactory import androidx.work.Configuration import com.infomaniak.auth.data.preferences.SentryPreferences +import com.infomaniak.auth.lib.room.appsettings.AppSettingsDatabase import com.infomaniak.auth.service.DeviceInfoUpdateWorker import com.infomaniak.auth.utils.AccountUtils import com.infomaniak.auth.utils.NotificationUtils @@ -48,6 +49,9 @@ open class MainApplication : Application(), Configuration.Provider { @Inject lateinit var notificationUtils: NotificationUtils + @Inject + lateinit var db: AppSettingsDatabase // Workaround to ensure it's initialized eagerly, before StrictMode is activated. + @Inject lateinit var workerFactory: HiltWorkerFactory