taler-android

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

commit afc74f27519cdd3677dead1b5214cba341f4b4ca
parent 5c34c05d7efc86e0ee6a96e158bd874d69556a04
Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sun, 11 May 2025 17:21:48 +0200

[pos] moving to the zxing from mlkit

Diffstat:
Mmerchant-terminal/build.gradle | 4++--
Mmerchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
2 files changed, 64 insertions(+), 23 deletions(-)

diff --git a/merchant-terminal/build.gradle b/merchant-terminal/build.gradle @@ -78,8 +78,8 @@ dependencies { implementation "androidx.camera:camera-lifecycle:1.4.2" implementation "androidx.camera:camera-view:1.4.2" - // ML Kit – on-device barcode/QR detector - implementation "com.google.mlkit:barcode-scanning:17.3.0" + // ZXING core – on-device barcode/QR detector + implementation "com.google.zxing:core:3.5.2" // Navigation implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/config/ConfigFragment.kt @@ -21,6 +21,7 @@ import android.app.TimePickerDialog import android.app.DatePickerDialog import android.content.Intent import android.content.pm.PackageManager +import android.media.Image import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -57,12 +58,10 @@ import net.taler.merchantpos.R import net.taler.merchantpos.config.ConfigFragmentDirections.Companion.actionSettingsToOrder import net.taler.merchantpos.databinding.FragmentMerchantConfigBinding import androidx.core.view.isVisible -import com.google.mlkit.vision.barcode.BarcodeScannerOptions -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.barcode.common.Barcode -import com.google.mlkit.vision.common.InputImage +import com.google.zxing.* import net.taler.merchantpos.MainActivity import android.text.format.DateFormat +import com.google.zxing.common.HybridBinarizer import java.util.Calendar import java.util.Locale @@ -77,18 +76,17 @@ class ConfigFragment : Fragment() { private lateinit var ui: FragmentMerchantConfigBinding - private val scanner by lazy { - BarcodeScanning.getClient( - BarcodeScannerOptions.Builder() - .setBarcodeFormats(Barcode.FORMAT_QR_CODE) - .build() - ) - } - private val cameraExecutor by lazy { ContextCompat.getMainExecutor(requireContext()) } + private val qrReader = MultiFormatReader().apply { + setHints(mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), + DecodeHintType.CHARACTER_SET to "UTF-8" + )) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -359,16 +357,40 @@ class ConfigFragment : Fragment() { .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build().also { useCase -> useCase.setAnalyzer(cameraExecutor) { proxy -> - val media = proxy.image ?: run { proxy.close(); return@setAnalyzer } - val image = InputImage.fromMediaImage( - media, - proxy.imageInfo.rotationDegrees - ) - scanner.process(image) - .addOnSuccessListener { codes -> - codes.firstOrNull()?.rawValue?.let { onQrDecoded(it) } + val mediaImage = proxy.image + if (mediaImage != null) { + // 1) Convert YUV_420_888 to a ZXing-friendly NV21 byte array + val nv21 = yuv420888ToNv21(mediaImage) + val width = mediaImage.width + val height = mediaImage.height + + // 2) Build ZXing’s LuminanceSource + val source = PlanarYUVLuminanceSource( + nv21, width, height, + 0, 0, width, height, + false + ) + + // 3) (Optional) rotate the source if your sensor’s orientation needs it + val rotated = when (proxy.imageInfo.rotationDegrees) { + 90 -> source.rotateCounterClockwise() + 270 -> source.rotateCounterClockwise() + else -> source + } + + // 4) Try to decode + val bitmap = BinaryBitmap(HybridBinarizer(rotated)) + try { + val result = qrReader.decodeWithState(bitmap) + onQrDecoded(result.text) + } catch (e: NotFoundException) { + // no QR code in this frame + } finally { + proxy.close() } - .addOnCompleteListener { proxy.close() } + } else { + proxy.close() + } } } @@ -382,6 +404,25 @@ class ConfigFragment : Fragment() { }, cameraExecutor) } + // 3) convert YUV_420_888 to NV21 + private fun yuv420888ToNv21(image: Image): ByteArray { + val yPlane = image.planes[0].buffer + val uPlane = image.planes[1].buffer + val vPlane = image.planes[2].buffer + + val ySize = yPlane.remaining() + val uSize = uPlane.remaining() + val vSize = vPlane.remaining() + val nv21 = ByteArray(ySize + uSize + vSize) + + // U and V are swapped + yPlane.get(nv21, 0, ySize) + vPlane.get(nv21, ySize, vSize) + uPlane.get(nv21, ySize + vSize, uSize) + + return nv21 + } + private fun onQrDecoded(raw: String) { if (!raw.startsWith("taler-pos://")) return // guard