taler-android

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

commit 29b7cddd08d308fb6458eba962a2dbe0f622e316
parent 933ebfe23ad1a67baa2d2a2a327d7de9db71e508
Author: Iván Ávalos <avalos@disroot.org>
Date:   Mon, 16 Feb 2026 23:09:40 +0100

[wallet] fix #11043 + refactor QR center log

Diffstat:
Mmerchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt | 74++++++--------------------------------------------------------------------
Mtaler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Rmerchant-terminal/src/main/res/drawable/ic_taler_logo_qr.xml -> taler-kotlin-android/src/main/res/drawable/ic_taler_logo_qr.xml | 0
Mwallet/src/main/java/net/taler/wallet/compose/ExpandableCard.kt | 6+++++-
Mwallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt | 19+++++--------------
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt | 10++++++----
Mwallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt | 8++++++--
7 files changed, 111 insertions(+), 101 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 @@ -48,6 +48,7 @@ import net.taler.merchantpos.MainViewModel import net.taler.merchantpos.R import net.taler.merchantpos.databinding.FragmentProcessPaymentBinding import androidx.core.graphics.createBitmap +import net.taler.common.QrCodeManager class ProcessPaymentFragment : Fragment() { @@ -203,81 +204,18 @@ class ProcessPaymentFragment : Fragment() { } private fun makePaymentQrCode(text: String, size: Int): Bitmap { - val qrBitmap = makeQrCode( + return makeQrCode( text = text, size = size, margin = 1, errorCorrection = ErrorCorrectionLevel.H, + centerLogo = ContextCompat.getDrawable( + requireContext(), + R.drawable.ic_taler_logo_qr, + ), ) - val logoDrawable = ContextCompat.getDrawable(requireContext(), R.drawable.ic_taler_logo_qr) - ?: return qrBitmap - return addCenteredLogo(qrBitmap, logoDrawable) } - private fun addCenteredLogo(qrBitmap: Bitmap, logoDrawable: Drawable): Bitmap { - val result = qrBitmap.copy(ARGB_8888, true) - val canvas = Canvas(result) - val logoBitmap = drawableToBitmap(logoDrawable) - var logoMaxWidth = (result.width * 0.30f).toInt() - val logoAspectRatio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat() - var logoWidth = logoMaxWidth - var logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) - var horizontalPadding = (logoHeight * 0.12f).toInt() - var verticalPadding = (logoHeight * 0.09f).toInt() - - val maxOcclusionRatio = 0.11f - val currentOcclusionRatio = - ((logoWidth + horizontalPadding * 2f) * (logoHeight + verticalPadding * 2f)) / - (result.width.toFloat() * result.height.toFloat()) - if (currentOcclusionRatio > maxOcclusionRatio) { - val scale = kotlin.math.sqrt(maxOcclusionRatio / currentOcclusionRatio) - logoMaxWidth = (logoMaxWidth * scale).toInt().coerceAtLeast(1) - logoWidth = logoMaxWidth - logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) - horizontalPadding = (horizontalPadding * scale).toInt() - verticalPadding = (verticalPadding * scale).toInt() - } - - val centerX = result.width / 2 - val centerY = result.height / 2 - val halfBackgroundWidth = (logoWidth / 2f) + horizontalPadding - val halfBackgroundHeight = (logoHeight / 2f) + verticalPadding - val backgroundRect = RectF( - centerX - halfBackgroundWidth, - centerY - halfBackgroundHeight, - centerX + halfBackgroundWidth, - centerY + halfBackgroundHeight, - ) - - val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - style = Paint.Style.FILL - color = android.graphics.Color.WHITE - } - val cornerRadius = halfBackgroundHeight // * 0.8f taler has circle in logo, so it can be fine - canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) - - val destinationRect = Rect( - centerX - logoWidth / 2, - centerY - logoHeight / 2, - centerX + logoWidth / 2, - centerY + logoHeight / 2, - ) - canvas.drawBitmap(logoBitmap, null, destinationRect, Paint(Paint.ANTI_ALIAS_FLAG)) - return result - } - - private fun drawableToBitmap(drawable: Drawable): Bitmap { - if (drawable is BitmapDrawable && drawable.bitmap != null) { - return drawable.bitmap - } - val width = drawable.intrinsicWidth.coerceAtLeast(1) - val height = drawable.intrinsicHeight.coerceAtLeast(1) - val bitmap = createBitmap(width, height) - val canvas = Canvas(bitmap) - drawable.setBounds(0, 0, canvas.width, canvas.height) - drawable.draw(canvas) - return bitmap - } } 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 @@ -17,11 +17,16 @@ package net.taler.common 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.BLACK import android.graphics.Color.WHITE -import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable import androidx.core.graphics.createBitmap import androidx.core.graphics.set import com.google.zxing.BarcodeFormat.QR_CODE @@ -37,7 +42,8 @@ object QrCodeManager { size: Int = 256, margin: Int = 4, errorCorrection: ErrorCorrectionLevel = ErrorCorrectionLevel.M, - centerLogo: ((size: Int) -> Bitmap)? = null, + centerLogo: Drawable? = null, + drawBackground: Boolean = false, ): Bitmap { val qrCodeWriter = QRCodeWriter() val hints = mapOf( @@ -54,19 +60,84 @@ object QrCodeManager { } } - if (centerLogo != null) { - val combined = createBitmap(bmp.width, bmp.height, bmp.config!!) - val canvas = Canvas(combined) - canvas.drawBitmap(bmp, Matrix(), null) + return if (centerLogo != null) { + addCenteredLogo(bmp, centerLogo, drawBackground) + } else { + bmp + } + } + + private fun addCenteredLogo( + qrBitmap: Bitmap, + logoDrawable: Drawable, + drawBackground: Boolean = false, + ): Bitmap { + val result = qrBitmap.copy(ARGB_8888, true) + val canvas = Canvas(result) + val logoBitmap = drawableToBitmap(logoDrawable) - val logo = centerLogo(canvas.width / 6) - val centreX = (canvas.width - logo.width) / 2f - val centreY = (canvas.height - logo.height) / 2f + var logoMaxWidth = (result.width * 0.22f).toInt() + val logoAspectRatio = logoBitmap.width.toFloat() / logoBitmap.height.toFloat() + var logoWidth = logoMaxWidth + var logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) + var horizontalPadding = (logoHeight * 0.12f).toInt() + var verticalPadding = (logoHeight * 0.09f).toInt() - canvas.drawBitmap(logo, centreX, centreY, null) - return combined + val maxOcclusionRatio = 0.11f + val currentOcclusionRatio = + ((logoWidth + horizontalPadding * 2f) * (logoHeight + verticalPadding * 2f)) / + (result.width.toFloat() * result.height.toFloat()) + if (currentOcclusionRatio > maxOcclusionRatio) { + val scale = kotlin.math.sqrt(maxOcclusionRatio / currentOcclusionRatio) + logoMaxWidth = (logoMaxWidth * scale).toInt().coerceAtLeast(1) + logoWidth = logoMaxWidth + logoHeight = (logoWidth / logoAspectRatio).toInt().coerceAtLeast(1) + horizontalPadding = (horizontalPadding * scale).toInt() + verticalPadding = (verticalPadding * scale).toInt() } - return bmp + val centerX = result.width / 2 + val centerY = result.height / 2 + + if (drawBackground) { + val halfBackgroundWidth = (logoWidth / 2f) + horizontalPadding + val halfBackgroundHeight = (logoHeight / 2f) + verticalPadding + val backgroundRect = RectF( + centerX - halfBackgroundWidth, + centerY - halfBackgroundHeight, + centerX + halfBackgroundWidth, + centerY + halfBackgroundHeight, + ) + + val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = WHITE + } + val cornerRadius = + halfBackgroundHeight // * 0.8f taler has circle in logo, so it can be fine + canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint) + } + + val destinationRect = Rect( + centerX - logoWidth / 2, + centerY - logoHeight / 2, + centerX + logoWidth / 2, + centerY + logoHeight / 2, + ) + canvas.drawBitmap(logoBitmap, null, destinationRect, Paint(Paint.ANTI_ALIAS_FLAG)) + return result + } + + private fun drawableToBitmap(drawable: Drawable): Bitmap { + if (drawable is BitmapDrawable && drawable.bitmap != null) { + return drawable.bitmap + } + val width = drawable.intrinsicWidth.coerceAtLeast(1) + val height = drawable.intrinsicHeight.coerceAtLeast(1) + val bitmap = createBitmap(width, height) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bitmap } } diff --git a/merchant-terminal/src/main/res/drawable/ic_taler_logo_qr.xml b/taler-kotlin-android/src/main/res/drawable/ic_taler_logo_qr.xml diff --git a/wallet/src/main/java/net/taler/wallet/compose/ExpandableCard.kt b/wallet/src/main/java/net/taler/wallet/compose/ExpandableCard.kt @@ -43,9 +43,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import net.taler.wallet.R @Composable @@ -141,6 +143,8 @@ fun ExpandableSection( fun ExpandableCardPreview( section: Boolean = false, ) { + val context = LocalContext.current + TalerSurface { var expanded by remember { mutableStateOf(true) } ExpandableCard( @@ -152,7 +156,7 @@ fun ExpandableCardPreview( QrCodeUriComposable( talerUri = "taler://withdraw-exchange", clipBoardLabel = "", - centerLogo = painterResource(R.drawable.ic_swiss_qr), + centerLogo = ContextCompat.getDrawable(context, R.drawable.ic_swiss_qr), showContents = false, ) } diff --git a/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/QrCodeUriComposable.kt @@ -16,6 +16,7 @@ package net.taler.wallet.compose +import android.graphics.drawable.Drawable import androidx.compose.foundation.Image import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement @@ -66,30 +67,20 @@ import kotlin.let fun ColumnScope.QrCodeUriComposable( talerUri: String, clipBoardLabel: String, - centerLogo: Painter? = null, + centerLogo: Drawable? = null, + drawCenterLogoBackground: Boolean = false, buttonText: String = stringResource(R.string.copy), showContents: Boolean = true, shareAsQrCode: Boolean = false, inBetween: (@Composable ColumnScope.() -> Unit)? = null, ) { val qrCodeSize = getQrCodeSize() - val density = LocalDensity.current - val direction = LocalLayoutDirection.current - val qrState = produceState<ImageBitmap?>(null) { value = QrCodeManager.makeQrCode( talerUri, qrCodeSize.value.toInt(), - centerLogo = centerLogo?.let {{ size -> - centerLogo.toImageBitmap( - Size( - size * (centerLogo.intrinsicSize.width / centerLogo.intrinsicSize.height), - size.toFloat(), - ), - density, - direction, - ).asAndroidBitmap() - }}, + centerLogo = centerLogo, + drawBackground = drawCenterLogoBackground, ).asImageBitmap() } diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt @@ -29,11 +29,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import net.taler.common.Amount import net.taler.common.CurrencySpecification import net.taler.common.Timestamp @@ -106,9 +108,8 @@ fun ColumnScope.PeerQrCode( talerUri: String?, instructionResId: Int, ) { - remember(state) { - 10.dp - } + val context = LocalContext.current + if (state == TransactionState(Pending) && state.minor != MergeKycRequired) { Text( modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp), @@ -122,7 +123,8 @@ fun ColumnScope.PeerQrCode( QrCodeUriComposable( talerUri = talerUri, clipBoardLabel = "Push payment", - centerLogo = painterResource(R.drawable.ic_taler_qr), + centerLogo = ContextCompat.getDrawable(context, R.drawable.ic_taler_logo_qr), + drawCenterLogoBackground = true, buttonText = stringResource(id = R.string.copy), ) { Text( diff --git a/wallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt b/wallet/src/main/java/net/taler/wallet/transfer/PaytoQrCard.kt @@ -22,9 +22,11 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import net.taler.wallet.R import net.taler.wallet.compose.ExpandableCard import net.taler.wallet.compose.QrCodeUriComposable @@ -44,6 +46,8 @@ fun PaytoQrCard( else -> return } + val context = LocalContext.current + ExpandableCard( expanded = expanded, setExpanded = setExpanded, @@ -57,9 +61,9 @@ fun PaytoQrCard( showContents = true, shareAsQrCode = true, centerLogo = when (qrCode.type) { - SPC -> painterResource(R.drawable.ic_swiss_qr) + SPC -> ContextCompat.getDrawable(context, R.drawable.ic_swiss_qr) else -> null - } + }, ) Spacer(Modifier.height(8.dp))