From 02508b7ae9d5e53b67e8994a56630ee6a8212289 Mon Sep 17 00:00:00 2001 From: Juliano Cezar Chagas Tavares Date: Thu, 18 Jul 2024 16:36:20 -0300 Subject: [PATCH 1/4] Add initial multicredential card implementation --- .../com/spruceid/wallet/sdk/BaseCredential.kt | 23 ++- .../com/spruceid/wallet/sdk/CredentialPack.kt | 108 +++++++++++ .../java/com/spruceid/wallet/sdk/W3CVC.kt | 34 ++++ .../com/spruceid/wallet/sdk/ui/BaseCard.kt | 169 ++++++++++++++++++ 4 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialPack.kt create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/W3CVC.kt create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BaseCredential.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BaseCredential.kt index 7d9e7b9..98dc46c 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BaseCredential.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BaseCredential.kt @@ -1,12 +1,33 @@ package com.spruceid.wallet.sdk -open class BaseCredential constructor(private val id: String?) { +open class BaseCredential { + private var id: String? + + constructor() { + this.id = null + } + + constructor(id: String) { + this.id = id + } fun getId(): String? { return this.id } + fun setId(id: String) { + this.id = id + } + override fun toString(): String { return "Credential($id)" } + + open fun get(keys: List): Map { + return if (keys.contains("id")) { + mapOf("id" to this.id!!) + } else { + emptyMap() + } + } } \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialPack.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialPack.kt new file mode 100644 index 0000000..ba46da8 --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialPack.kt @@ -0,0 +1,108 @@ +package com.spruceid.wallet.sdk + +import java.security.KeyFactory +import java.security.KeyStore +import java.security.cert.Certificate +import java.security.cert.CertificateFactory +import java.security.spec.PKCS8EncodedKeySpec +import java.util.Base64 + +/** + * Collection of BaseCredentials with methods to interact with all instances + */ +class CredentialPack { + private val credentials: MutableList + + constructor() { + credentials = mutableListOf() + } + + constructor(credentialsArray: MutableList) { + this.credentials = credentialsArray + } + + fun addW3CVC(credentialString: String): List { + val vc = W3CVC(credentialString = credentialString) + credentials.add(vc) + return credentials + } + + fun addMDoc( + id: String, + mdocBase64: String, + keyPEM: String, + keyBase64: String + ): List { + try { + val decodedKey = Base64.getDecoder().decode( + keyBase64 + ) + + val privateKey = KeyFactory.getInstance( + "EC" + ).generatePrivate( + PKCS8EncodedKeySpec( + decodedKey + ) + ) + + val cert: Array = arrayOf( + CertificateFactory.getInstance( + "X.509" + ).generateCertificate( + keyPEM.byteInputStream() + ) + ) + + val ks: KeyStore = KeyStore.getInstance( + "AndroidKeyStore" + ) + + ks.load( + null + ) + + ks.setKeyEntry( + "someAlias", + privateKey, + null, + cert + ) + + credentials.add( + MDoc( + id, + Base64.getDecoder().decode(mdocBase64), + "someAlias" + ) + ) + } catch (e: Throwable) { + print( + e + ) + throw e + } + return credentials + } + + fun get(keys: List): Map> { + val values = emptyMap>().toMutableMap() + + for (credential in credentials) { + values[credential.getId()!!] = credential.get(keys) + } + return values + } + + fun getCredentialsByIds(credentialsIds: List): List { + return credentials.filter { credential -> credentialsIds.contains(credential.getId()) } + } + + fun getCredentials(): List { + return credentials + } + + fun getCredentialById(credentialId: String): BaseCredential? { + return credentials.find { credential -> credential.getId().equals(credentialId) } + } +} \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/W3CVC.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/W3CVC.kt new file mode 100644 index 0000000..e0e4e26 --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/W3CVC.kt @@ -0,0 +1,34 @@ +package com.spruceid.wallet.sdk + +import org.json.JSONObject + +class W3CVC(credentialString: String): BaseCredential() { + private var credential: JSONObject = JSONObject(credentialString) + + init { + super.setId(credential.getString("id")) + } + + override fun get(keys: List): Map { + val res = mutableMapOf() + + for (key in keys) { + res[key] = keyPathFinder(credential, key.split(".").toMutableList()) + } + return res + } + + private fun keyPathFinder(json: Any, path: MutableList): Any { + try { + val firstKey = path.first() + val element = (json as JSONObject)[firstKey] + path.removeAt(0) + if (path.isNotEmpty()) { + return keyPathFinder(element, path) + } + return element + } catch (e: Exception) { + return "" + } + } +} diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt new file mode 100644 index 0000000..5973213 --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt @@ -0,0 +1,169 @@ +package com.spruceid.wallet.sdk.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.spruceid.wallet.sdk.CredentialPack + +/** + * Data class with the specification to display the credential pack in a list view + * @property titleKeys A list of keys that will be used to generate a list of values extracted from the credentials + * @property titleFormatter Method used to create a custom title field. Receives an array of values based on the array of keys for the same field + * @property descriptionKeys A list of keys that will be used to generate a list of values extracted from the credentials + * @property descriptionFormatter Method used to create a custom description field. Receives an array of values based on the array of keys for the same field + * @property leadingIconKeys A list of keys that will be used to generate a list of values extracted from the credentials + * @property leadingIconFormatter Method used to create a custom leading icon formatter. Receives an array of values based on the array of keys for the same field + * @property trailingActionKeys A list of keys that will be used to generate a list of values extracted from the credentials + * @property trailingActionButton Method used to create a custom trailing action button. Receives an array of values based on the array of keys for the same field + */ +data class CardRenderingListView( + val titleKeys: List, + val titleFormatter: @Composable ((values: Map>) -> Unit)? = null, + val descriptionKeys: List? = null, + val descriptionFormatter: @Composable ((values: Map>) -> Unit)? = null, + val leadingIconKeys: List? = null, + val leadingIconFormatter: @Composable ((values: Map>) -> Unit)? = null, + val trailingActionKeys: List? = null, + val trailingActionButton: @Composable ((values: Map>) -> Unit)? = null +) + +/** + * Data class with the specification to display the credential field in a details view + * @property keys A list of keys that will be used to generate a list of values extracted from the credentials + * @property formatter Method used to create a custom field. Receives an array of values based on the array of keys for the same field + */ +data class CardRenderingDetailsField( + val keys: List, + val formatter: @Composable ((values: Map>) -> Unit)? = null +) + +/** + * Data class with the specification to display the credential in a details view + * @property fields A list of field render settings that will be used to generate a UI element with the defined keys + */ +data class CardRenderingDetailsView( + val fields: List +) + + +/** + * Interface aggregating two types: + * (LIST == CardRenderingListView) and + * (DETAILS == CardRenderingDetailsView) + */ +sealed interface CardRendering +@JvmInline +value class LIST(val rendering: CardRenderingListView) : CardRendering +@JvmInline +value class DETAILS(val rendering: CardRenderingDetailsView) : CardRendering + +/** + * Method to convert CardRenderingListView to CardRendering + */ +fun CardRenderingListView.toCardRendering() = LIST(this) +/** + * Method to convert CardRenderingDetailsView to CardRendering + */ +fun CardRenderingDetailsView.toCardRendering() = DETAILS(this) + +/** + * Manages the card rendering type according with the render object + * @property credentialPack CredentialPack instance + * @property rendering CardRendering instance + */ +@Composable +fun BaseCard( + credentialPack: CredentialPack, + rendering: CardRendering +) { + when(rendering) { + is LIST -> + CardListView(credentialPack = credentialPack, rendering = rendering.rendering) + is DETAILS -> + CardDetailsView(credentialPack = credentialPack, rendering = rendering.rendering) + } +} + +/** + * Renders the credential as a list view item + * @property credentialPack CredentialPack instance + * @property rendering CardRenderingListView instance + */ +@Composable +fun CardListView( + credentialPack: CredentialPack, + rendering: CardRenderingListView +) { + val titleValues = credentialPack.get(rendering.titleKeys) + val descriptionValues = credentialPack.get(rendering.descriptionKeys ?: emptyList()) + + Row { + // Leading icon + if(rendering.leadingIconFormatter != null) { + rendering.leadingIconFormatter.invoke( + credentialPack.get(rendering.leadingIconKeys ?: emptyList()) + ) + } + + Column { + // Title + if(rendering.titleFormatter != null) { + rendering.titleFormatter.invoke(titleValues) + } else { + Text(text = titleValues.values + .fold(emptyList()) { acc, next -> acc + next.values + .joinToString(" ") { value -> value.toString() } + }.joinToString("").trim()) + } + + // Description + if(rendering.descriptionFormatter != null) { + rendering.descriptionFormatter.invoke(descriptionValues) + } else { + Text(text = descriptionValues.values + .fold(emptyList()) { acc, next -> acc + next.values + .joinToString(" ") { value -> value.toString() } + }.joinToString("").trim()) + } + } + + Spacer(modifier = Modifier.weight(1.0f)) + + // Trailing action button + if(rendering.trailingActionButton != null) { + rendering.trailingActionButton.invoke( + credentialPack.get(rendering.trailingActionKeys ?: emptyList()) + ) + } + } +} + +/** + * Renders the credential as a details view + * @property credentialPack CredentialPack instance + * @property rendering CardRenderingDetailsView instance + */ +@Composable +fun CardDetailsView( + credentialPack: CredentialPack, + rendering: CardRenderingDetailsView +) { + Column { + rendering.fields.forEach { + val values = credentialPack.get(it.keys) + + if(it.formatter != null) { + it.formatter.invoke(values) + } else { + Text(text = values.values + .fold(emptyList()) { acc, next -> acc + next.values + .joinToString(" ") { value -> value.toString() } + }.joinToString("").trim()) + } + } + } + +} From 6f818cca510cb52d5bf1537cc0eac324f33e020c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juliano=20C=C3=A9zar=20Chagas=20Tavares?= Date: Fri, 23 Aug 2024 12:52:33 -0300 Subject: [PATCH 2/4] Adds PDF417 and MRZ scanners (#20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add scanners and analyzers * Update WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/MRZScanner.kt Co-authored-by: Gregório Granado Magalhães --------- Co-authored-by: Gregório Granado Magalhães --- WalletSdk/build.gradle.kts | 4 +- .../spruceid/wallet/sdk/BleSessionManager.kt | 14 +- .../wallet/sdk/CredentialsViewModel.kt | 12 +- .../main/java/com/spruceid/wallet/sdk/MDoc.kt | 2 +- .../com/spruceid/wallet/sdk/ui/BaseCard.kt | 7 +- .../wallet/sdk/ui/GenericCameraXScanner.kt | 167 +++++++ .../com/spruceid/wallet/sdk/ui/MRZScanner.kt | 261 +++++++++++ .../spruceid/wallet/sdk/ui/PDF417Scanner.kt | 270 +++++++++++ .../spruceid/wallet/sdk/ui/QRCodeAnalyzer.kt | 60 --- .../spruceid/wallet/sdk/ui/QRCodeScanner.kt | 434 ++++++++---------- 10 files changed, 912 insertions(+), 319 deletions(-) create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/GenericCameraXScanner.kt create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/MRZScanner.kt create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/PDF417Scanner.kt delete mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeAnalyzer.kt diff --git a/WalletSdk/build.gradle.kts b/WalletSdk/build.gradle.kts index b627da1..05922bc 100644 --- a/WalletSdk/build.gradle.kts +++ b/WalletSdk/build.gradle.kts @@ -128,10 +128,10 @@ dependencies { implementation("androidx.camera:camera-view:1.3.2") implementation("com.google.zxing:core:3.5.1") implementation("com.google.accompanist:accompanist-permissions:0.34.0") - implementation("androidx.test.ext:junit-ktx:1.1.5") + implementation("androidx.camera:camera-mlkit-vision:1.3.0-alpha06") + implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0") /* End UI dependencies */ testImplementation("junit:junit:4.13.2") - androidTestImplementation("com.android.support.test:runner:1.0.2") androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2") } diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BleSessionManager.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BleSessionManager.kt index bd02b95..37d3491 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BleSessionManager.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/BleSessionManager.kt @@ -2,13 +2,13 @@ package com.spruceid.wallet.sdk import android.bluetooth.BluetoothManager import android.util.Log -import com.spruceid.wallet.sdk.rs.ItemsRequest -import com.spruceid.wallet.sdk.rs.SessionManager -import com.spruceid.wallet.sdk.rs.SessionManagerEngaged -import com.spruceid.wallet.sdk.rs.initialiseSession -import com.spruceid.wallet.sdk.rs.handleRequest -import com.spruceid.wallet.sdk.rs.submitResponse -import com.spruceid.wallet.sdk.rs.submitSignature +import com.spruceid.mobile.sdk.rs.ItemsRequest +import com.spruceid.mobile.sdk.rs.SessionManager +import com.spruceid.mobile.sdk.rs.SessionManagerEngaged +import com.spruceid.mobile.sdk.rs.initialiseSession +import com.spruceid.mobile.sdk.rs.handleRequest +import com.spruceid.mobile.sdk.rs.submitResponse +import com.spruceid.mobile.sdk.rs.submitSignature import java.security.KeyStore import java.security.Signature import java.util.UUID diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialsViewModel.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialsViewModel.kt index de9b2fa..61673b7 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialsViewModel.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/CredentialsViewModel.kt @@ -3,12 +3,12 @@ package com.spruceid.wallet.sdk import android.bluetooth.BluetoothManager import android.util.Log import androidx.lifecycle.ViewModel -import com.spruceid.wallet.sdk.rs.RequestData -import com.spruceid.wallet.sdk.rs.SessionData -import com.spruceid.wallet.sdk.rs.handleRequest -import com.spruceid.wallet.sdk.rs.initialiseSession -import com.spruceid.wallet.sdk.rs.submitResponse -import com.spruceid.wallet.sdk.rs.submitSignature +import com.spruceid.mobile.sdk.rs.RequestData +import com.spruceid.mobile.sdk.rs.SessionData +import com.spruceid.mobile.sdk.rs.handleRequest +import com.spruceid.mobile.sdk.rs.initialiseSession +import com.spruceid.mobile.sdk.rs.submitResponse +import com.spruceid.mobile.sdk.rs.submitSignature import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import java.security.KeyStore diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/MDoc.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/MDoc.kt index 82dc1c9..903a793 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/MDoc.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/MDoc.kt @@ -1,7 +1,7 @@ package com.spruceid.wallet.sdk import android.util.Log -import com.spruceid.wallet.sdk.rs.MDoc as InnerMDoc +import com.spruceid.mobile.sdk.rs.MDoc as InnerMDoc class MDoc(id: String, issuerAuth: ByteArray, val keyAlias: String) : BaseCredential(id) { val inner: InnerMDoc diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt index 5973213..e1d1667 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/BaseCard.kt @@ -1,8 +1,10 @@ package com.spruceid.wallet.sdk.ui import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -100,7 +102,9 @@ fun CardListView( val titleValues = credentialPack.get(rendering.titleKeys) val descriptionValues = credentialPack.get(rendering.descriptionKeys ?: emptyList()) - Row { + Row( + Modifier.height(intrinsicSize = IntrinsicSize.Max) + ) { // Leading icon if(rendering.leadingIconFormatter != null) { rendering.leadingIconFormatter.invoke( @@ -112,6 +116,7 @@ fun CardListView( // Title if(rendering.titleFormatter != null) { rendering.titleFormatter.invoke(titleValues) +// verifyJwtVp } else { Text(text = titleValues.values .fold(emptyList()) { acc, next -> acc + next.values diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/GenericCameraXScanner.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/GenericCameraXScanner.kt new file mode 100644 index 0000000..0b1af24 --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/GenericCameraXScanner.kt @@ -0,0 +1,167 @@ +package com.spruceid.wallet.sdk.ui + +import android.content.Context +import android.util.Range +import android.view.Surface +import androidx.camera.core.CameraControl +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat + +@Composable +fun GenericCameraXScanner( + title: String = "Scan QR Code", + subtitle: String = "Please align within the guides", + cancelButtonLabel: String = "Cancel", + onCancel: () -> Unit, + fontFamily: FontFamily = FontFamily.Default, + textColor: Color = Color.White, + imageAnalyzer: ImageAnalysis.Analyzer, + background: @Composable () -> Unit +) { + val context = LocalContext.current + val cameraProviderFuture = + remember { + ProcessCameraProvider.getInstance(context) + } + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(key1 = cameraProviderFuture) { + onDispose { + cameraProviderFuture.get()?.unbindAll() + } + } + + fun setupCamera(context: Context): PreviewView { + val previewView = PreviewView(context) + val preview = + Preview.Builder() + .setTargetFrameRate(Range(20, 45)) + .setTargetRotation(Surface.ROTATION_0) + .build() + val selector = + CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + preview.setSurfaceProvider(previewView.surfaceProvider) + val imageAnalysis = + ImageAnalysis.Builder() + .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) + .build() + imageAnalysis.setAnalyzer( + ContextCompat.getMainExecutor(context), + imageAnalyzer + ) + var cameraControl: CameraControl? = null + try { + cameraControl = cameraProviderFuture + .get() + .bindToLifecycle( + lifecycleOwner, + selector, + preview, + imageAnalysis, + ).cameraControl + } catch (e: Exception) { + e.printStackTrace() + } + try { + cameraControl?.setZoomRatio(2f) + } catch (e: Exception) { + e.printStackTrace() + } + return previewView + } + + Column( + modifier = Modifier.fillMaxSize(), + ) { + Box( + Modifier.fillMaxSize(), + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + setupCamera(context = context) + }, + ) + background() + Column( + Modifier.fillMaxSize(), + verticalArrangement = Arrangement.SpaceBetween, + ) { + Column( + Modifier + .fillMaxWidth() + .padding(top = 80.dp) + .padding(horizontal = 30.dp), + ) { + Text( + text = title, + fontFamily = fontFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + color = textColor, + ) + Text( + text = subtitle, + fontFamily = fontFamily, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = textColor, + ) + } + + Column( + Modifier.fillMaxWidth(), + ) { + Button( + onClick = onCancel, + modifier = + Modifier + .padding(bottom = 50.dp) + .padding(horizontal = 30.dp), + colors = + ButtonDefaults.buttonColors( + containerColor = Color.Transparent, + contentColor = Color.Transparent, + ), + ) { + Text( + text = cancelButtonLabel, + fontFamily = fontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + color = textColor, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/MRZScanner.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/MRZScanner.kt new file mode 100644 index 0000000..1db7ad8 --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/MRZScanner.kt @@ -0,0 +1,261 @@ +package com.spruceid.wallet.sdk.ui + +import androidx.camera.core.ImageAnalysis.COORDINATE_SYSTEM_ORIGINAL +import androidx.camera.mlkit.vision.MlKitAnalyzer +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import com.google.mlkit.vision.text.TextRecognition +import com.google.mlkit.vision.text.latin.TextRecognizerOptions + +@Composable +fun MRZScanner( + title: String = "Scan QR Code", + subtitle: String = "Please align within the guides", + cancelButtonLabel: String = "Cancel", + onRead: (content: String) -> Unit, + isMatch: (content: String) -> Boolean = {_ -> true}, + onCancel: () -> Unit, + fontFamily: FontFamily = FontFamily.Default, + guidesColor: Color = Color.White, + readerColor: Color = Color.White, + textColor: Color = Color.White, + backgroundOpacity: Float = 0.5f, +) { + + val textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) + val firstLineRegex = Regex( + pattern = "IAUT[O0]\\d{10}SRC\\d{10}<<", + ) + val secondLineRegex = Regex( + pattern = "([0-9]{7}[MF<][0-9]{7}[A-Z<]{3}[A-Z0-9<]{11}[0-9])", + ) + val thirdLineRegex = Regex( + pattern = "([A-Z]+<)+<([A-Z]+<)+<+", + ) + + var firstLine by remember { mutableStateOf(null) } + var secondLine by remember { mutableStateOf(null) } + var thirdLine by remember { mutableStateOf(null) } + + val context = LocalContext.current + + GenericCameraXScanner( + title = title, + subtitle = subtitle, + cancelButtonLabel = cancelButtonLabel, + onCancel = onCancel, + fontFamily = fontFamily, + textColor = textColor, + imageAnalyzer = MlKitAnalyzer( + listOf(textRecognizer), + COORDINATE_SYSTEM_ORIGINAL, + ContextCompat.getMainExecutor(context) + ) { analyzerResult -> + analyzerResult.getValue(textRecognizer)?.let { text -> + text.textBlocks + .flatMap { textBlock -> textBlock.lines } + .mapNotNull { lines -> + lines.takeIf { firstLineRegex.matches(it.text) }?.let { + if(it.text.length == 30) { + firstLine = it.text + } + } + lines.takeIf { secondLineRegex.matches(it.text) }?.let { + if(it.text.length == 30) { + secondLine = it.text + } + } + lines.takeIf { thirdLineRegex.matches(it.text) }?.let { + if(it.text.length == 30) { + thirdLine = it.text + } + } + } + + if( + firstLine != null && secondLine != null && thirdLine != null) { + val mrz = """$firstLine + |$secondLine + |$thirdLine""".trimMargin() + if(isMatch(mrz)) { + onRead(mrz) + } + firstLine = null + secondLine = null + thirdLine = null + } + } + }, + background = { + MRZScannerBackground( + guidesColor = guidesColor, + readerColor = readerColor, + backgroundOpacity = backgroundOpacity, + ) + } + ) +} + +@Composable +fun MRZScannerBackground( + guidesColor: Color = Color.White, + readerColor: Color = Color.White, + backgroundOpacity: Float = 0.5f, +) { + var canvasSize by remember { + mutableStateOf(Size(0f, 0f)) + } + val infiniteTransition = rememberInfiniteTransition("Infinite QR code line transition remember") + val offsetTop by infiniteTransition.animateFloat( + initialValue = canvasSize.height * .35f, + targetValue = canvasSize.height * .35f + canvasSize.width * .6f, + animationSpec = + infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + "QR code line animation", + ) + + return Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = backgroundOpacity)) + .drawWithContent { + canvasSize = size + val canvasWidth = size.width + val canvasHeight = size.height + val width = canvasWidth * .6f + val height = canvasHeight * .6f + + + val left = (canvasWidth - width) / 2 + val top = (canvasHeight - height) / 2 + val right = left + width + val bottom = top + height + val cornerLength = 40f + val cornerRadius = 40f + drawContent() + drawRect(Color(0x99000000)) + drawRoundRect( + topLeft = Offset(left, top), + size = Size(width, height), + color = Color.Transparent, + blendMode = BlendMode.SrcIn, + cornerRadius = CornerRadius(cornerRadius - 10f), + ) + drawRect( + topLeft = Offset(left, offsetTop), + size = Size(width, 2f), + color = readerColor, + style = Stroke(2.dp.toPx()), + ) + + val path = Path() + + // top left + path.moveTo(left, (top + cornerRadius)) + path.arcTo( + Rect( + left = left, + top = top, + right = left + cornerRadius, + bottom = top + cornerRadius, + ), + 180f, + 90f, + true, + ) + path.moveTo(left + (cornerRadius / 2f), top) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, top) + path.moveTo(left, top + (cornerRadius / 2f)) + path.lineTo(left, top + (cornerRadius / 2f) + cornerLength) + + // top right + path.moveTo(right - cornerRadius, top) + path.arcTo( + Rect( + left = right - cornerRadius, + top = top, + right = right, + bottom = top + cornerRadius, + ), + 270f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), top) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, top) + path.moveTo(right, top + (cornerRadius / 2f)) + path.lineTo(right, top + (cornerRadius / 2f) + cornerLength) + + // bottom left + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = left, + top = bottom - cornerRadius, + right = left + cornerRadius, + bottom = bottom, + ), + 90f, + 90f, + true, + ) + path.moveTo(left + (cornerRadius / 2f), bottom) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, bottom) + path.moveTo(left, bottom - (cornerRadius / 2f)) + path.lineTo(left, bottom - (cornerRadius / 2f) - cornerLength) + + // bottom right + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = right - cornerRadius, + top = bottom - cornerRadius, + right = right, + bottom = bottom, + ), + 0f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), bottom) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, bottom) + path.moveTo(right, bottom - (cornerRadius / 2f)) + path.lineTo(right, bottom - (cornerRadius / 2f) - cornerLength) + + drawPath( + path, + color = guidesColor, + style = Stroke(width = 15f), + ) + }, + ) +} \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/PDF417Scanner.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/PDF417Scanner.kt new file mode 100644 index 0000000..2e46ece --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/PDF417Scanner.kt @@ -0,0 +1,270 @@ +package com.spruceid.wallet.sdk.ui + +import android.graphics.ImageFormat +import android.os.Build +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.pdf417.PDF417Reader +import java.nio.ByteBuffer +import java.util.EnumMap + +@Composable +fun PDF417Scanner( + title: String = "Scan QR Code", + subtitle: String = "Please align within the guides", + cancelButtonLabel: String = "Cancel", + onRead: (content: String) -> Unit, + isMatch: (content: String) -> Boolean = {_ -> true}, + onCancel: () -> Unit, + fontFamily: FontFamily = FontFamily.Default, + guidesColor: Color = Color.White, + readerColor: Color = Color.White, + textColor: Color = Color.White, + backgroundOpacity: Float = 0.5f, +) { + + GenericCameraXScanner( + title = title, + subtitle = subtitle, + cancelButtonLabel = cancelButtonLabel, + onCancel = onCancel, + fontFamily = fontFamily, + textColor = textColor, + imageAnalyzer = PDF417Analyzer( + isMatch = isMatch, + onQrCodeScanned = { result -> + onRead(result) + }), + background = { + PDF417ScannerBackground( + guidesColor = guidesColor, + readerColor = readerColor, + backgroundOpacity = backgroundOpacity, + ) + } + ) +} + +class PDF417Analyzer( + private val onQrCodeScanned: (String) -> Unit, + private val isMatch: (content: String) -> Boolean = {_ -> true}, +) : ImageAnalysis.Analyzer { + + private val supportedImageFormats = mutableListOf(ImageFormat.YUV_420_888) + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + supportedImageFormats.addAll(listOf(ImageFormat.YUV_422_888, ImageFormat.YUV_444_888)) + } + } + + override fun analyze(image: ImageProxy) { + if (image.format in supportedImageFormats) { + val bytes = image.planes[0].buffer.toByteArray() + val source = + PlanarYUVLuminanceSource( + bytes, + image.width, + image.height, + 0, + 0, + image.width, + image.height, + false, + ) + val binaryBmp = BinaryBitmap(HybridBinarizer(source)) + + val hints: MutableMap = EnumMap( + DecodeHintType::class.java + ) + + hints[DecodeHintType.TRY_HARDER] = true + + try { + val result = PDF417Reader().decode(binaryBmp, hints) + if (isMatch(result.text)) { + onQrCodeScanned(result.text) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + image.close() + } + } + } + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { + get(it) + } + } +} + +@Composable +fun PDF417ScannerBackground( + guidesColor: Color = Color.White, + readerColor: Color = Color.White, + backgroundOpacity: Float = 0.5f, +) { + var canvasSize by remember { + mutableStateOf(Size(0f, 0f)) + } + val infiniteTransition = rememberInfiniteTransition("Infinite QR code line transition remember") + val offsetTop by infiniteTransition.animateFloat( + initialValue = canvasSize.height * .35f, + targetValue = canvasSize.height * .35f + canvasSize.width * .6f, + animationSpec = + infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + "QR code line animation", + ) + + return Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = backgroundOpacity)) + .drawWithContent { + canvasSize = size + val canvasWidth = size.width + val canvasHeight = size.height + val width = canvasWidth * .6f + val height = canvasHeight * .6f + + + val left = (canvasWidth - width) / 2 + val top = (canvasHeight - height) / 2 + val right = left + width + val bottom = top + height + val cornerLength = 40f + val cornerRadius = 40f + drawContent() + drawRect(Color(0x99000000)) + drawRoundRect( + topLeft = Offset(left, top), + size = Size(width, height), + color = Color.Transparent, + blendMode = BlendMode.SrcIn, + cornerRadius = CornerRadius(cornerRadius - 10f), + ) + drawRect( + topLeft = Offset(left, offsetTop), + size = Size(width, 2f), + color = readerColor, + style = Stroke(2.dp.toPx()), + ) + + val path = Path() + + // top left + path.moveTo(left, (top + cornerRadius)) + path.arcTo( + Rect( + left = left, + top = top, + right = left + cornerRadius, + bottom = top + cornerRadius, + ), + 180f, + 90f, + true, + ) + path.moveTo(left + (cornerRadius / 2f), top) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, top) + path.moveTo(left, top + (cornerRadius / 2f)) + path.lineTo(left, top + (cornerRadius / 2f) + cornerLength) + + // top right + path.moveTo(right - cornerRadius, top) + path.arcTo( + Rect( + left = right - cornerRadius, + top = top, + right = right, + bottom = top + cornerRadius, + ), + 270f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), top) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, top) + path.moveTo(right, top + (cornerRadius / 2f)) + path.lineTo(right, top + (cornerRadius / 2f) + cornerLength) + + // bottom left + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = left, + top = bottom - cornerRadius, + right = left + cornerRadius, + bottom = bottom, + ), + 90f, + 90f, + true, + ) + path.moveTo(left + (cornerRadius / 2f), bottom) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, bottom) + path.moveTo(left, bottom - (cornerRadius / 2f)) + path.lineTo(left, bottom - (cornerRadius / 2f) - cornerLength) + + // bottom right + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = right - cornerRadius, + top = bottom - cornerRadius, + right = right, + bottom = bottom, + ), + 0f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), bottom) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, bottom) + path.moveTo(right, bottom - (cornerRadius / 2f)) + path.lineTo(right, bottom - (cornerRadius / 2f) - cornerLength) + + drawPath( + path, + color = guidesColor, + style = Stroke(width = 15f), + ) + }, + ) +} \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeAnalyzer.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeAnalyzer.kt deleted file mode 100644 index 8bcf56f..0000000 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeAnalyzer.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.spruceid.wallet.sdk.ui - -import android.graphics.ImageFormat -import android.os.Build -import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageProxy -import com.google.zxing.BinaryBitmap -import com.google.zxing.PlanarYUVLuminanceSource -import com.google.zxing.common.HybridBinarizer -import com.google.zxing.qrcode.QRCodeReader -import java.nio.ByteBuffer - -class QrCodeAnalyzer( - private val onQrCodeScanned: (String) -> Unit, - private val isMatch: (content: String) -> Boolean = {_ -> true}, -) : ImageAnalysis.Analyzer { - - private val supportedImageFormats = mutableListOf(ImageFormat.YUV_420_888) - - init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - supportedImageFormats.addAll(listOf(ImageFormat.YUV_422_888, ImageFormat.YUV_444_888)) - } - } - - override fun analyze(image: ImageProxy) { - if (image.format in supportedImageFormats) { - val bytes = image.planes[0].buffer.toByteArray() - val source = - PlanarYUVLuminanceSource( - bytes, - image.width, - image.height, - 0, - 0, - image.width, - image.height, - false, - ) - val binaryBmp = BinaryBitmap(HybridBinarizer(source)) - try { - val result = QRCodeReader().decode(binaryBmp) - if (isMatch(result.text)) { - onQrCodeScanned(result.text) - } - } catch (e: Exception) { - e.printStackTrace() - } finally { - image.close() - } - } - } - - private fun ByteBuffer.toByteArray(): ByteArray { - rewind() - return ByteArray(remaining()).also { - get(it) - } - } -} \ No newline at end of file diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeScanner.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeScanner.kt index 2cf3146..b6f149a 100644 --- a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeScanner.kt +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/ui/QRCodeScanner.kt @@ -1,18 +1,9 @@ package com.spruceid.wallet.sdk.ui -import android.content.res.Resources -import android.util.Range -import android.view.Surface -import androidx.camera.core.CameraControl -import androidx.camera.core.CameraSelector +import android.graphics.ImageFormat +import android.os.Build import androidx.camera.core.ImageAnalysis -import androidx.camera.core.ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST -import androidx.camera.core.ImageCapture -import androidx.camera.core.Preview -import androidx.camera.core.resolutionselector.ResolutionSelector -import androidx.camera.core.resolutionselector.ResolutionStrategy -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView +import androidx.camera.core.ImageProxy import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -20,15 +11,8 @@ import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -44,15 +28,15 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.ContextCompat - +import com.google.zxing.BinaryBitmap +import com.google.zxing.DecodeHintType +import com.google.zxing.PlanarYUVLuminanceSource +import com.google.zxing.common.HybridBinarizer +import com.google.zxing.qrcode.QRCodeReader +import java.nio.ByteBuffer +import java.util.EnumMap @Composable fun QRCodeScanner( @@ -68,16 +52,35 @@ fun QRCodeScanner( textColor: Color = Color.White, backgroundOpacity: Float = 0.5f, ) { - var code by remember { - mutableStateOf("") - } - val context = LocalContext.current - val cameraProviderFuture = - remember { - ProcessCameraProvider.getInstance(context) + + GenericCameraXScanner( + title = title, + subtitle = subtitle, + cancelButtonLabel = cancelButtonLabel, + onCancel = onCancel, + fontFamily = fontFamily, + textColor = textColor, + imageAnalyzer = QrCodeAnalyzer( + isMatch = isMatch, + onQrCodeScanned = { result -> + onRead(result) + }), + background = { + QRCodeScannerBackground( + guidesColor = guidesColor, + readerColor = readerColor, + backgroundOpacity = backgroundOpacity, + ) } - val lifecycleOwner = LocalLifecycleOwner.current + ) +} +@Composable +fun QRCodeScannerBackground( + guidesColor: Color = Color.White, + readerColor: Color = Color.White, + backgroundOpacity: Float = 0.5f, +) { var canvasSize by remember { mutableStateOf(Size(0f, 0f)) } @@ -93,225 +96,172 @@ fun QRCodeScanner( "QR code line animation", ) + return Box( + Modifier + .fillMaxSize() + .background(Color.Black.copy(alpha = backgroundOpacity)) + .drawWithContent { + canvasSize = size + val canvasWidth = size.width + val canvasHeight = size.height + val width = canvasWidth * .6f - Column( - modifier = Modifier.fillMaxSize(), - ) { - Box( - Modifier.fillMaxSize(), - ) { - AndroidView( - factory = { context -> - val previewView = PreviewView(context) - val preview = - Preview.Builder() - .setTargetFrameRate(Range(20, 45)) - .setTargetRotation(Surface.ROTATION_0) - .build() - val selector = - CameraSelector.Builder() - .requireLensFacing(CameraSelector.LENS_FACING_BACK) - .build() - preview.setSurfaceProvider(previewView.surfaceProvider) - val imageAnalysis = - ImageAnalysis.Builder() - .setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) - .build() + val left = (canvasWidth - width) / 2 + val top = canvasHeight * .35f + val right = left + width + val bottom = top + width + val cornerLength = 40f + val cornerRadius = 40f + drawContent() + drawRect(Color(0x99000000)) + drawRoundRect( + topLeft = Offset(left, top), + size = Size(width, width), + color = Color.Transparent, + blendMode = BlendMode.SrcIn, + cornerRadius = CornerRadius(cornerRadius - 10f), + ) + drawRect( + topLeft = Offset(left, offsetTop), + size = Size(width, 2f), + color = readerColor, + style = Stroke(2.dp.toPx()), + ) - imageAnalysis.setAnalyzer( - ContextCompat.getMainExecutor(context), - QrCodeAnalyzer( - isMatch = isMatch, - onQrCodeScanned = { result -> - onRead(result) - code = result - }), - ) - var cameraControl: CameraControl? = null - try { - cameraControl = cameraProviderFuture - .get() - .bindToLifecycle( - lifecycleOwner, - selector, - preview, - imageAnalysis, - ).cameraControl - } catch (e: Exception) { - e.printStackTrace() - } - try { - cameraControl?.setZoomRatio(2f) - } catch (e: Exception) { - e.printStackTrace() - } - previewView - }, - modifier = Modifier.fillMaxSize(), + val path = Path() + + // top left + path.moveTo(left, (top + cornerRadius)) + path.arcTo( + Rect( + left = left, + top = top, + right = left + cornerRadius, + bottom = top + cornerRadius, + ), + 180f, + 90f, + true, ) - Box( - Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = backgroundOpacity)) - .drawWithContent { - canvasSize = size - val canvasWidth = size.width - val canvasHeight = size.height - val width = canvasWidth * .6f + path.moveTo(left + (cornerRadius / 2f), top) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, top) + path.moveTo(left, top + (cornerRadius / 2f)) + path.lineTo(left, top + (cornerRadius / 2f) + cornerLength) - val left = (canvasWidth - width) / 2 - val top = canvasHeight * .35f - val right = left + width - val bottom = top + width - val cornerLength = 40f - val cornerRadius = 40f - drawContent() - drawRect(Color(0x99000000)) - drawRoundRect( - topLeft = Offset(left, top), - size = Size(width, width), - color = Color.Transparent, - blendMode = BlendMode.SrcIn, - cornerRadius = CornerRadius(cornerRadius - 10f), - ) - drawRect( - topLeft = Offset(left, offsetTop), - size = Size(width, 2f), - color = readerColor, - style = Stroke(2.dp.toPx()), - ) - val path = Path() + // top right + path.moveTo(right - cornerRadius, top) + path.arcTo( + Rect( + left = right - cornerRadius, + top = top, + right = right, + bottom = top + cornerRadius, + ), + 270f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), top) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, top) + path.moveTo(right, top + (cornerRadius / 2f)) + path.lineTo(right, top + (cornerRadius / 2f) + cornerLength) - // top left - path.moveTo(left, (top + cornerRadius)) - path.arcTo( - Rect( - left = left, - top = top, - right = left + cornerRadius, - bottom = top + cornerRadius, - ), - 180f, - 90f, - true, - ) - path.moveTo(left + (cornerRadius / 2f), top) - path.lineTo(left + (cornerRadius / 2f) + cornerLength, top) - path.moveTo(left, top + (cornerRadius / 2f)) - path.lineTo(left, top + (cornerRadius / 2f) + cornerLength) + // bottom left + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = left, + top = bottom - cornerRadius, + right = left + cornerRadius, + bottom = bottom, + ), + 90f, + 90f, + true, + ) + path.moveTo(left + (cornerRadius / 2f), bottom) + path.lineTo(left + (cornerRadius / 2f) + cornerLength, bottom) + path.moveTo(left, bottom - (cornerRadius / 2f)) + path.lineTo(left, bottom - (cornerRadius / 2f) - cornerLength) - // top right - path.moveTo(right - cornerRadius, top) - path.arcTo( - Rect( - left = right - cornerRadius, - top = top, - right = right, - bottom = top + cornerRadius, - ), - 270f, - 90f, - true, - ) - path.moveTo(right - (cornerRadius / 2f), top) - path.lineTo(right - (cornerRadius / 2f) - cornerLength, top) - path.moveTo(right, top + (cornerRadius / 2f)) - path.lineTo(right, top + (cornerRadius / 2f) + cornerLength) + // bottom right + path.moveTo(left, bottom - cornerRadius) + path.arcTo( + Rect( + left = right - cornerRadius, + top = bottom - cornerRadius, + right = right, + bottom = bottom, + ), + 0f, + 90f, + true, + ) + path.moveTo(right - (cornerRadius / 2f), bottom) + path.lineTo(right - (cornerRadius / 2f) - cornerLength, bottom) + path.moveTo(right, bottom - (cornerRadius / 2f)) + path.lineTo(right, bottom - (cornerRadius / 2f) - cornerLength) - // bottom left - path.moveTo(left, bottom - cornerRadius) - path.arcTo( - Rect( - left = left, - top = bottom - cornerRadius, - right = left + cornerRadius, - bottom = bottom, - ), - 90f, - 90f, - true, - ) - path.moveTo(left + (cornerRadius / 2f), bottom) - path.lineTo(left + (cornerRadius / 2f) + cornerLength, bottom) - path.moveTo(left, bottom - (cornerRadius / 2f)) - path.lineTo(left, bottom - (cornerRadius / 2f) - cornerLength) + drawPath( + path, + color = guidesColor, + style = Stroke(width = 15f), + ) + }, + ) +} - // bottom right - path.moveTo(left, bottom - cornerRadius) - path.arcTo( - Rect( - left = right - cornerRadius, - top = bottom - cornerRadius, - right = right, - bottom = bottom, - ), - 0f, - 90f, - true, - ) - path.moveTo(right - (cornerRadius / 2f), bottom) - path.lineTo(right - (cornerRadius / 2f) - cornerLength, bottom) - path.moveTo(right, bottom - (cornerRadius / 2f)) - path.lineTo(right, bottom - (cornerRadius / 2f) - cornerLength) +class QrCodeAnalyzer( + private val onQrCodeScanned: (String) -> Unit, + private val isMatch: (content: String) -> Boolean = {_ -> true}, +) : ImageAnalysis.Analyzer { + + private val supportedImageFormats = mutableListOf(ImageFormat.YUV_420_888) + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + supportedImageFormats.addAll(listOf(ImageFormat.YUV_422_888, ImageFormat.YUV_444_888)) + } + } - drawPath( - path, - color = guidesColor, - style = Stroke(width = 15f), - ) - }, + override fun analyze(image: ImageProxy) { + if (image.format in supportedImageFormats) { + val bytes = image.planes[0].buffer.toByteArray() + val source = + PlanarYUVLuminanceSource( + bytes, + image.width, + image.height, + 0, + 0, + image.width, + image.height, + false, ) - Column( - Modifier.fillMaxSize(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - Column( - Modifier - .fillMaxWidth() - .padding(top = 80.dp) - .padding(horizontal = 30.dp), - ) { - Text( - text = title, - fontFamily = fontFamily, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - color = textColor, - ) - Text( - text = subtitle, - fontFamily = fontFamily, - fontWeight = FontWeight.Bold, - fontSize = 16.sp, - color = textColor, - ) - } + val binaryBmp = BinaryBitmap(HybridBinarizer(source)) + + val hints: MutableMap = EnumMap( + DecodeHintType::class.java + ) - Column( - Modifier.fillMaxWidth(), - ) { - Button( - onClick = onCancel, - modifier = - Modifier - .padding(bottom = 50.dp) - .padding(horizontal = 30.dp), - colors = - ButtonDefaults.buttonColors( - containerColor = Color.Transparent, - contentColor = Color.Transparent, - ), - ) { - Text( - text = cancelButtonLabel, - fontFamily = fontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = textColor, - ) - } - } + hints[DecodeHintType.TRY_HARDER] = true + try { + val result = QRCodeReader().decode(binaryBmp, hints) + if (isMatch(result.text)) { + onQrCodeScanned(result.text) } + } catch (e: Exception) { + e.printStackTrace() + } finally { + image.close() } + } } -} + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() + return ByteArray(remaining()).also { + get(it) + } + } +} \ No newline at end of file From 975e1c62feeceeb7877c9d1748ec9b31ca4abdf4 Mon Sep 17 00:00:00 2001 From: Juliano Cezar Chagas Tavares Date: Tue, 27 Aug 2024 11:06:14 -0300 Subject: [PATCH 3/4] Remove missing comment --- MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt index ecec5ae..e2f4fc9 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/ui/BaseCard.kt @@ -116,7 +116,6 @@ fun CardListView( // Title if(rendering.titleFormatter != null) { rendering.titleFormatter.invoke(titleValues) -// verifyJwtVp } else { Text(text = titleValues.values .fold(emptyList()) { acc, next -> acc + next.values From f9a35dae08326f45b4a8903b2d2d233d5146f772 Mon Sep 17 00:00:00 2001 From: Juliano Cezar Chagas Tavares Date: Wed, 28 Aug 2024 12:38:33 -0300 Subject: [PATCH 4/4] bump mobilesdkrs dependency --- MobileSdk/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index c8f7bf9..92c2db8 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -118,7 +118,7 @@ android { } dependencies { - api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.26") + api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.28") //noinspection GradleCompatible implementation("com.android.support:appcompat-v7:28.0.0") /* Begin UI dependencies */