taler-android

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

commit af7a85e5834fccb3ad991826f87fc67e717046bf
parent f7f4133493878258740a7d016a5fef801458df95
Author: Iván Ávalos <avalos@disroot.org>
Date:   Tue, 17 Mar 2026 15:05:55 +0100

[shared] improve loading of QR codes

Diffstat:
Mmerchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt | 9++++++---
Mtaler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt | 9+++++----
Mtaler-kotlin-android/src/main/java/net/taler/lib/android/AnimatedQrCodeComposable.kt | 80+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mwallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt | 5+----
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt | 2+-
5 files changed, 59 insertions(+), 46 deletions(-)

diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt @@ -25,15 +25,16 @@ import androidx.activity.OnBackPressedCallback import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import kotlinx.coroutines.launch import net.taler.common.QrCodeManager.makeQrCode import net.taler.common.copyToClipBoard import net.taler.common.fadeIn @@ -197,7 +198,9 @@ class ProcessPaymentFragment : Fragment() { ui.qrcodeView.post { val blockSize = minOf(ui.qrcodeView.width, ui.qrcodeView.height).coerceAtLeast(256) val qrSize = (blockSize * 0.88f).toInt().coerceAtLeast(256) - currentQrBitmap = makePaymentQrCode(text, qrSize) + lifecycleScope.launch { + currentQrBitmap = makePaymentQrCode(text, qrSize) + } ui.qrcodeView.setContent { PosTheme { AnimatedQrCodeComposable( @@ -211,7 +214,7 @@ class ProcessPaymentFragment : Fragment() { } } - private fun makePaymentQrCode(text: String, size: Int): Bitmap { + private suspend fun makePaymentQrCode(text: String, size: Int): Bitmap { return makeQrCode( text = text, size = size, diff --git a/taler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt b/taler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt @@ -20,7 +20,6 @@ import android.graphics.Bitmap import android.graphics.Bitmap.Config.ARGB_8888 import android.graphics.Bitmap.Config.RGB_565 import android.graphics.Canvas -import android.graphics.Color import android.graphics.Color.BLACK import android.graphics.Color.WHITE import android.graphics.Paint @@ -35,6 +34,8 @@ import com.google.zxing.EncodeHintType.ERROR_CORRECTION import com.google.zxing.EncodeHintType.MARGIN import com.google.zxing.qrcode.QRCodeWriter import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext enum class QrLogoSize(val size: Float) { SMALL(0.15f), @@ -44,7 +45,7 @@ enum class QrLogoSize(val size: Float) { object QrCodeManager { - fun makeQrCode( + suspend fun makeQrCode( text: String, size: Int = 256, margin: Int = 2, @@ -55,7 +56,7 @@ object QrCodeManager { darkColor: Int = BLACK, lightColor: Int = WHITE, trimQuietZone: Boolean = false, - ): Bitmap { + ): Bitmap = withContext(Dispatchers.IO) { val qrCodeWriter = QRCodeWriter() val hints = mapOf( MARGIN to margin.coerceAtLeast(0), @@ -73,7 +74,7 @@ object QrCodeManager { val qrBitmap = if (trimQuietZone) trimQrQuietZone(bmp, lightColor) else bmp - return if (centerLogo != null && centerLogoSize != null && drawBackground != null) { + return@withContext if (centerLogo != null && centerLogoSize != null && drawBackground != null) { addCenteredLogo(qrBitmap, centerLogo, centerLogoSize, drawBackground, lightColor) } else { qrBitmap diff --git a/taler-kotlin-android/src/main/java/net/taler/lib/android/AnimatedQrCodeComposable.kt b/taler-kotlin-android/src/main/java/net/taler/lib/android/AnimatedQrCodeComposable.kt @@ -35,25 +35,29 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.nativeCanvas import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex +import androidx.core.graphics.toColorInt import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import net.taler.common.QrCodeManager.makeQrCode @@ -79,8 +83,7 @@ fun AnimatedQrCodeComposable( ), label = "qrStripeAngle", ) - - val stripeColor = MaterialTheme.colorScheme.primary + val stripeColor = Color("#3047a3".toColorInt()) // FIXME: load from light-mode primary val stripeBase = Color.White //MaterialTheme.colorScheme.surfaceVariant val stripeSoft = remember(stripeBase, stripeColor) { lerp(stripeBase, stripeColor, 0.55f) @@ -116,7 +119,7 @@ fun AnimatedQrCodeComposable( contentAlignment = Alignment.Center, ) { val density = LocalDensity.current - var drawSize by remember { mutableStateOf<Dp?>(null) } + var drawSize by remember { mutableStateOf<Dp?>(379.dp) } Box( modifier = Modifier @@ -128,11 +131,13 @@ fun AnimatedQrCodeComposable( ) Canvas( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .onGloballyPositioned { with(density) { + drawSize = it.size.width.toDp() + } }, ) { val s = with(density) { size.width.toDp() } - drawSize = s - val cornerRadius = s * qrCornerRadiusFraction val stripeWidth = s * QR_STRIPE_WIDTH val stripePx = stripeWidth.toPx() @@ -157,35 +162,42 @@ fun AnimatedQrCodeComposable( ) } - val blockSize = drawSize ?: return@Box - val qrSize = blockSize * QR_DATA_SIZE - val logoWidth = blockSize * QR_LOGO_SIZE - val qrSizePx = with(LocalDensity.current) { qrSize.roundToPx().coerceAtLeast(256) } - val qrBitmap = remember(link, qrSizePx) { - makeQrCode( - text = link, - size = qrSizePx, - margin = 0, - errorCorrection = ErrorCorrectionLevel.H, - centerLogo = null, - drawBackground = true, - trimQuietZone = true, - ) - } + drawSize?.let { drawSize -> + val qrSize = drawSize * QR_DATA_SIZE + val qrSizePx = with(density) { + qrSize.roundToPx().coerceAtLeast(256) } + val logoSize = drawSize * QR_LOGO_SIZE - Image( - bitmap = qrBitmap.asImageBitmap(), - contentDescription = null, - contentScale = ContentScale.FillBounds, - modifier = Modifier.size(qrSize).background(Color.White), - ) + val qrBitmap by produceState<ImageBitmap?>(null) { + value = makeQrCode( + text = link, + size = qrSizePx, + margin = 0, + errorCorrection = ErrorCorrectionLevel.H, + centerLogo = null, + drawBackground = true, + trimQuietZone = true, + ).asImageBitmap() + } - if (logoPainter != null) { - Image( - painter = logoPainter, - contentDescription = null, - modifier = Modifier.width(logoWidth), - ) + qrBitmap?.let { qr -> + Image( + bitmap = qr, + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = Modifier + .size(qrSize) + .background(Color.White), + ) + + if (logoPainter != null) { + Image( + painter = logoPainter, + contentDescription = null, + modifier = Modifier.width(logoSize), + ) + } + } } } } diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt @@ -98,10 +98,7 @@ fun ColumnScope.QrCodeUriComposable( is QrCodeParams.Custom -> params.centerLogoSize }, - drawBackground = when (params) { - QrCodeParams.Taler -> true - is QrCodeParams.Custom -> false - } + drawBackground = false, ) } diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt @@ -105,7 +105,7 @@ fun ColumnScope.PeerQrCode( ) { if (state == TransactionState(Pending) && state.minor != KycRequired) { Text( - modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp), + modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 8.dp), style = MaterialTheme.typography.bodyLarge, text = stringResource(id = instructionResId, amount.toString()), textAlign = TextAlign.Center,