taler-android

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

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:
Mmerchant-terminal/build.gradle | 4++--
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigManager.kt | 2+-
Amerchant-terminal/src/main/java/net/taler/merchantpos/payment/AnimatedQrBorderView.kt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmerchant-terminal/src/main/java/net/taler/merchantpos/payment/ProcessPaymentFragment.kt | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Amerchant-terminal/src/main/res/drawable/ic_taler_logo_qr.xml | 34++++++++++++++++++++++++++++++++++
Mmerchant-terminal/src/main/res/layout/fragment_process_payment.xml | 52++++++++++++++++++++++++++++++++++++++++++++++------
Mtaler-kotlin-android/src/main/java/net/taler/common/QrCodeManager.kt | 16++++++++++++++--
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)