QR codes scanning improvements (#1722)
Introduce Mlkit Barcodes library Added `MlkitQrCodeAnalyzer` component Changelogs update
This commit is contained in:
parent
9916a346d6
commit
805a1b26b7
|
@ -8,6 +8,11 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
|
|||
|
||||
### Changed
|
||||
- Send Confirmation & Send Progress screens have been refactored
|
||||
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
|
||||
us better results in testing
|
||||
|
||||
### Fixed
|
||||
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed
|
||||
|
||||
## [1.3.1 (822)] - 2025-01-07
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
### Changed
|
||||
- Send Confirmation & Send Progress screens have been refactored with bugfixes and optimizations
|
||||
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
|
||||
us better results in testing
|
||||
|
||||
### Fixed
|
||||
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed
|
||||
|
||||
## [1.3.1 (822)] - 2025-01-07
|
||||
|
||||
|
|
|
@ -14,6 +14,11 @@ directly impact users rather than highlighting other key architectural updates.*
|
|||
|
||||
### Changed
|
||||
- Send Confirmation & Send Progress screens have been refactored with bugfixes and optimizations
|
||||
- ZXing QR codes scanning library has been replaced with a more recent MLkit Barcodes scanning library, which gives
|
||||
us better results in testing
|
||||
|
||||
### Fixed
|
||||
- The way how Zashi treats ZIP 321 single address within URIs results has been fixed
|
||||
|
||||
## [1.3.1 (822)] - 2025-01-07
|
||||
|
||||
|
|
|
@ -204,6 +204,7 @@ KOTLINX_SERIALIZABLE_JSON_VERSION=1.6.3
|
|||
KOVER_VERSION=0.7.3
|
||||
LOTTIE_VERSION=6.5.0
|
||||
MARKDOWN_VERSION=0.7.3
|
||||
MLKIT_SCANNING_VERSION=17.3.0
|
||||
PLAY_APP_UPDATE_VERSION=2.1.0
|
||||
PLAY_APP_UPDATE_KTX_VERSION=2.1.0
|
||||
PLAY_PUBLISHER_API_VERSION=v3-rev20231030-2.0.0
|
||||
|
|
|
@ -80,13 +80,15 @@ dependencyResolutionManagement {
|
|||
"androidx.benchmark",
|
||||
"androidx.navigation",
|
||||
"com.android",
|
||||
"com.google.android.apps.common.testing.accessibility.framework",
|
||||
"com.google.android.datatransport",
|
||||
"com.google.android.gms",
|
||||
"com.google.android.material",
|
||||
"com.google.android.odml",
|
||||
"com.google.android.play",
|
||||
"com.google.firebase",
|
||||
"com.google.mlkit",
|
||||
"com.google.testing.platform",
|
||||
"com.google.android.apps.common.testing.accessibility.framework"
|
||||
)
|
||||
val googleRegexes = listOf(
|
||||
"androidx\\..*",
|
||||
|
@ -181,6 +183,7 @@ dependencyResolutionManagement {
|
|||
val kotlinxSerializableJsonVersion = extra["KOTLINX_SERIALIZABLE_JSON_VERSION"].toString()
|
||||
val lottieVersion = extra["LOTTIE_VERSION"].toString()
|
||||
val markdownVersion = extra["MARKDOWN_VERSION"].toString()
|
||||
val mlkitScanningVersion = extra["MLKIT_SCANNING_VERSION"].toString()
|
||||
val playAppUpdateVersion = extra["PLAY_APP_UPDATE_VERSION"].toString()
|
||||
val playAppUpdateKtxVersion = extra["PLAY_APP_UPDATE_KTX_VERSION"].toString()
|
||||
val tinkVersion = extra["TINK_VERSION"].toString()
|
||||
|
@ -245,6 +248,7 @@ dependencyResolutionManagement {
|
|||
library("kotlinx-serializable-json", "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializableJsonVersion")
|
||||
library("lottie", "com.airbnb.android:lottie-compose:$lottieVersion")
|
||||
library("markdown", "org.jetbrains:markdown:$markdownVersion")
|
||||
library("mlkit-scanning", "com.google.mlkit:barcode-scanning:$mlkitScanningVersion")
|
||||
library("play-update", "com.google.android.play:app-update:$playAppUpdateVersion")
|
||||
library("play-update-ktx", "com.google.android.play:app-update-ktx:$playAppUpdateKtxVersion")
|
||||
library("tink", "com.google.crypto.tink:tink-android:$tinkVersion")
|
||||
|
|
|
@ -140,6 +140,7 @@ dependencies {
|
|||
implementation(libs.kotlinx.datetime)
|
||||
implementation(libs.kotlinx.immutable)
|
||||
implementation(libs.kotlinx.serializable.json)
|
||||
implementation(libs.mlkit.scanning)
|
||||
implementation(libs.zcash.sdk)
|
||||
implementation(libs.zcash.sdk.incubator)
|
||||
implementation(libs.zcash.bip39)
|
||||
|
|
|
@ -42,7 +42,8 @@ internal class Zip321ParseUriValidationUseCase(
|
|||
|
||||
return when (paymentRequest) {
|
||||
is ZIP321.ParserResult.Request -> Zip321ParseUriValidation.Valid(zip321Uri)
|
||||
// null or [ZIP321.ParserResult.SingleAddress] is not valid for our ZIP 321 Uri to Proposal use case
|
||||
is ZIP321.ParserResult.SingleAddress ->
|
||||
Zip321ParseUriValidation.SingleAddress(paymentRequest.singleRecipient.value)
|
||||
else -> Zip321ParseUriValidation.Invalid
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +51,8 @@ internal class Zip321ParseUriValidationUseCase(
|
|||
internal sealed class Zip321ParseUriValidation {
|
||||
data class Valid(val zip321Uri: String) : Zip321ParseUriValidation()
|
||||
|
||||
data class SingleAddress(val address: String) : Zip321ParseUriValidation()
|
||||
|
||||
data object Invalid : Zip321ParseUriValidation()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
package co.electriccoin.zcash.ui.screen.scan.util
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Matrix
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import co.electriccoin.zcash.spackle.Twig
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition
|
||||
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
|
||||
|
||||
class MlkitQrCodeAnalyzer(
|
||||
private val framePosition: FramePosition,
|
||||
private val onQrCodeScanned: (String) -> Unit,
|
||||
) : ImageAnalysis.Analyzer {
|
||||
private val supportedImageFormat = Barcode.FORMAT_QR_CODE
|
||||
|
||||
@OptIn(ExperimentalGetImage::class)
|
||||
override fun analyze(imageProxy: ImageProxy) {
|
||||
Twig.verbose { "Mlkit image proxy: ${imageProxy.imageInfo}" }
|
||||
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null) {
|
||||
val bitmap = imageProxy.toBitmap()
|
||||
|
||||
val rotatedBitmap = bitmap.rotate(imageProxy.imageInfo.rotationDegrees)
|
||||
val croppedBitmap = rotatedBitmap.crop(framePosition)
|
||||
|
||||
// No rotation for cropped Bitmap
|
||||
val image = InputImage.fromBitmap(croppedBitmap, 0)
|
||||
|
||||
Twig.verbose {
|
||||
"Scan result: " +
|
||||
"Frame: $framePosition, "
|
||||
"Format: ${mediaImage.format}, " +
|
||||
"Image width: ${mediaImage.width}, " +
|
||||
"Image height: ${mediaImage.height}"
|
||||
"Rotation: ${imageProxy.imageInfo.rotationDegrees}"
|
||||
}
|
||||
|
||||
// Configure Barcode Scanner Options
|
||||
val options =
|
||||
BarcodeScannerOptions.Builder()
|
||||
.setBarcodeFormats(supportedImageFormat)
|
||||
// We could optionally use this to enhance scan success ratio. If it's specified, then the library
|
||||
// will suggest zooming the camera if the barcode is too far away or too small to be detected.
|
||||
// .setZoomSuggestionOptions()
|
||||
.build()
|
||||
|
||||
// Initialize Barcode Scanner
|
||||
val scanner = BarcodeScanning.getClient(options)
|
||||
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
for (barcode in barcodes) {
|
||||
barcode.rawValue?.let { value ->
|
||||
Twig.debug { "Mlkit barcode value: $value" }
|
||||
onQrCodeScanned(value)
|
||||
// Note that we only take the first code from the list of discovered codes
|
||||
return@addOnSuccessListener
|
||||
}
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { e ->
|
||||
Twig.error(e) { "Barcode detection failed" }
|
||||
}
|
||||
.addOnCompleteListener {
|
||||
// Close the image proxy
|
||||
imageProxy.close()
|
||||
}
|
||||
} else {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Bitmap.rotate(rotationDegrees: Int): Bitmap {
|
||||
// Rotate the matrix by the specified degrees
|
||||
val matrix =
|
||||
Matrix().also {
|
||||
it.postRotate(rotationDegrees.toFloat())
|
||||
}
|
||||
return Bitmap.createBitmap(
|
||||
// source
|
||||
this,
|
||||
// x
|
||||
0,
|
||||
// y
|
||||
0,
|
||||
// width
|
||||
width,
|
||||
// height
|
||||
height,
|
||||
// m
|
||||
matrix,
|
||||
// filter (Filter for better quality)
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Crop Bitmap to the specified dimensions given by [FramePosition]
|
||||
*/
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun Bitmap.crop(framePosition: FramePosition): Bitmap {
|
||||
// TODO [#1380]: Leverage FramePosition in QrCodeAnalyzer
|
||||
// TODO [#1380]: https://github.com/Electric-Coin-Company/zashi-android/issues/1380
|
||||
return Bitmap.createBitmap(
|
||||
this,
|
||||
// left
|
||||
(width * LEFT_OFFSET).toInt(),
|
||||
// top
|
||||
(height * TOP_OFFSET).toInt(),
|
||||
// width
|
||||
(width * WIDTH_OFFSET).toInt(),
|
||||
// height
|
||||
(height * HEIGHT_OFFSET).toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
private const val LEFT_OFFSET = .15
|
||||
private const val TOP_OFFSET = .25
|
||||
private const val WIDTH_OFFSET = .7
|
||||
private const val HEIGHT_OFFSET = .45
|
|
@ -88,7 +88,7 @@ import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
|||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.ImageUriToQrCodeConverter
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.MlkitQrCodeAnalyzer
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.view.CAMERA_TRANSLUCENT_BORDER
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.view.FramePosition
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
@ -709,7 +709,7 @@ fun ImageAnalysis.qrCodeFlow(framePosition: FramePosition): Flow<String> {
|
|||
callbackFlow {
|
||||
setAnalyzer(
|
||||
ContextCompat.getMainExecutor(context),
|
||||
QrCodeAnalyzer(
|
||||
MlkitQrCodeAnalyzer(
|
||||
framePosition = framePosition,
|
||||
onQrCodeScanned = { result ->
|
||||
Twig.debug { "Scan result onQrCodeScanned: $result" }
|
||||
|
|
|
@ -41,48 +41,76 @@ internal class ScanViewModel(
|
|||
mutex.withLock {
|
||||
if (!hasBeenScannedSuccessfully) {
|
||||
val addressValidationResult = getSynchronizer().validateAddress(result)
|
||||
|
||||
val zip321ValidationResult = zip321ParseUriValidationUseCase(result)
|
||||
|
||||
state.update {
|
||||
if (addressValidationResult is AddressType.Valid) {
|
||||
ScanValidationState.INVALID
|
||||
} else if (zip321ValidationResult is Zip321ParseUriValidation.Valid) {
|
||||
ScanValidationState.INVALID
|
||||
} else {
|
||||
ScanValidationState.NONE
|
||||
}
|
||||
}
|
||||
|
||||
if (zip321ValidationResult is Zip321ParseUriValidation.Valid) {
|
||||
hasBeenScannedSuccessfully = true
|
||||
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
|
||||
} else if (addressValidationResult is AddressType.Valid) {
|
||||
hasBeenScannedSuccessfully = true
|
||||
|
||||
val serializableAddress = SerializableAddress(result, addressValidationResult)
|
||||
|
||||
when (args) {
|
||||
DEFAULT -> {
|
||||
navigateBack.emit(
|
||||
ScanResultState.Address(
|
||||
Json.encodeToString(
|
||||
SerializableAddress.serializer(),
|
||||
serializableAddress
|
||||
)
|
||||
)
|
||||
)
|
||||
when {
|
||||
zip321ValidationResult is Zip321ParseUriValidation.Valid ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
|
||||
}
|
||||
|
||||
ADDRESS_BOOK -> {
|
||||
navigateCommand.emit(AddContactArgs(serializableAddress.address))
|
||||
zip321ValidationResult is Zip321ParseUriValidation.SingleAddress ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
val singleAddressValidation =
|
||||
getSynchronizer()
|
||||
.validateAddress(zip321ValidationResult.address)
|
||||
when (singleAddressValidation) {
|
||||
is AddressType.Invalid -> {
|
||||
state.update { ScanValidationState.INVALID }
|
||||
}
|
||||
else -> {
|
||||
state.update { ScanValidationState.VALID }
|
||||
processAddress(zip321ValidationResult.address, singleAddressValidation)
|
||||
}
|
||||
}
|
||||
}
|
||||
addressValidationResult is AddressType.Valid ->
|
||||
{
|
||||
hasBeenScannedSuccessfully = true
|
||||
state.update { ScanValidationState.VALID }
|
||||
processAddress(result, addressValidationResult)
|
||||
}
|
||||
else -> {
|
||||
hasBeenScannedSuccessfully = false
|
||||
state.update { ScanValidationState.INVALID }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processAddress(
|
||||
address: String,
|
||||
addressType: AddressType
|
||||
) {
|
||||
require(addressType is AddressType.Valid)
|
||||
|
||||
val serializableAddress =
|
||||
SerializableAddress(
|
||||
address = address,
|
||||
type = addressType
|
||||
)
|
||||
|
||||
when (args) {
|
||||
DEFAULT -> {
|
||||
navigateBack.emit(
|
||||
ScanResultState.Address(
|
||||
Json.encodeToString(
|
||||
SerializableAddress.serializer(),
|
||||
serializableAddress
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ADDRESS_BOOK -> {
|
||||
navigateCommand.emit(AddContactArgs(serializableAddress.address))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onScannedError() =
|
||||
viewModelScope.launch {
|
||||
mutex.withLock {
|
||||
|
|
|
@ -88,7 +88,7 @@ import co.electriccoin.zcash.ui.design.util.stringRes
|
|||
import co.electriccoin.zcash.ui.screen.scan.ScanTag
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanScreenState
|
||||
import co.electriccoin.zcash.ui.screen.scan.model.ScanValidationState
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.QrCodeAnalyzer
|
||||
import co.electriccoin.zcash.ui.screen.scan.util.MlkitQrCodeAnalyzer
|
||||
import co.electriccoin.zcash.ui.screen.scankeystone.model.ScanKeystoneState
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
|
@ -730,7 +730,7 @@ fun ImageAnalysis.qrCodeFlow(framePosition: FramePosition): Flow<String> {
|
|||
callbackFlow {
|
||||
setAnalyzer(
|
||||
ContextCompat.getMainExecutor(context),
|
||||
QrCodeAnalyzer(
|
||||
MlkitQrCodeAnalyzer(
|
||||
framePosition = framePosition,
|
||||
onQrCodeScanned = { result ->
|
||||
Twig.debug { "Scan result onQrCodeScanned: $result" }
|
||||
|
|
Loading…
Reference in New Issue