taler-android

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

commit cad2f90dc1d990248972bbf3acb18cb8dc198ac6
parent d29862f332a9e9132b4fbf77ba3836d5e59cfbe5
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 12 Jun 2024 11:08:49 -0600

[wallet] Implement native networking

bug 0008920

Diffstat:
Mmerchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt | 20+-------------------
Mtaler-kotlin-android/build.gradle | 8++++++++
Ataler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/backend/BackendManager.kt | 3++-
Awallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 215 insertions(+), 20 deletions(-)

diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt b/merchant-lib/src/main/java/net/taler/merchantlib/MerchantApi.kt @@ -18,8 +18,6 @@ package net.taler.merchantlib import io.ktor.client.HttpClient import io.ktor.client.call.body -import io.ktor.client.engine.okhttp.OkHttp -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.delete import io.ktor.client.request.get @@ -29,11 +27,10 @@ import io.ktor.client.request.setBody import io.ktor.http.ContentType.Application.Json import io.ktor.http.HttpHeaders.Authorization import io.ktor.http.contentType -import io.ktor.serialization.kotlinx.json.json import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json +import net.taler.common.getDefaultHttpClient import net.taler.merchantlib.Response.Companion.response class MerchantApi( @@ -109,18 +106,3 @@ class MerchantApi( header(Authorization, "Bearer ${merchantConfig.apiKey}") } } - -fun getDefaultHttpClient(): HttpClient = HttpClient(OkHttp) { - expectSuccess = true - engine { - config { - retryOnConnectionFailure(true) - } - } - install(ContentNegotiation) { - json(Json { - encodeDefaults = false - ignoreUnknownKeys = true - }) - } -} diff --git a/taler-kotlin-android/build.gradle b/taler-kotlin-android/build.gradle @@ -77,4 +77,12 @@ dependencies { api 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2' testImplementation "junit:junit:$junit_version" testImplementation 'org.json:json:20220320' + + // Networking + api "io.ktor:ktor-client:$ktor_version" + api "io.ktor:ktor-client-okhttp:$ktor_version" + implementation "io.ktor:ktor-client-serialization-jvm:$ktor_version" + implementation "io.ktor:ktor-serialization-kotlinx-json:$ktor_version" + implementation "io.ktor:ktor-client-content-negotiation:$ktor_version" + implementation("io.ktor:ktor-client-logging:$ktor_version") } 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 @@ -0,0 +1,72 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.common + +import io.ktor.client.HttpClient +import io.ktor.client.engine.okhttp.OkHttp +import io.ktor.client.plugins.HttpTimeout +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.ANDROID +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger +import io.ktor.client.plugins.logging.Logging +import io.ktor.http.HttpMethod +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +fun getDefaultHttpClient( + withJson: Boolean = true, + timeoutMs: Long? = null, + followRedirect: Boolean = false, +): HttpClient = HttpClient(OkHttp) { + expectSuccess = true + followRedirects = followRedirect + engine { + config { + retryOnConnectionFailure(true) + } + } + install(ContentNegotiation) { + if (withJson) { + json(Json { + encodeDefaults = false + ignoreUnknownKeys = true + }) + } + } + install(HttpTimeout) { + requestTimeoutMillis = if (timeoutMs != null && timeoutMs > 0) { + timeoutMs + } else { + HttpTimeout.INFINITE_TIMEOUT_MS + } + } + install(Logging) { + logger = Logger.ANDROID + level = LogLevel.HEADERS + } +} + +fun String.toHttpMethod(): HttpMethod? = when(this) { + "GET" -> HttpMethod.Get + "POST" -> HttpMethod.Post + "PUT" -> HttpMethod.Put + "PATCH" -> HttpMethod.Patch + "DELETE" -> HttpMethod.Delete + "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 @@ -47,12 +47,13 @@ class BackendManager( private val walletCore = TalerWalletCore() private val requestManager = RequestManager() + private val networkInterface = NetworkInterface() init { // TODO using Dagger/Hilt and @Singleton would be nice as well if (initialized.getAndSet(true)) error("Already initialized") walletCore.setMessageHandler { onMessageReceived(it) } - walletCore.setCurlHttpClient() + walletCore.setHttpClient(networkInterface) if (BuildConfig.DEBUG) walletCore.setStdoutHandler { Log.d(TAG_CORE, it) } diff --git a/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt b/wallet/src/main/java/net/taler/wallet/backend/NetworkInterface.kt @@ -0,0 +1,130 @@ +/* + * This file is part of GNU Taler + * (C) 2024 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.backend + +import android.util.Log +import io.ktor.client.call.body +import io.ktor.client.plugins.ClientRequestException +import io.ktor.client.plugins.ServerResponseException +import io.ktor.client.request.header +import io.ktor.client.request.headers +import io.ktor.client.request.request +import io.ktor.client.request.setBody +import io.ktor.client.request.url +import io.ktor.util.toMap +import io.ktor.utils.io.errors.IOException +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.serialization.SerializationException +import net.taler.common.getDefaultHttpClient +import net.taler.common.toHttpMethod +import net.taler.qtart.Networking +import net.taler.wallet.TAG +import java.util.concurrent.ConcurrentHashMap + +@OptIn(DelicateCoroutinesApi::class) +class NetworkInterface: Networking.RequestHandler { + private val requests: ConcurrentHashMap<Int, Job> = ConcurrentHashMap() + + override fun handleRequest( + req: Networking.RequestInfo, + id: Int, + sendResponse: (resp: Networking.ResponseInfo) -> Unit + ) { + Log.d(TAG, "HTTP: handleRequest($req, $id") +// if (req.debug) debugHttpRequest(req) + debugHttpRequest(req) + + requests[id] = GlobalScope.launch { + val resp = try { + getDefaultHttpClient( + timeoutMs = req.timeoutMs, + followRedirect = req.redirectMode == Networking.RedirectMode.Transparent, + ).request { + url(req.url) + + method = req.method.toHttpMethod() ?: error("invalid method") + + headers { + req.headers.forEach { + val parts = it.split(':', limit = 2) + if (parts.size == 2) header(parts[0].trim(), parts[1].trim()) + } + } + + if (req.body != null) { + setBody(req.body) + } + } + } catch (e: ClientRequestException) { + Log.d(TAG, e.message) + null + } catch (e: ServerResponseException) { + Log.d(TAG, e.message) + null + } catch (e: IOException) { + Log.d(TAG, e.message ?: "IOException") + null + } catch (e: SerializationException) { + Log.d(TAG, e.message ?: "SerializationException") + null + } ?: return@launch + + // HTTP response status code or 0 on error. + val status = if (resp.status.value in 200 until 300) resp.status.value else 0 + + // When status is 0, error message. + val errorMsg = if (status == 0) "There was an error" else null + + Log.d(TAG, "Sending response to wallet-core") + sendResponse( + Networking.ResponseInfo( + requestId = id, + status = status, + errorMsg = errorMsg, + headers = resp.headers.toMap() + .map { (k, v) -> "$k: $v" } + .toTypedArray(), + body = resp.body(), + ) + ) + } + } + + override fun cancelRequest(id: Int): Boolean { + Log.d(TAG, "HTTP: cancelRequest($id") + requests[id]?.let { job -> + job.cancel() + requests.remove(id) + } + + return true + } + + private fun debugHttpRequest(req: Networking.RequestInfo) { + Log.d(TAG, "HTTP request: body = ${req.body}") + req.headers.forEachIndexed { i, header -> + Log.d(TAG, "HTTP: header[$i] = $header") + } + Log.d(TAG, "HTTP request: method = ${req.method}") + Log.d(TAG, "HTTP request: redirectMode = ${req.redirectMode}") + Log.d(TAG, "HTTP request: timeoutMs = ${req.timeoutMs}") + Log.d(TAG, "HTTP request: url = ${req.url}") + } +} +\ No newline at end of file