taler-android

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

commit a6bcae6d08832a25162e93e238be8eda95e55875
parent a7bf749c741854c4dc82287852e7d4f1f6feac3e
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Thu,  8 Oct 2026 21:49:31 +0200

[pos] fix HTTP client lifecycle and bump version to 1.6.6

Diffstat:
Mmerchant-lib/src/main/java/net/taler/merchantlib/Response.kt | 3+++
Amerchant-lib/src/test/java/net/taler/merchantlib/ResponseCancellationTest.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Mmerchant-terminal/build.gradle | 4++--
Amerchant-terminal/fastlane/metadata/android/en-US/changelogs/29.txt | 3+++
Mmerchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt | 4++--
Mtaler-kotlin-android/src/main/java/net/taler/common/HttpUtils.kt | 15++++++++-------
Ataler-kotlin-android/src/test/java/net/taler/common/HttpUtilsTest.kt | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 113 insertions(+), 11 deletions(-)

diff --git a/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt b/merchant-lib/src/main/java/net/taler/merchantlib/Response.kt @@ -23,6 +23,7 @@ import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException import java.nio.channels.UnresolvedAddressException +import kotlinx.coroutines.CancellationException import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -36,6 +37,8 @@ class Response<out T> private constructor( suspend fun <T> response(request: suspend () -> T): Response<T> { return try { success(request()) + } catch (e: CancellationException) { + throw e } catch (e: Throwable) { println(e) failure(e) diff --git a/merchant-lib/src/test/java/net/taler/merchantlib/ResponseCancellationTest.kt b/merchant-lib/src/test/java/net/taler/merchantlib/ResponseCancellationTest.kt @@ -0,0 +1,44 @@ +/* + * This file is part of GNU Taler + * (C) 2026 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.merchantlib + +import java.io.IOException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Assert.fail +import org.junit.Test + +class ResponseCancellationTest { + @Test + fun cancellationPropagatesInsteadOfBecomingAnOrderError() = runBlocking { + val cancellation = CancellationException("Parent job is Completed") + try { + Response.response<Unit> { throw cancellation } + fail("Cancellation must propagate to the caller") + } catch (e: CancellationException) { + assertSame(cancellation, e) + } + } + + @Test + fun networkErrorsStillBecomeFailures() = runBlocking { + Response.response<Unit> { throw IOException("Connection lost") }.assertFailure { + assertEquals("Connection lost", it) + } + } +} diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle @@ -13,8 +13,8 @@ android { applicationId "net.taler.merchantpos" minSdkVersion 23 targetSdkVersion 36 - versionCode 28 - versionName "1.6.5" + versionCode 29 + versionName "1.6.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField("String", "BACKEND_API_VERSION", "\"20:0:8\"") diff --git a/merchant-terminal/fastlane/metadata/android/en-US/changelogs/29.txt b/merchant-terminal/fastlane/metadata/android/en-US/changelogs/29.txt @@ -0,0 +1,3 @@ +Bug fixes: +- Fixed order creation failing with "Parent job is Completed" after closing and reopening the app. +- Prevented cancelled requests from appearing as order creation errors. diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/MainViewModel.kt @@ -21,7 +21,7 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import io.ktor.client.statement.* import io.ktor.http.HttpStatusCode -import net.taler.common.getDefaultHttpClient +import net.taler.common.createDefaultHttpClient import net.taler.merchantlib.MerchantApi import net.taler.merchantpos.config.ConfigManager import net.taler.merchantpos.history.HistoryManager @@ -31,7 +31,7 @@ import net.taler.merchantpos.refund.RefundManager class MainViewModel(app: Application) : AndroidViewModel(app) { - private val httpClient = getDefaultHttpClient(followRedirect = true) + private val httpClient = createDefaultHttpClient(followRedirect = true) private val api = MerchantApi(httpClient) val orderManager = OrderManager(app) 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 @@ -35,19 +35,21 @@ import java.util.concurrent.TimeUnit private val clientCache = mutableMapOf<String, HttpClient>() +/** Returns a process-wide shared client. Callers must not close it or install owner-specific callbacks. */ fun getDefaultHttpClient( withJson: Boolean = true, followRedirect: Boolean = false, logging: Boolean = true, ): HttpClient { val key = "$withJson-$followRedirect-$logging" - return clientCache.getOrPut(key) { createClient(withJson, followRedirect, logging) } + return clientCache.getOrPut(key) { createDefaultHttpClient(withJson, followRedirect, logging) } } -private fun createClient( - withJson: Boolean, - followRedirect: Boolean, - logging: Boolean, +/** Creates an independent client that its owner must close when no longer needed. */ +fun createDefaultHttpClient( + withJson: Boolean = true, + followRedirect: Boolean = false, + logging: Boolean = true, ): HttpClient = HttpClient(OkHttp) { expectSuccess = true followRedirects = followRedirect @@ -89,4 +91,4 @@ fun String.toHttpMethod(): HttpMethod? = when(this) { "HEAD" -> HttpMethod.Head "OPTIONS" -> HttpMethod.Options else -> null -} -\ No newline at end of file +} diff --git a/taler-kotlin-android/src/test/java/net/taler/common/HttpUtilsTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/HttpUtilsTest.kt @@ -0,0 +1,51 @@ +/* + * This file is part of GNU Taler + * (C) 2026 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 kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertSame +import org.junit.Assert.assertTrue +import org.junit.Test + +class HttpUtilsTest { + @Test + fun closingOwnerDoesNotCloseSharedClientOrNextOwner() = runBlocking { + val shared = getDefaultHttpClient(followRedirect = true, logging = false) + val first = createDefaultHttpClient(followRedirect = true, logging = false) + try { + assertNotSame(shared, first) + first.close() + first.coroutineContext[Job]!!.join() + + val reopened = createDefaultHttpClient(followRedirect = true, logging = false) + try { + assertNotSame(first, reopened) + assertNotSame(first.engine, reopened.engine) + assertTrue(reopened.coroutineContext[Job]!!.isActive) + assertTrue(reopened.engine.coroutineContext[Job]!!.isActive) + assertTrue(shared.coroutineContext[Job]!!.isActive) + assertSame(shared, getDefaultHttpClient(followRedirect = true, logging = false)) + } finally { + reopened.close() + } + } finally { + first.close() + } + } +}