diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts
index ce1063c..e99224f 100644
--- a/composeApp/build.gradle.kts
+++ b/composeApp/build.gradle.kts
@@ -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 {
diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index e2bd569..14039c8 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -4,6 +4,9 @@
+
+
+
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 = buildMap {
+// put(DecodeHintType.TRY_HARDER, true) // optimize for accuracy, not speed
+ put(DecodeHintType.CHARACTER_SET, charset)
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt
index 3fd0fc8..eca754d 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/appskeleton/FloatingActionMenu.kt
@@ -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()
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt
index 3ff4206..47f2798 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/composables/StateHandler.kt
@@ -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 }
}
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt
index 159a684..72b740d 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/config/Internationalization.kt
@@ -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"
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ApplicationErrorDialog.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ApplicationErrorDialog.kt
index 44b9f49..32ac52e 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ApplicationErrorDialog.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/dialogs/ApplicationErrorDialog.kt
@@ -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)
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/DecodeEpcQrCodeResult.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/DecodeEpcQrCodeResult.kt
new file mode 100644
index 0000000..d19c703
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/DecodeEpcQrCodeResult.kt
@@ -0,0 +1,7 @@
+package net.codinux.banking.ui.model
+
+data class DecodeEpcQrCodeResult(
+ val data: ShowTransferMoneyDialogData?,
+ val error: String? = null,
+ val charset: String? = null
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/error/ErroneousAction.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/error/ErroneousAction.kt
index 2bf4454..ae81ef9 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/error/ErroneousAction.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/model/error/ErroneousAction.kt
@@ -7,6 +7,8 @@ enum class ErroneousAction {
TransferMoney,
+ ReadEpcQrCode,
+
SaveToDatabase,
BiometricAuthentication
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/TransferMoneyFromQrCodeScreen.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/TransferMoneyFromQrCodeScreen.kt
new file mode 100644
index 0000000..cedbae0
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/screens/TransferMoneyFromQrCodeScreen.kt
@@ -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)
+ }
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/EpcQrCodeService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/EpcQrCodeService.kt
index 467e20a..b6ecd96 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/EpcQrCodeService.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/EpcQrCodeService.kt
@@ -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)
+ }
+ }
+
}
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/QrCodeService.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/QrCodeService.kt
new file mode 100644
index 0000000..2a2ac07
--- /dev/null
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/service/QrCodeService.kt
@@ -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?
+)
\ No newline at end of file
diff --git a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt
index e2fe157..cb3d74a 100644
--- a/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt
+++ b/composeApp/src/commonMain/kotlin/net/codinux/banking/ui/state/UiState.kt
@@ -67,6 +67,8 @@ class UiState : ViewModel() {
val showTransferMoneyDialogData = MutableStateFlow(null)
+ val showTransferMoneyFromEpcQrCodeScreen = MutableStateFlow(false)
+
val showCreateEpcQrCodeScreen = MutableStateFlow(false)
val showAccountTransactionDetailsScreenForId = MutableStateFlow(null)
diff --git a/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/service/QrCodeService.desktop.kt b/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/service/QrCodeService.desktop.kt
new file mode 100644
index 0000000..59104b1
--- /dev/null
+++ b/composeApp/src/desktopMain/kotlin/net/codinux/banking/ui/service/QrCodeService.desktop.kt
@@ -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) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/QrCodeService.ios.kt b/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/QrCodeService.ios.kt
new file mode 100644
index 0000000..59104b1
--- /dev/null
+++ b/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/QrCodeService.ios.kt
@@ -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) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/composeApp/src/jsMain/kotlin/net/codinux/banking/ui/service/QrCodeService.js.kt b/composeApp/src/jsMain/kotlin/net/codinux/banking/ui/service/QrCodeService.js.kt
new file mode 100644
index 0000000..59104b1
--- /dev/null
+++ b/composeApp/src/jsMain/kotlin/net/codinux/banking/ui/service/QrCodeService.js.kt
@@ -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) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0014f23..989bf6c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -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" }