Skip to content

Commit

Permalink
QR codes scanning improvements (#1722)
Browse files Browse the repository at this point in the history
Introduce Mlkit Barcodes library

Added `MlkitQrCodeAnalyzer` component

Changelogs update
  • Loading branch information
HonzaR authored Jan 9, 2025
1 parent 9916a34 commit 805a1b2
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 37 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/whatsNew/WHATS_NEW_EN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions docs/whatsNew/WHATS_NEW_ES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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\\..*",
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions ui-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,17 @@ 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
}
}

internal sealed class Zip321ParseUriValidation {
data class Valid(val zip321Uri: String) : Zip321ParseUriValidation()

data class SingleAddress(val address: String) : Zip321ParseUriValidation()

data object Invalid : Zip321ParseUriValidation()
}
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,75 @@ 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
when {
zip321ValidationResult is Zip321ParseUriValidation.Valid ->
{
hasBeenScannedSuccessfully = true
state.update { ScanValidationState.VALID }
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
}
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 }
}
}
}
}
}

if (zip321ValidationResult is Zip321ParseUriValidation.Valid) {
hasBeenScannedSuccessfully = true
navigateBack.emit(ScanResultState.Zip321Uri(zip321ValidationResult.zip321Uri))
} else if (addressValidationResult is AddressType.Valid) {
hasBeenScannedSuccessfully = true
private suspend fun processAddress(
address: String,
addressType: AddressType
) {
require(addressType is AddressType.Valid)

val serializableAddress = SerializableAddress(result, addressValidationResult)
val serializableAddress =
SerializableAddress(
address = address,
type = addressType
)

when (args) {
DEFAULT -> {
navigateBack.emit(
ScanResultState.Address(
Json.encodeToString(
SerializableAddress.serializer(),
serializableAddress
)
)
)
}
when (args) {
DEFAULT -> {
navigateBack.emit(
ScanResultState.Address(
Json.encodeToString(
SerializableAddress.serializer(),
serializableAddress
)
)
)
}

ADDRESS_BOOK -> {
navigateCommand.emit(AddContactArgs(serializableAddress.address))
}
}
}
}
ADDRESS_BOOK -> {
navigateCommand.emit(AddContactArgs(serializableAddress.address))
}
}
}

fun onScannedError() =
viewModelScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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" }
Expand Down

0 comments on commit 805a1b2

Please sign in to comment.