taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit 5a50c1764be2f885469db3816853ae0beea7a8f1
parent f65a976c0d4a2bb838e973eecd019ed446a4784d
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 16 Jul 2026 16:57:14 +0200

[common] reuse HTTP client across requests

Diffstat:
Mtaler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt | 25++++++++++++++++++-------
Mwallet/src/main/java/net/taler/wallet/backend/BackendManager.kt | 9++++++++-
Mwallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt | 29++++++++++++++++++++++-------
3 files changed, 48 insertions(+), 15 deletions(-)

diff --git a/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt @@ -29,18 +29,33 @@ import io.ktor.client.plugins.logging.Logging import io.ktor.http.HttpMethod import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json +import okhttp3.ConnectionPool +import okhttp3.Protocol +import java.util.concurrent.TimeUnit + +private val clientCache = mutableMapOf<String, HttpClient>() fun getDefaultHttpClient( withJson: Boolean = true, - timeoutMs: Long? = null, followRedirect: Boolean = false, logging: Boolean = true, +): HttpClient { + val key = "$withJson-$followRedirect-$logging" + return clientCache.getOrPut(key) { createClient(withJson, followRedirect, logging) } +} + +private fun createClient( + withJson: Boolean, + followRedirect: Boolean, + logging: Boolean, ): HttpClient = HttpClient(OkHttp) { expectSuccess = true followRedirects = followRedirect engine { config { retryOnConnectionFailure(true) + connectionPool(ConnectionPool(5, 30, TimeUnit.SECONDS)) + protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1)) } } install(ContentNegotiation) { @@ -52,12 +67,7 @@ fun getDefaultHttpClient( } } install(HttpTimeout) { - requestTimeoutMillis = if (timeoutMs != null && timeoutMs > 0) { - timeoutMs - } else { - INFINITE_TIMEOUT_MS - } - + requestTimeoutMillis = INFINITE_TIMEOUT_MS socketTimeoutMillis = INFINITE_TIMEOUT_MS connectTimeoutMillis = INFINITE_TIMEOUT_MS } @@ -76,6 +86,7 @@ fun String.toHttpMethod(): HttpMethod? = when(this) { "PUT" -> HttpMethod.Put "PATCH" -> HttpMethod.Patch "DELETE" -> HttpMethod.Delete + "HEAD" -> HttpMethod.Head "OPTIONS" -> HttpMethod.Options else -> null } \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt b/wallet/src/main/java/net/taler/wallet/backend/BackendManager.kt @@ -17,6 +17,11 @@ package net.taler.wallet.backend import android.util.Log +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import net.taler.qtart.TalerWalletCore import net.taler.wallet.BuildConfig @@ -48,6 +53,7 @@ class BackendManager( private val walletCore = TalerWalletCore() private val requestManager = RequestManager() private val networkInterface = NetworkInterface() + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) fun run() { if (!initialized.getAndSet(true)) { @@ -62,6 +68,7 @@ class BackendManager( fun destroy() { if (initialized.getAndSet(false)) { + scope.cancel() walletCore.destroy() } } @@ -79,7 +86,7 @@ class BackendManager( } } - private fun onMessageReceived(msg: String) { + private fun onMessageReceived(msg: String) = scope.launch { Log.d(TAG, "message received: $msg") when (val message = json.decodeFromString<ApiMessage>(msg)) { is ApiMessage.Notification -> { diff --git a/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt b/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt @@ -18,6 +18,7 @@ package net.taler.wallet.backend import android.util.Log import io.ktor.client.call.body +import io.ktor.client.plugins.* import io.ktor.client.plugins.ResponseException import io.ktor.client.request.header import io.ktor.client.request.headers @@ -41,6 +42,14 @@ import java.util.concurrent.ConcurrentHashMap class NetworkInterface: Networking.RequestHandler { private val requests: ConcurrentHashMap<Int, Job> = ConcurrentHashMap() + private val sharedClient by lazy { + getDefaultHttpClient(followRedirect = false, logging = false) + } + + private val redirectClient by lazy { + getDefaultHttpClient(followRedirect = true, logging = false) + } + override fun handleRequest( req: Networking.RequestInfo, id: Int, @@ -49,16 +58,15 @@ class NetworkInterface: Networking.RequestHandler { Log.d(TAG, "HTTP: handleRequest($req, $id") requests[id] = GlobalScope.launch { - val client = getDefaultHttpClient( - timeoutMs = req.timeoutMs, - followRedirect = req.redirectMode == Networking.RedirectMode.Transparent, - logging = req.debug, - ) + val client = if (req.redirectMode == Networking.RedirectMode.Transparent) { + redirectClient + } else { + sharedClient + } var errorMsg: String? = null val resp = try { - // TODO: reuse the same client for every request client.request { url(req.url) @@ -74,6 +82,14 @@ class NetworkInterface: Networking.RequestHandler { if (req.body != null) { setBody(req.body) } + + timeout { + val t = req.timeoutMs + if (t > 0) { + requestTimeoutMillis = t + socketTimeoutMillis = t + } + } } } catch (e: ResponseException) { e.response // send non-200 responses to wallet-core anyway @@ -87,7 +103,6 @@ class NetworkInterface: Networking.RequestHandler { null } finally { cleanupRequest(id) - client.close() } // HTTP response status code or 0 on error.