Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QR codes scanning improvements #1722

Merged
merged 1 commit into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading