Implemented decoding EPC QR Code on Android
This commit is contained in:
parent
fbd9c9485a
commit
d47bc46cf8
|
@ -109,6 +109,12 @@ kotlin {
|
|||
implementation(libs.androidx.biometric)
|
||||
|
||||
implementation(libs.favre.bcrypt)
|
||||
|
||||
// for reading EPC QR Codes from camera
|
||||
implementation(libs.zxing.core)
|
||||
implementation(libs.camerax.camera2)
|
||||
implementation(libs.camerax.view)
|
||||
implementation(libs.camerax.lifecycle)
|
||||
}
|
||||
|
||||
iosMain.dependencies {
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.google.zxing.*
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import com.google.zxing.qrcode.QRCodeReader
|
||||
import net.codinux.banking.persistence.AndroidContext
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.log.logger
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
actual object QrCodeService {
|
||||
|
||||
private val cameraExecutor = Executors.newCachedThreadPool()
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
actual val supportsReadingQrCodesFromCamera = true
|
||||
|
||||
@Composable
|
||||
actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) {
|
||||
val context = AndroidContext.mainActivity
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
|
||||
|
||||
val localContext = LocalContext.current
|
||||
log.info { "LocalContext.current = ${localContext.javaClass} ${localContext}" }
|
||||
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
// val context = LocalContext.current
|
||||
val previewView = remember {
|
||||
PreviewView(context)
|
||||
}
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
// Used to bind the lifecycle of cameras to the lifecycle owner
|
||||
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
|
||||
|
||||
// Preview
|
||||
val preview = Preview.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
// Select back camera as a default
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.build()
|
||||
.also {
|
||||
it.setAnalyzer(cameraExecutor, QrCodeImageAnalyzer(resultCallback))
|
||||
}
|
||||
|
||||
try {
|
||||
// Unbind use cases before rebinding
|
||||
cameraProvider.unbindAll()
|
||||
|
||||
// Bind use cases to camera
|
||||
cameraProvider.bindToLifecycle(context as FragmentActivity, cameraSelector, preview, imageAnalyzer)
|
||||
|
||||
} catch(e: Exception) {
|
||||
log.error(e) { "Use case binding failed" }
|
||||
}
|
||||
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
|
||||
|
||||
AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class QrCodeImageAnalyzer(private val resultCallback: (QrCodeReadResult) -> Unit) : ImageAnalysis.Analyzer {
|
||||
|
||||
private val reader = QRCodeReader()
|
||||
|
||||
private val readerHints = readerHintsForCharset(Charsets.UTF_8.name())
|
||||
|
||||
private val log by logger()
|
||||
|
||||
|
||||
override fun analyze(image: ImageProxy) {
|
||||
try {
|
||||
val bitmap = getBinaryBitmap(image)
|
||||
|
||||
val result = reader.decode(bitmap, readerHints)
|
||||
|
||||
if (result != null && result.text != null) {
|
||||
val decodeResult = DI.epcQrCodeService.decode(result.text)
|
||||
if (decodeResult.charset == null || decodeResult.charset == "UTF-8") {
|
||||
this.resultCallback(QrCodeReadResult(result.text))
|
||||
} else { // the charset for receiver name, reference, ... was not UTF-8 -> decode image in EPC QR Code's charset
|
||||
val resultForEncoding = reader.decode(bitmap, readerHintsForCharset(decodeResult.charset))
|
||||
|
||||
this.resultCallback(QrCodeReadResult(resultForEncoding?.text ?: result.text))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
if (e !is NotFoundException) {
|
||||
log.error(e) { "Could not decode image to QR code" }
|
||||
}
|
||||
}
|
||||
|
||||
image.close() // to continue image analysis / avoid blocking production of further images
|
||||
}
|
||||
|
||||
private fun ByteBuffer.toIntArray(): IntArray {
|
||||
val bytes = this.toByteArray()
|
||||
|
||||
val pixels = IntArray(bytes.size)
|
||||
bytes.indices.forEach { index ->
|
||||
pixels[index] = bytes[index].toInt() and 0xFF
|
||||
}
|
||||
|
||||
return pixels
|
||||
}
|
||||
|
||||
private fun ByteBuffer.toByteArray(): ByteArray {
|
||||
rewind() // Rewind the buffer to zero
|
||||
val data = ByteArray(remaining())
|
||||
get(data) // Copy the buffer into a byte array
|
||||
return data // Return the byte array
|
||||
}
|
||||
|
||||
private fun getBinaryBitmap(image: ImageProxy): BinaryBitmap {
|
||||
val buffer = image.planes[0].buffer
|
||||
val bitmapBuffer = buffer.toIntArray()
|
||||
|
||||
val luminanceSource = RGBLuminanceSource(image.width, image.height, bitmapBuffer)
|
||||
return BinaryBitmap(HybridBinarizer(luminanceSource))
|
||||
}
|
||||
|
||||
private fun readerHintsForCharset(charset: String): Map<DecodeHintType, *> = buildMap {
|
||||
// put(DecodeHintType.TRY_HARDER, true) // optimize for accuracy, not speed
|
||||
put(DecodeHintType.CHARACTER_SET, charset)
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,7 @@ import net.codinux.banking.ui.config.Style.FabMenuSpacing
|
|||
import net.codinux.banking.ui.config.Style.FabSize
|
||||
import net.codinux.banking.ui.config.Style.SmallFabSize
|
||||
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
||||
import net.codinux.banking.ui.service.QrCodeService
|
||||
|
||||
|
||||
private val uiState = DI.uiState
|
||||
|
@ -62,6 +63,14 @@ fun FloatingActionMenu(
|
|||
}
|
||||
}
|
||||
|
||||
if (QrCodeService.supportsReadingQrCodesFromCamera) {
|
||||
FloatingActionMenuItem("Überweisung aus QR-Code", "Neue Überweisung mit Daten aus EPC QR Code (GiroCode, scan2Code, Zahlen mit Code, ...)", enabled = accountsThatSupportMoneyTransfer.isNotEmpty()) {
|
||||
handleClick {
|
||||
uiState.showTransferMoneyFromEpcQrCodeScreen.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FloatingActionMenuItem("Überweisung", "Neue Überweisung", enabled = accountsThatSupportMoneyTransfer.isNotEmpty()) {
|
||||
handleClick {
|
||||
uiState.showTransferMoneyDialogData.value = ShowTransferMoneyDialogData()
|
||||
|
|
|
@ -15,6 +15,7 @@ private val formatUtil = DI.formatUtil
|
|||
fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
||||
val showAddAccountDialog by uiState.showAddAccountDialog.collectAsState()
|
||||
val showTransferMoneyDialogData by uiState.showTransferMoneyDialogData.collectAsState()
|
||||
val showTransferMoneyFromEpcQrCodeScreen by uiState.showTransferMoneyFromEpcQrCodeScreen.collectAsState()
|
||||
val showCreateEpcQrCodeScreen by uiState.showCreateEpcQrCodeScreen.collectAsState()
|
||||
|
||||
val showAccountTransactionDetailsScreenForId by uiState.showAccountTransactionDetailsScreenForId.collectAsState()
|
||||
|
@ -39,6 +40,10 @@ fun StateHandler(uiState: UiState, snackbarHostState: SnackbarHostState) {
|
|||
TransferMoneyDialog(data) { uiState.showTransferMoneyDialogData.value = null }
|
||||
}
|
||||
|
||||
if (showTransferMoneyFromEpcQrCodeScreen) {
|
||||
TransferMoneyFromQrCodeScreen { uiState.showTransferMoneyFromEpcQrCodeScreen.value = false }
|
||||
}
|
||||
|
||||
if (showCreateEpcQrCodeScreen) {
|
||||
CreateEpcQrCodeScreen { uiState.showCreateEpcQrCodeScreen.value = false }
|
||||
}
|
||||
|
|
|
@ -13,7 +13,9 @@ object Internationalization {
|
|||
|
||||
const val ErrorTransferMoney = "Überweisung konnte nicht ausgeführt werden"
|
||||
|
||||
const val SaveToDatabase = "Daten konnten nicht in der Datenbank gespeichert werden"
|
||||
const val ErrorReadEpcQrCode = "Überweisungsdaten konnten nicht aus dem QR Code ausgelesen werden"
|
||||
|
||||
const val ErrorSaveToDatabase = "Daten konnten nicht in der Datenbank gespeichert werden"
|
||||
|
||||
const val ErrorBiometricAuthentication = "Biometrische Authentifizierung fehlgeschlagen"
|
||||
|
||||
|
|
|
@ -11,11 +11,10 @@ fun ApplicationErrorDialog(error: ApplicationError, onDismiss: (() -> Unit)? = n
|
|||
ErroneousAction.AddAccount -> Internationalization.ErrorAddAccount
|
||||
ErroneousAction.UpdateAccountTransactions -> Internationalization.ErrorUpdateAccountTransactions
|
||||
ErroneousAction.TransferMoney -> Internationalization.ErrorTransferMoney
|
||||
ErroneousAction.SaveToDatabase -> Internationalization.SaveToDatabase
|
||||
ErroneousAction.ReadEpcQrCode -> Internationalization.ErrorReadEpcQrCode
|
||||
ErroneousAction.SaveToDatabase -> Internationalization.ErrorSaveToDatabase
|
||||
ErroneousAction.BiometricAuthentication -> Internationalization.ErrorBiometricAuthentication
|
||||
}
|
||||
|
||||
// add exception stacktrace?
|
||||
|
||||
ErrorDialog(error.errorMessage, title, error.exception, onDismiss = onDismiss)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package net.codinux.banking.ui.model
|
||||
|
||||
data class DecodeEpcQrCodeResult(
|
||||
val data: ShowTransferMoneyDialogData?,
|
||||
val error: String? = null,
|
||||
val charset: String? = null
|
||||
)
|
|
@ -7,6 +7,8 @@ enum class ErroneousAction {
|
|||
|
||||
TransferMoney,
|
||||
|
||||
ReadEpcQrCode,
|
||||
|
||||
SaveToDatabase,
|
||||
|
||||
BiometricAuthentication
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package net.codinux.banking.ui.screens
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import net.codinux.banking.ui.config.DI
|
||||
import net.codinux.banking.ui.model.error.ErroneousAction
|
||||
import net.codinux.banking.ui.service.QrCodeService
|
||||
|
||||
@Composable
|
||||
fun TransferMoneyFromQrCodeScreen(onClosed: () -> Unit) {
|
||||
|
||||
if (QrCodeService.supportsReadingQrCodesFromCamera) {
|
||||
FullscreenViewBase("Überweisungsdaten aus QR Code lesen", "Abbrechen", onClosed = onClosed) {
|
||||
QrCodeService.readQrCodeFromCamera { result ->
|
||||
onClosed()
|
||||
|
||||
if (result.decodedQrCodeText != null) {
|
||||
val decodingResult = DI.epcQrCodeService.decode(result.decodedQrCodeText)
|
||||
|
||||
if (decodingResult.data != null) {
|
||||
DI.uiState.showTransferMoneyDialogData.value = decodingResult.data
|
||||
} else if (decodingResult.error != null) {
|
||||
DI.uiState.applicationErrorOccurred(ErroneousAction.ReadEpcQrCode, decodingResult.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,36 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import net.codinux.banking.client.model.Amount
|
||||
import net.codinux.banking.epcqrcode.EpcQrCode
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeConfig
|
||||
import net.codinux.banking.epcqrcode.EpcQrCodeGenerator
|
||||
import net.codinux.banking.epcqrcode.parser.EpcQrCodeParser
|
||||
import net.codinux.banking.ui.model.DecodeEpcQrCodeResult
|
||||
import net.codinux.banking.ui.model.ShowTransferMoneyDialogData
|
||||
|
||||
class EpcQrCodeService {
|
||||
|
||||
fun generateEpcQrCode(receiverName: String, iban: String, bic: String?, amount: String?, reference: String?, informationForUser: String? = null, heightAndWidth: Int = EpcQrCode.DefaultHeightAndWidth): ByteArray {
|
||||
val generator = EpcQrCodeGenerator()
|
||||
private val generator = EpcQrCodeGenerator()
|
||||
|
||||
private val parser = EpcQrCodeParser()
|
||||
|
||||
|
||||
fun generateEpcQrCode(receiverName: String, iban: String, bic: String?, amount: String?, reference: String?, informationForUser: String? = null, heightAndWidth: Int = EpcQrCode.DefaultHeightAndWidth): ByteArray {
|
||||
return generator.generateEpcQrCode(EpcQrCodeConfig(receiverName, iban, bic, amount, reference, informationForUser, qrCodeHeightAndWidth = heightAndWidth)).bytes
|
||||
}
|
||||
|
||||
fun decode(decodedQrCodeText: String): DecodeEpcQrCodeResult {
|
||||
val result = parser.parseEpcQrCode(decodedQrCodeText)
|
||||
|
||||
return if (result.epcQrCode != null) {
|
||||
val code = result.epcQrCode!!
|
||||
DecodeEpcQrCodeResult(
|
||||
ShowTransferMoneyDialogData(null, code.receiverName, code.bic, code.iban, code.amount?.let { Amount(it) }, code.remittance),
|
||||
charset = code.coding.charsetName
|
||||
)
|
||||
} else {
|
||||
DecodeEpcQrCodeResult(null, result.error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
expect object QrCodeService {
|
||||
|
||||
val supportsReadingQrCodesFromCamera: Boolean
|
||||
|
||||
@Composable
|
||||
fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit)
|
||||
|
||||
}
|
||||
|
||||
data class QrCodeReadResult(
|
||||
val decodedQrCodeText: String?
|
||||
)
|
|
@ -67,6 +67,8 @@ class UiState : ViewModel() {
|
|||
|
||||
val showTransferMoneyDialogData = MutableStateFlow<ShowTransferMoneyDialogData?>(null)
|
||||
|
||||
val showTransferMoneyFromEpcQrCodeScreen = MutableStateFlow(false)
|
||||
|
||||
val showCreateEpcQrCodeScreen = MutableStateFlow(false)
|
||||
|
||||
val showAccountTransactionDetailsScreenForId = MutableStateFlow<Long?>(null)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
actual object QrCodeService {
|
||||
|
||||
actual val supportsReadingQrCodesFromCamera = false
|
||||
|
||||
@Composable
|
||||
actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
actual object QrCodeService {
|
||||
|
||||
actual val supportsReadingQrCodesFromCamera = false
|
||||
|
||||
@Composable
|
||||
actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package net.codinux.banking.ui.service
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
actual object QrCodeService {
|
||||
|
||||
actual val supportsReadingQrCodesFromCamera = false
|
||||
|
||||
@Composable
|
||||
actual fun readQrCodeFromCamera(resultCallback: (QrCodeReadResult) -> Unit) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,9 @@ androidx-biometric = "1.1.0"
|
|||
androidx-test-junit = "1.2.1"
|
||||
compose-plugin = "1.6.11"
|
||||
|
||||
zxing = "3.5.3"
|
||||
camerax = "1.3.4"
|
||||
|
||||
junit = "4.13.2"
|
||||
|
||||
[libraries]
|
||||
|
@ -69,6 +72,11 @@ androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle
|
|||
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" }
|
||||
kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
|
||||
|
||||
zxing-core = { group = "com.google.zxing", name = "core", version.ref = "zxing" }
|
||||
camerax-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
|
||||
camerax-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
|
||||
camerax-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
|
||||
|
||||
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
|
||||
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||
|
|
Loading…
Reference in New Issue