From 08a878dcc17e5258317d7a3ec725d9fa7e0dd3fc Mon Sep 17 00:00:00 2001 From: oleksandrsarapulovgl <82441124+oleksandrsarapulovgl@users.noreply.github.com> Date: Thu, 26 Aug 2021 15:44:49 +0300 Subject: [PATCH] Feature/qr code detection update (#102) * Added scanning QR code * Added scanning of QRs fetched from taking photo * Extracted logic to fetch green certificate * Extracted request keys for adding qrs * Updated PDF scanning logic * Fixed PDF scanning logic --- .idea/sonarIssues.xml | 25 +++++++ .../DefaultGreenCertificateFetcher.kt | 66 +++++++++++++++++++ .../certificate/GreenCertificateFetcher.kt | 31 +++++++++ .../android/certificate/add/BitmapFetcher.kt | 3 +- .../certificate/add/DefaultBitmapFetcher.kt | 15 +++-- .../certificate/add/pdf/ImportPdfViewModel.kt | 38 +++++++++-- .../add/pick/image/PickImageViewModel.kt | 10 ++- .../add/take/photo/TakePhotoViewModel.kt | 11 +++- .../claim/ClaimCertificateViewModel.kt | 36 ++-------- .../wallet/app/android/di/DecoderModule.kt | 20 ++++++ 10 files changed, 207 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/dgca/wallet/app/android/certificate/DefaultGreenCertificateFetcher.kt create mode 100644 app/src/main/java/dgca/wallet/app/android/certificate/GreenCertificateFetcher.kt diff --git a/.idea/sonarIssues.xml b/.idea/sonarIssues.xml index 1b3e5c29..90092077 100644 --- a/.idea/sonarIssues.xml +++ b/.idea/sonarIssues.xml @@ -108,6 +108,16 @@ + + + + + + + + + + @@ -158,6 +168,11 @@ + + + + + @@ -603,11 +618,21 @@ + + + + + + + + + + diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/DefaultGreenCertificateFetcher.kt b/app/src/main/java/dgca/wallet/app/android/certificate/DefaultGreenCertificateFetcher.kt new file mode 100644 index 00000000..a909d726 --- /dev/null +++ b/app/src/main/java/dgca/wallet/app/android/certificate/DefaultGreenCertificateFetcher.kt @@ -0,0 +1,66 @@ +/* + * ---license-start + * eu-digital-green-certificates / dgca-wallet-app-android + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + * + * Created by osarapulov on 8/26/21 10:36 AM + */ + +package dgca.wallet.app.android.certificate + +import dgca.verifier.app.decoder.base45.Base45Service +import dgca.verifier.app.decoder.cbor.CborService +import dgca.verifier.app.decoder.compression.CompressorService +import dgca.verifier.app.decoder.cose.CoseService +import dgca.verifier.app.decoder.model.GreenCertificate +import dgca.verifier.app.decoder.model.VerificationResult +import dgca.verifier.app.decoder.prefixvalidation.PrefixValidationService +import dgca.verifier.app.decoder.schema.SchemaValidator +import timber.log.Timber + +class DefaultGreenCertificateFetcher( + private val prefixValidationService: PrefixValidationService, + private val base45Service: Base45Service, + private val compressorService: CompressorService, + private val coseService: CoseService, + private val schemaValidator: SchemaValidator, + private val cborService: CborService, +) : GreenCertificateFetcher { + override fun fetchDataFromQrString(qrString: String): Pair { + val verificationResult = VerificationResult() + val plainInput = prefixValidationService.decode(qrString, verificationResult) + val compressedCose = base45Service.decode(plainInput, verificationResult) + val coseResult: ByteArray? = compressorService.decode(compressedCose, verificationResult) + + if (coseResult == null) { + Timber.d("Verification failed: Too many bytes read") + return Pair(null, null) + } + val cose: ByteArray = coseResult + + val coseData = coseService.decode(cose, verificationResult) + if (coseData == null) { + Timber.d("Verification failed: COSE not decoded") + return Pair(cose, null) + } + + schemaValidator.validate(coseData.cbor, verificationResult) + return Pair(cose, cborService.decode(coseData.cbor, verificationResult)) + } + + override fun fetchGreenCertificateFromQrString(qrString: String): GreenCertificate? = fetchDataFromQrString(qrString).second +} \ No newline at end of file diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/GreenCertificateFetcher.kt b/app/src/main/java/dgca/wallet/app/android/certificate/GreenCertificateFetcher.kt new file mode 100644 index 00000000..30b79e9c --- /dev/null +++ b/app/src/main/java/dgca/wallet/app/android/certificate/GreenCertificateFetcher.kt @@ -0,0 +1,31 @@ +/* + * ---license-start + * eu-digital-green-certificates / dgca-wallet-app-android + * --- + * Copyright (C) 2021 T-Systems International GmbH and all other contributors + * --- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ---license-end + * + * Created by osarapulov on 8/26/21 10:35 AM + */ + +package dgca.wallet.app.android.certificate + +import dgca.verifier.app.decoder.model.GreenCertificate + +interface GreenCertificateFetcher { + fun fetchDataFromQrString(qrString: String): Pair + + fun fetchGreenCertificateFromQrString(qrString: String): GreenCertificate? +} \ No newline at end of file diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/add/BitmapFetcher.kt b/app/src/main/java/dgca/wallet/app/android/certificate/add/BitmapFetcher.kt index c5a3a2b9..b0d5679c 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/add/BitmapFetcher.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/add/BitmapFetcher.kt @@ -28,5 +28,6 @@ import android.net.Uri interface BitmapFetcher { fun loadBitmapByImageUri(uri: Uri): Bitmap - fun loadBitmapByPdfUri(uri: Uri): Bitmap + @Throws(Exception::class) + fun loadBitmapByPdfUri(uri: Uri): List } \ No newline at end of file diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/add/DefaultBitmapFetcher.kt b/app/src/main/java/dgca/wallet/app/android/certificate/add/DefaultBitmapFetcher.kt index fc639718..1504f485 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/add/DefaultBitmapFetcher.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/add/DefaultBitmapFetcher.kt @@ -41,14 +41,19 @@ class DefaultBitmapFetcher(context: Context) : BitmapFetcher { }.copy(Bitmap.Config.ARGB_8888, true) } - override fun loadBitmapByPdfUri(uri: Uri): Bitmap = + @Throws(Exception::class) + override fun loadBitmapByPdfUri(uri: Uri): List = appContext.contentResolver.openFileDescriptor(uri, "r")!!.use { fileDescriptor -> PdfRenderer(fileDescriptor).use { pdfRenderer -> - pdfRenderer.openPage(0).use { page -> - val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) - page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - bitmap + val bitmaps = mutableListOf() + for (i in 0 until pdfRenderer.pageCount) { + pdfRenderer.openPage(i).use { page -> + val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) + page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + bitmaps.add(bitmap) + } } + bitmaps.toList() } } } \ No newline at end of file diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/add/pdf/ImportPdfViewModel.kt b/app/src/main/java/dgca/wallet/app/android/certificate/add/pdf/ImportPdfViewModel.kt index 79c9e401..76cfedae 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/add/pdf/ImportPdfViewModel.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/add/pdf/ImportPdfViewModel.kt @@ -22,18 +22,22 @@ package dgca.wallet.app.android.certificate.add.pdf +import android.graphics.Bitmap import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dgca.verifier.app.decoder.model.GreenCertificate +import dgca.wallet.app.android.certificate.GreenCertificateFetcher import dgca.wallet.app.android.certificate.add.BitmapFetcher import dgca.wallet.app.android.certificate.add.FileSaver import dgca.wallet.app.android.certificate.add.QrCodeFetcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject sealed class ImportPdfResult { @@ -46,7 +50,8 @@ sealed class ImportPdfResult { class ImportPdfViewModel @Inject constructor( private val bitmapFetcher: BitmapFetcher, private val qrCodeFetcher: QrCodeFetcher, - private val fileSaver: FileSaver + private val fileSaver: FileSaver, + private val greenCertificateFetcher: GreenCertificateFetcher ) : ViewModel() { private val _result = MutableLiveData() val result: LiveData = _result @@ -60,15 +65,34 @@ class ImportPdfViewModel @Inject constructor( } private fun Uri.handle(): ImportPdfResult { - val qrCodeString: String? = try { - bitmapFetcher.loadBitmapByPdfUri(this) - .let { bitmap -> qrCodeFetcher.fetchQrCodeString(bitmap) } + val qrStrings = mutableListOf() + var bitmaps: List? = null + try { + bitmaps = bitmapFetcher.loadBitmapByPdfUri(this) + bitmaps.forEach { bitmap -> + qrStrings.add(qrCodeFetcher.fetchQrCodeString(bitmap)) + bitmap.recycle() + } } catch (exception: Exception) { - null + Timber.d(exception, "Error fetching qr strings from bitmaps") + } finally { + bitmaps?.forEach { bitmap -> bitmap.recycle() } + } + + var qrString = "" + var greenCertificate: GreenCertificate? = null + + qrStrings.forEach { curQrString -> + val curGreenCertificate = greenCertificateFetcher.fetchGreenCertificateFromQrString(curQrString) + if (curGreenCertificate != null) { + qrString = curQrString + greenCertificate = curGreenCertificate + return@forEach + } } - return if (qrCodeString?.isNotBlank() == true) { - ImportPdfResult.QrRecognised(qrCodeString) + return if (greenCertificate != null) { + ImportPdfResult.QrRecognised(qrString) } else { val file = try { fileSaver.saveFileFromUri(this, "images", "${System.currentTimeMillis()}.pdf") diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/add/pick/image/PickImageViewModel.kt b/app/src/main/java/dgca/wallet/app/android/certificate/add/pick/image/PickImageViewModel.kt index 610d4274..36d4ac84 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/add/pick/image/PickImageViewModel.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/add/pick/image/PickImageViewModel.kt @@ -28,6 +28,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dgca.verifier.app.decoder.model.GreenCertificate +import dgca.wallet.app.android.certificate.GreenCertificateFetcher import dgca.wallet.app.android.certificate.add.BitmapFetcher import dgca.wallet.app.android.certificate.add.FileSaver import dgca.wallet.app.android.certificate.add.QrCodeFetcher @@ -46,7 +48,8 @@ sealed class PickImageResult { class PickImageViewModel @Inject constructor( private val qrCodeFetcher: QrCodeFetcher, private val bitmapFetcher: BitmapFetcher, - private val fileSaver: FileSaver + private val fileSaver: FileSaver, + private val greenCertificateFetcher: GreenCertificateFetcher ) : ViewModel() { private val _result = MutableLiveData() val result: LiveData = _result @@ -66,7 +69,10 @@ class PickImageViewModel @Inject constructor( null } - return if (qrCodeString?.isNotBlank() == true) { + val greenCertificate: GreenCertificate? = + qrCodeString?.let { qrString -> greenCertificateFetcher.fetchGreenCertificateFromQrString(qrString) } + + return if (greenCertificate != null) { PickImageResult.QrRecognised(qrCodeString) } else { val file = try { diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/add/take/photo/TakePhotoViewModel.kt b/app/src/main/java/dgca/wallet/app/android/certificate/add/take/photo/TakePhotoViewModel.kt index d1c4f1d4..c3971a73 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/add/take/photo/TakePhotoViewModel.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/add/take/photo/TakePhotoViewModel.kt @@ -28,6 +28,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dgca.verifier.app.decoder.model.GreenCertificate +import dgca.wallet.app.android.certificate.GreenCertificateFetcher import dgca.wallet.app.android.certificate.add.BitmapFetcher import dgca.wallet.app.android.certificate.add.FileSaver import dgca.wallet.app.android.certificate.add.QrCodeFetcher @@ -48,7 +50,8 @@ class TakePhotoViewModel @Inject constructor( private val qrCodeFetcher: QrCodeFetcher, private val bitmapFetcher: BitmapFetcher, private val uriProvider: UriProvider, - private val fileSaver: FileSaver + private val fileSaver: FileSaver, + private val greenCertificateFetcher: GreenCertificateFetcher ) : ViewModel() { val uriLiveData: LiveData = MutableLiveData(uriProvider.getUriFor("temp", "temp.jpeg")) private val _result = MutableLiveData() @@ -70,9 +73,13 @@ class TakePhotoViewModel @Inject constructor( } catch (exception: Exception) { null } + val greenCertificate: GreenCertificate? = + qrCodeString?.let { qrString -> greenCertificateFetcher.fetchGreenCertificateFromQrString(qrString) } return when { - qrCodeString?.isNotBlank() == true && uriProvider.deleteFileByUri(this) -> { + greenCertificate != null && uriProvider.deleteFileByUri( + this + ) -> { TakePhotoResult.QrRecognised(qrCodeString) } else -> { diff --git a/app/src/main/java/dgca/wallet/app/android/certificate/claim/ClaimCertificateViewModel.kt b/app/src/main/java/dgca/wallet/app/android/certificate/claim/ClaimCertificateViewModel.kt index fcb543ca..d5c2f8b7 100644 --- a/app/src/main/java/dgca/wallet/app/android/certificate/claim/ClaimCertificateViewModel.kt +++ b/app/src/main/java/dgca/wallet/app/android/certificate/claim/ClaimCertificateViewModel.kt @@ -28,16 +28,11 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dgca.verifier.app.decoder.* -import dgca.verifier.app.decoder.base45.Base45Service -import dgca.verifier.app.decoder.cbor.CborService -import dgca.verifier.app.decoder.compression.CompressorService -import dgca.verifier.app.decoder.cose.CoseService import dgca.verifier.app.decoder.model.GreenCertificate -import dgca.verifier.app.decoder.model.VerificationResult import dgca.verifier.app.decoder.prefixvalidation.PrefixValidationService -import dgca.verifier.app.decoder.schema.SchemaValidator import dgca.wallet.app.android.BuildConfig import dgca.wallet.app.android.Event +import dgca.wallet.app.android.certificate.GreenCertificateFetcher import dgca.wallet.app.android.data.CertificateModel import dgca.wallet.app.android.data.ConfigRepository import dgca.wallet.app.android.data.WalletRepository @@ -56,12 +51,8 @@ import javax.inject.Inject @HiltViewModel class ClaimCertificateViewModel @Inject constructor( + private val greenCertificateFetcher: GreenCertificateFetcher, private val prefixValidationService: PrefixValidationService, - private val base45Service: Base45Service, - private val compressorService: CompressorService, - private val coseService: CoseService, - private val schemaValidator: SchemaValidator, - private val cborService: CborService, private val configRepository: ConfigRepository, private val walletRepository: WalletRepository ) : ViewModel() { @@ -81,26 +72,9 @@ class ClaimCertificateViewModel @Inject constructor( fun init(qrCodeText: String) { viewModelScope.launch { _inProgress.value = true - withContext(Dispatchers.IO) { - val verificationResult = VerificationResult() - val plainInput = prefixValidationService.decode(qrCodeText, verificationResult) - val compressedCose = base45Service.decode(plainInput, verificationResult) - val coseResult: ByteArray? = compressorService.decode(compressedCose, verificationResult) - - if (coseResult == null) { - Timber.d("Verification failed: Too many bytes read") - return@withContext - } - cose = coseResult - - val coseData = coseService.decode(cose, verificationResult) - if (coseData == null) { - Timber.d("Verification failed: COSE not decoded") - return@withContext - } - - schemaValidator.validate(coseData.cbor, verificationResult) - greenCertificate = cborService.decode(coseData.cbor, verificationResult) + withContext(Dispatchers.IO) { greenCertificateFetcher.fetchDataFromQrString(qrCodeText) }.apply { + cose = first ?: cose + greenCertificate = second } _inProgress.value = false _certificate.value = greenCertificate?.toCertificateModel() diff --git a/app/src/main/java/dgca/wallet/app/android/di/DecoderModule.kt b/app/src/main/java/dgca/wallet/app/android/di/DecoderModule.kt index 3ba2fb16..1fe850e2 100644 --- a/app/src/main/java/dgca/wallet/app/android/di/DecoderModule.kt +++ b/app/src/main/java/dgca/wallet/app/android/di/DecoderModule.kt @@ -44,6 +44,8 @@ import dgca.verifier.app.decoder.prefixvalidation.PrefixValidationService import dgca.verifier.app.decoder.schema.DefaultSchemaValidator import dgca.verifier.app.decoder.schema.SchemaValidator import dgca.verifier.app.decoder.services.X509 +import dgca.wallet.app.android.certificate.DefaultGreenCertificateFetcher +import dgca.wallet.app.android.certificate.GreenCertificateFetcher import javax.inject.Singleton /** @@ -93,4 +95,22 @@ object DecoderModule { @Singleton @Provides fun provideCryptoService(x509: X509): CryptoService = VerificationCryptoService(x509) + + @Singleton + @Provides + fun provide( + prefixValidationService: PrefixValidationService, + base45Service: Base45Service, + compressorService: CompressorService, + coseService: CoseService, + schemaValidator: SchemaValidator, + cborService: CborService, + ): GreenCertificateFetcher = DefaultGreenCertificateFetcher( + prefixValidationService, + base45Service, + compressorService, + coseService, + schemaValidator, + cborService + ) } \ No newline at end of file