commit b626d88fa3adb0bf21049b6ba4e9cf9993df7198
parent 906c046bd7e562532709f8e94ecd732ca91a81b8
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Wed, 11 Feb 2026 18:47:15 +0100
[merchant-terminal] bohdan doing anything but prepare for exams or work on dolibarr
Diffstat:
7 files changed, 398 insertions(+), 23 deletions(-)
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 19
- versionName "1.3.1"
+ versionCode 20
+ versionName "1.3.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField("String", "BACKEND_API_VERSION", "\"20:0:8\"")
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt
@@ -91,7 +91,7 @@ internal const val OLD_CONFIG_PASSWORD_DEMO = ""
private const val SETTINGS_MERCHANT_URL = "merchantUrl"
private const val SETTINGS_ACCESS_TOKEN = "accessToken"
-internal const val NEW_CONFIG_URL_DEMO = "https://backend.demo.taler.net"
+internal const val NEW_CONFIG_URL_DEMO = "https://my.taler-ops.ch"
private val VERSION = Version.parse(BuildConfig.BACKEND_API_VERSION)!!
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/payment/AnimatedQrBorderView.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/payment/AnimatedQrBorderView.kt
@@ -0,0 +1,127 @@
+package net.taler.merchantpos.payment
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.RectF
+import android.graphics.SweepGradient
+import android.util.AttributeSet
+import android.view.View
+import android.view.animation.LinearInterpolator
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.ColorUtils
+import net.taler.merchantpos.R
+import androidx.core.view.isVisible
+
+/**
+ * Draws a rounded border around the QR code with two animated gradient lines.
+ */
+class AnimatedQrBorderView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+) : View(context, attrs, defStyleAttr) {
+
+ private val strokeWidthPx = 6f * resources.displayMetrics.density
+ private val cornerRadiusPx = 20f * resources.displayMetrics.density
+ private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+ style = Paint.Style.STROKE
+ strokeWidth = strokeWidthPx
+ }
+ private val borderRect = RectF()
+ private val gradientMatrix = Matrix()
+
+ private var gradient: SweepGradient? = null
+ private var rotationAngle = 0f
+
+ private val animator = ValueAnimator.ofFloat(0f, 360f).apply {
+ duration = 5250L
+ repeatCount = ValueAnimator.INFINITE
+ interpolator = LinearInterpolator()
+ addUpdateListener {
+ rotationAngle = it.animatedValue as Float
+ invalidate()
+ }
+ }
+
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ if (isVisible && !animator.isStarted) animator.start()
+ }
+
+ override fun onDetachedFromWindow() {
+ animator.cancel()
+ super.onDetachedFromWindow()
+ }
+
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
+ super.onVisibilityChanged(changedView, visibility)
+ if (visibility == VISIBLE) {
+ if (!animator.isStarted) animator.start()
+ } else {
+ animator.cancel()
+ }
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ val halfStroke = strokeWidthPx / 2f
+ borderRect.set(
+ halfStroke,
+ halfStroke,
+ w.toFloat() - halfStroke,
+ h.toFloat() - halfStroke,
+ )
+ gradient = createGradient(w / 2f, h / 2f)
+ strokePaint.shader = gradient
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val shader = gradient ?: return
+ gradientMatrix.reset()
+ gradientMatrix.setRotate(rotationAngle, width / 2f, height / 2f)
+ shader.setLocalMatrix(gradientMatrix)
+ canvas.drawRoundRect(borderRect, cornerRadiusPx, cornerRadiusPx, strokePaint)
+ }
+
+ private fun createGradient(cx: Float, cy: Float): SweepGradient {
+ val accent = ContextCompat.getColor(context, R.color.colorPrimary)
+ val background = ContextCompat.getColor(context, R.color.colorSurface)
+ val softAccent = ColorUtils.blendARGB(background, accent, 0.55f)
+ return SweepGradient(
+ cx,
+ cy,
+ intArrayOf(
+ background,
+ background,
+ softAccent,
+ accent,
+ softAccent,
+ background,
+ background,
+ softAccent,
+ accent,
+ softAccent,
+ background,
+ background,
+ ),
+ floatArrayOf(
+ 0.00f,
+ 0.05f,
+ 0.09f,
+ 0.12f,
+ 0.16f,
+ 0.21f,
+ 0.50f,
+ 0.55f,
+ 0.59f,
+ 0.62f,
+ 0.66f,
+ 1.00f,
+ ),
+ )
+ }
+}
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
@@ -16,27 +16,38 @@
package net.taler.merchantpos.payment
+import android.graphics.Bitmap
+import android.graphics.Bitmap.Config.ARGB_8888
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.RectF
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
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 com.google.android.material.snackbar.BaseTransientBottomBar.LENGTH_LONG
import com.google.android.material.snackbar.Snackbar
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import net.taler.common.QrCodeManager.makeQrCode
import net.taler.common.copyToClipBoard
import net.taler.common.fadeIn
import net.taler.common.fadeOut
-import net.taler.common.navigate
import net.taler.common.shareText
import net.taler.common.showError
import net.taler.lib.android.TalerNfcService.Companion.hasNfc
import net.taler.merchantpos.MainViewModel
import net.taler.merchantpos.R
import net.taler.merchantpos.databinding.FragmentProcessPaymentBinding
+import androidx.core.graphics.createBitmap
class ProcessPaymentFragment : Fragment() {
@@ -44,6 +55,9 @@ class ProcessPaymentFragment : Fragment() {
private val paymentManager by lazy { model.paymentManager }
private lateinit var ui: FragmentProcessPaymentBinding
+ private lateinit var qrPreviewBackCallback: OnBackPressedCallback
+ private var currentPayUri: String? = null
+ private var deviceHasNfc: Boolean = false
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
@@ -54,12 +68,29 @@ class ProcessPaymentFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- val introRes =
- if (hasNfc(requireContext())) R.string.payment_intro_nfc else R.string.payment_intro
- ui.payIntroView.setText(introRes)
+ deviceHasNfc = hasNfc(requireContext())
+ ui.payIntroView.setText(R.string.payment_intro)
+ // Show only a simple loader before the first QR bitmap is rendered.
+ ui.qrcodeLayout.visibility = View.INVISIBLE
+ ui.qrcodeView.visibility = View.INVISIBLE
+ ui.progressBar.visibility = View.VISIBLE
+ ui.shareButton.isEnabled = false
+ ui.copyButton.isEnabled = false
paymentManager.payment.observe(viewLifecycleOwner) { payment ->
onPaymentStateChanged(payment)
}
+ ui.qrcodeView.setOnClickListener {
+ showQrPreview()
+ }
+ ui.qrPreviewOverlay.setOnClickListener {
+ hideQrPreview()
+ }
+ qrPreviewBackCallback = object : OnBackPressedCallback(false) {
+ override fun handleOnBackPressed() {
+ hideQrPreview()
+ }
+ }
+ requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, qrPreviewBackCallback)
ui.cancelPaymentButton.setOnClickListener {
onPaymentCancel()
}
@@ -71,6 +102,14 @@ class ProcessPaymentFragment : Fragment() {
}
private fun onPaymentStateChanged(payment: Payment) {
+ val previewShouldClose =
+ payment.error != null ||
+ payment.paid ||
+ payment.claimed ||
+ (currentPayUri != null && payment.talerPayUri != currentPayUri)
+ if (previewShouldClose) {
+ hideQrPreview()
+ }
if (payment.error != null) {
requireActivity().showError(R.string.error_payment, payment.error)
findNavController().navigateUp()
@@ -92,16 +131,39 @@ class ProcessPaymentFragment : Fragment() {
ui.qrcodeLayout.fadeOut()
ui.payIntroView.setText(R.string.payment_claimed)
} else {
- payment.talerPayUri?.let {
- ui.qrcodeView.setImageBitmap(makeQrCode(it))
- ui.shareButton.setOnClickListener { _ ->
- requireContext().shareText(it)
+ val introRes =
+ if (deviceHasNfc && payment.talerPayUri != null) {
+ R.string.payment_intro_nfc
+ } else {
+ R.string.payment_intro
}
- ui.copyButton.setOnClickListener { _ ->
- copyToClipBoard(requireContext(), "Payment URI", it)
+ ui.payIntroView.setText(introRes)
+ payment.talerPayUri?.let {
+ val uriChanged = it != currentPayUri
+ if (uriChanged) {
+ currentPayUri = it
+ renderPaymentQrCode(it) {
+ ui.qrcodeView.visibility = View.VISIBLE
+ if (ui.qrcodeLayout.visibility != View.VISIBLE) {
+ ui.qrcodeLayout.fadeIn()
+ }
+ ui.progressBar.fadeOut()
+ }
+ ui.shareButton.setOnClickListener { _ ->
+ requireContext().shareText(it)
+ }
+ ui.copyButton.setOnClickListener { _ ->
+ copyToClipBoard(requireContext(), "Payment URI", it)
+ }
+ } else {
+ if (ui.qrcodeLayout.visibility != View.VISIBLE) {
+ ui.qrcodeLayout.fadeIn()
+ }
+ ui.qrcodeView.visibility = View.VISIBLE
+ ui.progressBar.fadeOut()
}
- ui.qrcodeLayout.fadeIn()
- ui.progressBar.fadeOut()
+ ui.shareButton.isEnabled = true
+ ui.copyButton.isEnabled = true
}
}
ui.payIntroView.fadeIn()
@@ -118,4 +180,104 @@ class ProcessPaymentFragment : Fragment() {
Snackbar.make(requireView(), R.string.payment_canceled, LENGTH_LONG).show()
}
+ private fun showQrPreview() {
+ val qrBitmap = (ui.qrcodeView.drawable as? BitmapDrawable)?.bitmap ?: return
+ ui.qrPreviewImage.setImageBitmap(qrBitmap)
+ ui.qrPreviewOverlay.visibility = View.VISIBLE
+ qrPreviewBackCallback.isEnabled = true
+ }
+
+ private fun hideQrPreview() {
+ if (ui.qrPreviewOverlay.visibility != View.VISIBLE) return
+ ui.qrPreviewOverlay.visibility = View.GONE
+ ui.qrPreviewImage.setImageDrawable(null)
+ qrPreviewBackCallback.isEnabled = false
+ }
+
+ private fun renderPaymentQrCode(text: String, onRendered: (() -> Unit)? = null) {
+ ui.qrcodeView.post {
+ val qrSize = minOf(ui.qrcodeView.width, ui.qrcodeView.height).coerceAtLeast(256)
+ ui.qrcodeView.setImageBitmap(makePaymentQrCode(text, qrSize))
+ onRendered?.invoke()
+ }
+ }
+
+ private fun makePaymentQrCode(text: String, size: Int): Bitmap {
+ val qrBitmap = makeQrCode(
+ text = text,
+ size = size,
+ margin = 1,
+ errorCorrection = ErrorCorrectionLevel.H,
+ )
+ 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/merchant-terminal/src/main/res/drawable/ic_taler_logo_qr.xml b/merchant-terminal/src/main/res/drawable/ic_taler_logo_qr.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="201dp"
+ android:height="90dp"
+ android:viewportWidth="201"
+ android:viewportHeight="90">
+ <path
+ android:fillColor="#0042B3"
+ android:fillType="evenOdd"
+ android:pathData="M86.662,1.121C102.252,1.121,115.791,10.522,122.623,24.323L116.806,24.323C110.487,13.623,99.349,6.518,86.662,6.518C66.972,6.518,51.009,23.63,51.009,44.739C51.009,55.07,54.835,64.443,61.049,71.322C59.706,72.443,58.277,73.45,56.773,74.328C50.071,66.553,45.975,56.16,45.975,44.739C45.975,20.649,64.191,1.121,86.662,1.121ZM122.514,65.376C115.648,79.056,102.169,88.356,86.662,88.356C85.609,88.356,84.566,88.313,83.533,88.229C86.586,86.668,89.447,84.749,92.072,82.522C102.393,80.837,111.258,74.408,116.674,65.376Z" />
+ <path
+ android:fillColor="#0042B3"
+ android:fillType="evenOdd"
+ android:pathData="M64.212,1.121C65.265,1.121,66.308,1.164,67.341,1.248C64.289,2.809,61.427,4.729,58.803,6.956C41.68,9.75,28.559,25.602,28.559,44.739C28.559,59.003,35.85,71.441,46.653,78.008C45.06,78.275,43.426,78.415,41.763,78.415C40.523,78.415,39.301,78.335,38.099,78.185C29.191,70.184,23.525,58.172,23.525,44.739C23.525,20.649,41.741,1.121,64.212,1.121ZM69.622,82.522C79.943,80.837,88.808,74.408,94.224,65.375L100.065,65.375C93.198,79.056,79.719,88.356,64.212,88.356C63.16,88.356,62.116,88.313,61.084,88.229C64.136,86.668,66.998,84.749,69.622,82.522ZM94.356,24.323C91.216,19.008,86.888,14.58,81.771,11.47C83.365,11.203,84.998,11.063,86.662,11.063C87.902,11.063,89.124,11.142,90.326,11.292C94.342,14.899,97.699,19.322,100.175,24.323Z" />
+ <path
+ android:fillColor="#0042B3"
+ android:fillType="evenOdd"
+ android:pathData="M41.763,1.121C42.827,1.121,43.881,1.166,44.925,1.251C41.879,2.81,39.022,4.726,36.402,6.948C19.255,9.721,6.11,25.583,6.11,44.739C6.11,65.847,22.072,82.959,41.763,82.959C54.362,82.959,65.435,75.952,71.776,65.375L77.615,65.375C70.748,79.056,57.269,88.356,41.763,88.356C19.292,88.356,1.075,68.828,1.075,44.739C1.075,20.649,19.292,1.121,41.763,1.121ZM71.905,24.323C70.593,22.102,69.074,20.036,67.376,18.156C68.719,17.036,70.148,16.028,71.652,15.149C74.025,17.902,76.071,20.984,77.724,24.323Z" />
+ <path
+ android:fillColor="#121212"
+ android:pathData="M76.135,34.409L85.296,34.409L85.296,29.366L61.858,29.366L61.858,34.409L71.019,34.409L71.019,60.332L76.135,60.332Z" />
+ <path
+ android:fillColor="#121212"
+ android:pathData="M92.648,52.856L106.307,52.856L109.237,60.332L114.601,60.332L101.891,29.145L97.187,29.145L84.477,60.332L89.677,60.332ZM104.45,48.034L94.505,48.034L99.457,35.648Z" />
+ <path
+ android:fillColor="#121212"
+ android:pathData="M123.806,29.366L119.226,29.366L119.226,60.332L139.773,60.332L139.773,55.422L123.806,55.422Z" />
+ <path
+ android:fillColor="#121212"
+ android:pathData="M166.472,29.366L145.097,29.366L145.097,60.332L166.679,60.332L166.679,55.422L150.131,55.422L150.131,47.149L164.615,47.149L164.615,42.239L150.131,42.239L150.131,34.276L166.472,34.276Z" />
+ <path
+ android:fillColor="#121212"
+ android:pathData="M191.19,39.475C191.19,41.074,190.654,42.35,189.574,43.293C188.501,44.245,187.05,44.716,185.227,44.716L177.779,44.716L177.779,34.276L185.186,34.276C187.091,34.276,188.57,34.711,189.615,35.589C190.668,36.459,191.19,37.756,191.19,39.475ZM197.256,60.332L189.457,48.609C190.475,48.314,191.404,47.894,192.243,47.349C193.082,46.803,193.804,46.139,194.409,45.358C195.014,44.576,195.489,43.677,195.833,42.66C196.177,41.642,196.348,40.484,196.348,39.187C196.348,37.683,196.101,36.319,195.606,35.095C195.111,33.871,194.402,32.839,193.481,31.998C192.559,31.158,191.431,30.509,190.097,30.052C188.763,29.594,187.27,29.366,185.62,29.366L172.744,29.366L172.744,60.332L177.779,60.332L177.779,49.539L184.154,49.539L191.273,60.332Z" />
+</vector>
diff --git a/merchant-terminal/src/main/res/layout/fragment_process_payment.xml b/merchant-terminal/src/main/res/layout/fragment_process_payment.xml
@@ -33,17 +33,31 @@
android:visibility="invisible"
tools:visibility="visible">
- <ImageView
- android:id="@+id/qrcodeView"
- android:layout_width="match_parent"
+ <FrameLayout
+ android:id="@+id/qrcodeContainer"
+ android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="12dp"
+ app:layout_constraintDimensionRatio="1:1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@id/shareButton"
- tools:ignore="ContentDescription"
- tools:src="@tools:sample/avatars" />
+ app:layout_constraintBottom_toTopOf="@id/shareButton">
+
+ <ImageView
+ android:id="@+id/qrcodeView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_margin="6dp"
+ tools:ignore="ContentDescription"
+ tools:src="@tools:sample/avatars" />
+
+ <net.taler.merchantpos.payment.AnimatedQrBorderView
+ android:id="@+id/qrAnimatedBorder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ </FrameLayout>
<Button
android:id="@+id/shareButton"
@@ -70,6 +84,7 @@
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:indeterminateTint="@color/colorPrimary"
app:layout_constraintBottom_toBottomOf="@+id/qrcodeLayout"
app:layout_constraintEnd_toEndOf="@+id/qrcodeLayout"
app:layout_constraintStart_toStartOf="@+id/qrcodeLayout"
@@ -130,10 +145,35 @@
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:backgroundTint="@color/red"
+ android:textColor="?attr/colorOnError"
android:text="@string/payment_cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guideline" />
+ <FrameLayout
+ android:id="@+id/qrPreviewOverlay"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:background="#CC000000"
+ android:clickable="true"
+ android:focusable="true"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ImageView
+ android:id="@+id/qrPreviewImage"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:layout_margin="24dp"
+ android:adjustViewBounds="true"
+ android:scaleType="fitCenter"
+ tools:src="@tools:sample/avatars" />
+ </FrameLayout>
+
</androidx.constraintlayout.widget.ConstraintLayout>
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
@@ -21,13 +21,25 @@ import android.graphics.Bitmap.Config.RGB_565
import android.graphics.Color.BLACK
import android.graphics.Color.WHITE
import com.google.zxing.BarcodeFormat.QR_CODE
+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
object QrCodeManager {
- fun makeQrCode(text: String, size: Int = 256): Bitmap {
+ fun makeQrCode(
+ text: String,
+ size: Int = 256,
+ margin: Int = 4,
+ errorCorrection: ErrorCorrectionLevel = ErrorCorrectionLevel.M,
+ ): Bitmap {
val qrCodeWriter = QRCodeWriter()
- val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size)
+ val hints = mapOf(
+ MARGIN to margin.coerceAtLeast(0),
+ ERROR_CORRECTION to errorCorrection,
+ )
+ val bitMatrix = qrCodeWriter.encode(text, QR_CODE, size, size, hints)
val height = bitMatrix.height
val width = bitMatrix.width
val bmp = Bitmap.createBitmap(width, height, RGB_565)