diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt index e17a6f67242c845a2f5c6bef145ef5e04e334215..dbd4e65bd6e012ebd5efadd2e154f5a5ede65d1a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/ui/ErrorDialog.kt @@ -41,21 +41,22 @@ private fun MaterialAlertDialogBuilder.setMessageView( setView(textView) } -fun Throwable.toErrorDialogBuilder(context: Context) = MaterialAlertDialogBuilder(context).apply { - val error = this@toErrorDialogBuilder - val humanReadable = error.tryHumanReadableError(context) - - setTitle(humanReadable.title ?: context.getString(R.string.errors_generic_headline_short)) - setMessageView(humanReadable.description, textHasLinks = true) - - setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> } - - setNeutralButton(R.string.errors_generic_button_negative) { _, _ -> - MaterialAlertDialogBuilder(context).apply { - setMessageView( - error.toString() + "\n\n" + error.stackTraceToString(), - textHasLinks = false - ) - }.show() +fun Throwable.toErrorDialogBuilder(context: Context) = + MaterialAlertDialogBuilder(context).apply { + val error = this@toErrorDialogBuilder + val humanReadable = error.tryHumanReadableError(context) + + setTitle(humanReadable.title ?: context.getString(R.string.errors_generic_headline_short)) + setMessageView(humanReadable.description, textHasLinks = true) + + setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> } + + setNeutralButton(R.string.errors_generic_button_negative) { _, _ -> + MaterialAlertDialogBuilder(context).apply { + setMessageView( + error.toString() + "\n\n" + error.stackTraceToString(), + textHasLinks = false + ) + }.show() + } } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt index 8ba4b3d16c63934c206fd5f958151d0f20306a8a..abd463895d6662bb2066c423c8e3ecdf9350ac3c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt @@ -38,9 +38,9 @@ import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod import de.rki.coronawarnapp.util.lists.modular.mods.SavedStateMod import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod -import de.rki.coronawarnapp.vaccination.ui.homecards.CompleteVaccinationHomeCard -import de.rki.coronawarnapp.vaccination.ui.homecards.CreateVaccinationHomeCard -import de.rki.coronawarnapp.vaccination.ui.homecards.IncompleteVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.CompleteVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.CreateVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.IncompleteVaccinationHomeCard class HomeAdapter : ModularAdapter<HomeAdapter.HomeItemVH<HomeItem, ViewBinding>>(), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index e8a0344d8b8e8b636f882c3e1218e6c34edcf1e5..cb5e2c0458ce034d24aa54891b60b592bd331bd6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -74,9 +74,9 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository -import de.rki.coronawarnapp.vaccination.ui.homecards.CompleteVaccinationHomeCard -import de.rki.coronawarnapp.vaccination.ui.homecards.CreateVaccinationHomeCard -import de.rki.coronawarnapp.vaccination.ui.homecards.IncompleteVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.CompleteVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.CreateVaccinationHomeCard +import de.rki.coronawarnapp.vaccination.ui.homecard.IncompleteVaccinationHomeCard import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -366,7 +366,7 @@ class HomeFragmentViewModel @AssistedInject constructor( add( CreateVaccinationHomeCard.Item( onClickAction = { - // TODO: implement in another PR + routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToVaccinationNavGraph()) } ) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt index 767df359cbdf638d3a242a3778d569878863c953..47ddf2f674d5fc0ba24113a654e00d04e7677198 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeFragment.kt @@ -12,7 +12,7 @@ import com.google.android.material.transition.MaterialContainerTransform import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentScanCheckInQrCodeBinding +import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragment import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject @@ -26,13 +26,13 @@ import timber.log.Timber import javax.inject.Inject class ScanCheckInQrCodeFragment : - Fragment(R.layout.fragment_scan_check_in_qr_code), + Fragment(R.layout.fragment_scan_qr_code), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory private val viewModel: ScanCheckInQrCodeViewModel by cwaViewModels { viewModelFactory } - private val binding: FragmentScanCheckInQrCodeBinding by viewBindingLazy() + private val binding: FragmentScanQrCodeBinding by viewBindingLazy() private var showsPermissionDialog = false override fun onCreate(savedInstanceState: Bundle?) { @@ -47,13 +47,13 @@ class ScanCheckInQrCodeFragment : savedInstanceState: Bundle? ) { with(binding) { - checkInQrCodeScanTorch.setOnCheckedChangeListener { _, isChecked -> - binding.checkInQrCodeScanPreview.setTorch(isChecked) + qrCodeScanTorch.setOnCheckedChangeListener { _, isChecked -> + binding.qrCodeScanPreview.setTorch(isChecked) } - checkInQrCodeScanToolbar.setNavigationOnClickListener { viewModel.onNavigateUp() } - checkInQrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) - checkInQrCodeScanViewfinderView.setCameraPreview(binding.checkInQrCodeScanPreview) + qrCodeScanToolbar.setNavigationOnClickListener { viewModel.onNavigateUp() } + qrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + qrCodeScanViewfinderView.setCameraPreview(binding.qrCodeScanPreview) } viewModel.events.observe2(this) { navEvent -> @@ -76,7 +76,7 @@ class ScanCheckInQrCodeFragment : super.onResume() binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT) if (CameraPermissionHelper.hasCameraPermission(requireActivity())) { - binding.checkInQrCodeScanPreview.resume() + binding.qrCodeScanPreview.resume() startDecode() return } @@ -104,7 +104,7 @@ class ScanCheckInQrCodeFragment : } } - private fun startDecode() = binding.checkInQrCodeScanPreview + private fun startDecode() = binding.qrCodeScanPreview .decodeSingle { barcodeResult -> viewModel.onScanResult(barcodeResult) } @@ -156,7 +156,7 @@ class ScanCheckInQrCodeFragment : override fun onPause() { super.onPause() - binding.checkInQrCodeScanPreview.pause() + binding.qrCodeScanPreview.pause() } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt index c3cb9a33a9b0a32bc990ea329ebfe58d593955ed..6cb56193106107c879c31397d94b69503b4ca6db 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/InvalidHealthCertificateException.kt @@ -1,10 +1,32 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import android.content.Context +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.util.HasHumanReadableError +import de.rki.coronawarnapp.util.HumanReadableError +import de.rki.coronawarnapp.util.ui.CachedString +import de.rki.coronawarnapp.util.ui.LazyString +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_STORING_FAILED class InvalidHealthCertificateException( val errorCode: ErrorCode -) : InvalidQRCodeException(errorCode.message) { +) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) { enum class ErrorCode( val message: String ) { @@ -13,6 +35,61 @@ class InvalidHealthCertificateException( HC_COSE_TAG_INVALID("COSE tag invalid."), HC_COSE_MESSAGE_INVALID("COSE message invalid."), HC_CBOR_DECODING_FAILED("CBOR decoding failed."), - VC_NO_VACCINATION_ENTRY("Vaccination certificate missing.") + VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."), + VC_PREFIX_INVALID("Prefix invalid."), + VC_STORING_FAILED("Storing failed."), + VC_JSON_SCHEMA_INVALID("Json schema invalid."), + VC_NAME_MISMATCH("Name does not match."), + VC_ALREADY_REGISTERED("Certificate already registered."), + VC_DOB_MISMATCH("Date of birth does not match."), + VC_HC_CWT_NO_DGC("Dgc missing."), + VC_HC_CWT_NO_EXP("Expiration date missing."), + VC_HC_CWT_NO_HCERT("Health certificate missing."), + VC_HC_CWT_NO_ISS("Issuer missing."), + } + + val errorMessage: LazyString + get() = when (errorCode) { + HC_BASE45_DECODING_FAILED, + HC_CBOR_DECODING_FAILED, + HC_COSE_MESSAGE_INVALID, + HC_ZLIB_DECOMPRESSION_FAILED, + HC_COSE_TAG_INVALID, + VC_PREFIX_INVALID, + VC_HC_CWT_NO_DGC, + VC_HC_CWT_NO_EXP, + VC_HC_CWT_NO_HCERT, + VC_HC_CWT_NO_ISS, + VC_JSON_SCHEMA_INVALID, + -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_INVALID) + } + VC_NO_VACCINATION_ENTRY -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) + } + VC_STORING_FAILED -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) + } + VC_NAME_MISMATCH, VC_DOB_MISMATCH -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON) + } + VC_ALREADY_REGISTERED -> CachedString { context -> + context.getString(ERROR_MESSAGE_ALREADY_REGISTERED) + } + } + + override fun toHumanReadableError(context: Context): HumanReadableError { + var errorCodeString = errorCode.toString() + errorCodeString = if (errorCodeString.startsWith(PREFIX)) errorCodeString else PREFIX + errorCodeString + return HumanReadableError( + description = errorMessage.get(context) + "\n\n$errorCodeString" + ) } } + +private const val PREFIX = "VC_" +private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid +private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported +private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again +private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person +private const val ERROR_MESSAGE_ALREADY_REGISTERED = R.string.error_vc_already_registered diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt index b2dd023f44b48ddfe09babde27752af040b07e29..03c53611c16aeae70b08dc8538bee6c069327d2c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateCOSEParser.kt @@ -30,7 +30,7 @@ class VaccinationCertificateCOSEParser @Inject constructor( private fun CBORObject.decodeCBORObject(): VaccinationCertificateData { return try { - vaccinationCertificateV1Parser.decode(this) + vaccinationCertificateV1Parser.parse(this) } catch (e: Exception) { Timber.e(e) throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt index 7e73d4bc154b5602adde09ed6518bfa63dd94b34..0ffceb930dc079c8694c2f1537cd971f70167fec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationCertificateV1Parser.kt @@ -2,13 +2,21 @@ package de.rki.coronawarnapp.vaccination.core.qrcode import com.google.gson.Gson import com.upokecenter.cbor.CBORObject +import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.fromJson import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import org.joda.time.Instant import javax.inject.Inject -class VaccinationCertificateV1Parser @Inject constructor() { +class VaccinationCertificateV1Parser @Inject constructor( + @BaseGson private val gson: Gson +) { companion object { private val keyEuDgcV1 = CBORObject.FromObject(1) @@ -18,41 +26,43 @@ class VaccinationCertificateV1Parser @Inject constructor() { private val keyIssuedAt = CBORObject.FromObject(6) } - fun decode(map: CBORObject): VaccinationCertificateData { - try { - var issuer: String? = null - map[keyIssuer]?.let { - issuer = it.AsString() - } - var issuedAt: Instant? = null - map[keyIssuedAt]?.let { - issuedAt = Instant.ofEpochSecond(it.AsNumber().ToInt64Checked()) - } - var expiresAt: Instant? = null - map[keyExpiresAt]?.let { - expiresAt = Instant.ofEpochSecond(it.AsNumber().ToInt64Checked()) - } - var certificate: VaccinationCertificateV1? = null - map[keyHCert]?.let { hcert -> - hcert[keyEuDgcV1]?.let { - val json = it.ToJSONString() - certificate = Gson().fromJson<VaccinationCertificateV1>(json) - } - } - val header = VaccinationCertificateHeader( - issuer = issuer!!, - issuedAt = issuedAt!!, - expiresAt = expiresAt!! - ) - return VaccinationCertificateData( - header, - certificate!!.validate() - ) - } catch (e: InvalidHealthCertificateException) { - throw e - } catch (e: Throwable) { - throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) - } + fun parse(map: CBORObject): VaccinationCertificateData = try { + val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS) + + val issuedAt: Instant = map[keyIssuedAt]?.run { + Instant.ofEpochSecond(AsNumber().ToInt64Checked()) + } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS) + + val expiresAt: Instant = map[keyExpiresAt]?.run { + Instant.ofEpochSecond(AsNumber().ToInt64Checked()) + } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_EXP) + + val certificate: VaccinationCertificateV1 = map[keyHCert]?.run { + this[keyEuDgcV1]?.run { + toCertificate() + } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC) + } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT) + + val header = VaccinationCertificateHeader( + issuer = issuer, + issuedAt = issuedAt, + expiresAt = expiresAt + ) + VaccinationCertificateData( + header, + certificate.validate() + ) + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) + } + + private fun CBORObject.toCertificate() = try { + val json = ToJSONString() + gson.fromJson<VaccinationCertificateV1>(json) + } catch (e: Throwable) { + throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID) } private fun VaccinationCertificateV1.validate(): VaccinationCertificateV1 { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt index 3c4ac6d4912d0a207bbf2ac8a1a574ff2cd3f5b0..b2d9308d76d79eb7f9bc7cf4e1f093d0da8545ef 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt @@ -20,18 +20,18 @@ class VaccinationQRCodeExtractor @Inject constructor( private val prefix = "HC1:" - override fun canHandle(rawString: String): Boolean { - return rawString.startsWith(prefix) - } + override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix) override fun extract(rawString: String): VaccinationCertificateQRCode { val rawCOSEObject = rawString .removePrefix(prefix) .decodeBase45() .decompress() + val certificate = rawCOSEObject .decodeCOSEObject() - .decodeCBORObject() + .parseCBORObject() + return VaccinationCertificateQRCode( parsedData = certificate, certificateCOSE = rawCOSEObject, @@ -40,14 +40,14 @@ class VaccinationQRCodeExtractor @Inject constructor( private fun String.decodeBase45(): ByteArray = try { base45Decoder.decode(this) - } catch (e: Exception) { + } catch (e: Throwable) { Timber.e(e) throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED) } private fun ByteArray.decompress(): ByteArray = try { - zLIBDecompressor.decode(this) - } catch (e: Exception) { + zLIBDecompressor.decompress(this) + } catch (e: Throwable) { Timber.e(e) throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) } @@ -56,16 +56,16 @@ class VaccinationQRCodeExtractor @Inject constructor( healthCertificateCOSEDecoder.decode(this) } catch (e: InvalidHealthCertificateException) { throw e - } catch (e: Exception) { + } catch (e: Throwable) { Timber.e(e) throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID) } - private fun CBORObject.decodeCBORObject(): VaccinationCertificateData = try { - vaccinationCertificateV1Parser.decode(this) + private fun CBORObject.parseCBORObject(): VaccinationCertificateData = try { + vaccinationCertificateV1Parser.parse(this) } catch (e: InvalidHealthCertificateException) { throw e - } catch (e: Exception) { + } catch (e: Throwable) { Timber.e(e) throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt index cf074f8c459819031adeaacf2b1dd07c8231928e..07fb8293cf6413aad498f312d487ee82781245c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.vaccination.core.qrcode import dagger.Reusable -import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID import timber.log.Timber import javax.inject.Inject @@ -16,7 +16,7 @@ class VaccinationQRCodeValidator @Inject constructor( return findExtractor(rawString) ?.extract(rawString) ?.also { Timber.i("Extracted data from QR code is $it") } - ?: throw InvalidQRCodeException() + ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID) } private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt index 978864bd0b247ecb976bdc856f8529136182682c..79d3dad89e1dbdadb576a09dd8f7fd122986f810 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/storage/VaccinationContainer.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.vaccination.core.repository.storage import androidx.annotation.Keep +import com.google.gson.Gson import com.google.gson.annotations.SerializedName import de.rki.coronawarnapp.ui.Country import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier @@ -35,7 +36,7 @@ data class VaccinationContainer( private val certificateData: VaccinationCertificateData by lazy { preParsedData ?: VaccinationCertificateCOSEParser( HealthCertificateCOSEDecoder(), - VaccinationCertificateV1Parser(), + VaccinationCertificateV1Parser(Gson()), ).parse(vaccinationCertificateCOSE) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt index 8355af38ffa4b02533891c302ad95ab4d63a709c..b79a188440708d8f8f1f298e5096c91705e10a10 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/decoder/ZLIBDecompressor.kt @@ -5,7 +5,7 @@ import java.util.zip.InflaterInputStream import javax.inject.Inject class ZLIBDecompressor @Inject constructor() { - fun decode(input: ByteArray): ByteArray = if ( + fun decompress(input: ByteArray): ByteArray = if ( input.size >= 2 && input[0] == 0x78.toByte() && input[1] in listOf(0x01.toByte(), 0x5E.toByte(), 0x9C.toByte(), 0xDA.toByte()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt index 78afda0dd1b30f992f42528275d67e1a909e4420..9c5e2af207f1b60145cd75413de8522179a03ad7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/VaccinationUIModule.kt @@ -6,6 +6,8 @@ import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragment import de.rki.coronawarnapp.vaccination.ui.details.VaccinationDetailsFragmentModule import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragment import de.rki.coronawarnapp.vaccination.ui.list.VaccinationListFragmentModule +import de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanFragment +import de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanModule @Module abstract class VaccinationUIModule { @@ -15,4 +17,7 @@ abstract class VaccinationUIModule { @ContributesAndroidInjector(modules = [VaccinationDetailsFragmentModule::class]) abstract fun vaccinationDetailsFragment(): VaccinationDetailsFragment + + @ContributesAndroidInjector(modules = [VaccinationQrCodeScanModule::class]) + abstract fun vaccinationQrCodeScanFragment(): VaccinationQrCodeScanFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt similarity index 96% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt index 17965fc0f43db77bd64ce10681508f3081861655..f64b6f3fdaac4f755b4df64958c486619b5b135f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CompleteVaccinationHomeCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CompleteVaccinationHomeCard.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.vaccination.ui.homecards +package de.rki.coronawarnapp.vaccination.ui.homecard import android.view.ViewGroup import de.rki.coronawarnapp.R diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt similarity index 87% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt index d0a98e8a129130882ee02e013bdff80c6b042537..4f807e7ae20844dc457f4b45242ba32301eb9eab 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/CreateVaccinationHomeCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/CreateVaccinationHomeCard.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.vaccination.ui.homecards +package de.rki.coronawarnapp.vaccination.ui.homecard import android.view.ViewGroup import de.rki.coronawarnapp.R @@ -22,10 +22,17 @@ class CreateVaccinationHomeCard(parent: ViewGroup) : payloads: List<Any> ) -> Unit = { item, payloads -> - itemView.setOnClickListener { + fun onClick() { val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item curItem.onClickAction(item) } + + itemView.setOnClickListener { + onClick() + } + nextStepsAction.setOnClickListener { + onClick() + } } data class Item(val onClickAction: (Item) -> Unit) : HomeItem, HasPayloadDiffer { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt similarity index 96% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt index 37472a274df59c1acc8b14752c260bcc810f44da..915eb6d6802170e12083d67b394a2b7cf623e55d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/IncompleteVaccinationHomeCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/IncompleteVaccinationHomeCard.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.vaccination.ui.homecards +package de.rki.coronawarnapp.vaccination.ui.homecard import android.view.ViewGroup import de.rki.coronawarnapp.R diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt similarity index 85% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt index 5968607a69242fb9f81af3997ade2335978e40c9..92bc3708f9d994c3fca82ab6ec648a092bae6dfb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecards/VaccinationStatusItem.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/homecard/VaccinationStatusItem.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.vaccination.ui.homecards +package de.rki.coronawarnapp.vaccination.ui.homecard import de.rki.coronawarnapp.ui.main.home.items.HomeItem import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc4c36ec027b8316284d668df909b65be66eb821 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt @@ -0,0 +1,163 @@ +package de.rki.coronawarnapp.vaccination.ui.scan + +import android.Manifest +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT +import androidx.fragment.app.Fragment +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder +import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.permission.CameraPermissionHelper +import de.rki.coronawarnapp.util.ui.doNavigate +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +class VaccinationQrCodeScanFragment : + Fragment(R.layout.fragment_scan_qr_code), + AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: VaccinationQrCodeScanViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentScanQrCodeBinding by viewBindingLazy() + private var showsPermissionDialog = false + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + with(binding) { + qrCodeScanTorch.setOnCheckedChangeListener { _, isChecked -> + binding.qrCodeScanPreview.setTorch(isChecked) + } + + qrCodeScanToolbar.setNavigationOnClickListener { popBackStack() } + qrCodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + qrCodeScanViewfinderView.setCameraPreview(binding.qrCodeScanPreview) + qrCodeScanSpinner.hide() + } + + viewModel.event.observe(viewLifecycleOwner) { event -> + when (event) { + is VaccinationQrCodeScanViewModel.Event.QrCodeScanSucceeded -> { + binding.qrCodeScanSpinner.hide() + doNavigate( + VaccinationQrCodeScanFragmentDirections + .actionVaccinationQrCodeScanFragmentToVaccinationDetailsFragment(event.certificateId) + ) + } + VaccinationQrCodeScanViewModel.Event.QrCodeScanInProgress -> { + binding.qrCodeScanSpinner.show() + } + } + } + + viewModel.errorEvent.observe(viewLifecycleOwner) { + binding.qrCodeScanSpinner.hide() + it.toErrorDialogBuilder(requireContext()).apply { + setOnDismissListener { popBackStack() } + }.show() + } + } + + override fun onResume() { + super.onResume() + binding.checkInQrCodeScanContainer.sendAccessibilityEvent(TYPE_ANNOUNCEMENT) + if (CameraPermissionHelper.hasCameraPermission(requireActivity())) { + binding.qrCodeScanPreview.resume() + startDecode() + return + } + if (showsPermissionDialog) return + + requestCameraPermission() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array<String>, + grantResults: IntArray + ) { + if (requestCode == REQUEST_CAMERA_PERMISSION_CODE && + grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED + ) { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showCameraPermissionRationaleDialog() + viewModel.setCameraDeniedPermanently(false) + } else { + // User permanently denied access to the camera + showCameraPermissionDeniedDialog() + viewModel.setCameraDeniedPermanently(true) + } + } + } + + private fun startDecode() = binding.qrCodeScanPreview + .decodeSingle { barcodeResult -> + viewModel.onScanResult(barcodeResult) + } + + private fun showCameraPermissionDeniedDialog() { + val permissionDeniedDialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_qr_code_scan_permission_denied_dialog_headline, + R.string.submission_qr_code_scan_permission_denied_dialog_body, + R.string.submission_qr_code_scan_permission_denied_dialog_button, + cancelable = false, + positiveButtonFunction = { + leave() + } + ) + showsPermissionDialog = true + DialogHelper.showDialog(permissionDeniedDialog) + } + + private fun showCameraPermissionRationaleDialog() { + val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_qr_code_scan_permission_rationale_dialog_headline, + R.string.submission_qr_code_scan_permission_rationale_dialog_body, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_positive, + R.string.submission_qr_code_scan_permission_rationale_dialog_button_negative, + false, + positiveButtonFunction = { + showsPermissionDialog = false + requestCameraPermission() + }, + negativeButtonFunction = { + leave() + } + ) + + showsPermissionDialog = true + DialogHelper.showDialog(cameraPermissionRationaleDialogInstance) + } + + private fun requestCameraPermission() = requestPermissions( + arrayOf(Manifest.permission.CAMERA), + REQUEST_CAMERA_PERMISSION_CODE + ) + + private fun leave() { + showsPermissionDialog = false + popBackStack() + } + + override fun onPause() { + super.onPause() + binding.qrCodeScanPreview.pause() + } + + companion object { + private const val REQUEST_CAMERA_PERMISSION_CODE = 4000 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..aa5cd02dfd000d4e78466d6a5e9d5fd85f072ea7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.vaccination.ui.scan + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class VaccinationQrCodeScanModule { + @Binds + @IntoMap + @CWAViewModelKey(VaccinationQrCodeScanViewModel::class) + abstract fun vaccinationQrCodeScanFragment( + factory: VaccinationQrCodeScanViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..17b6d03349a9e7f953e69f55d66a0038fe6eb898 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt @@ -0,0 +1,47 @@ +package de.rki.coronawarnapp.vaccination.ui.scan + +import com.journeyapps.barcodescanner.BarcodeResult +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.permission.CameraSettings +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeValidator +import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository +import timber.log.Timber + +class VaccinationQrCodeScanViewModel @AssistedInject constructor( + private val cameraSettings: CameraSettings, + private val vaccinationQRCodeValidator: VaccinationQRCodeValidator, + private val vaccinationRepository: VaccinationRepository +) : CWAViewModel() { + + val event = SingleLiveEvent<Event>() + + val errorEvent = SingleLiveEvent<Throwable>() + + fun onScanResult(barcodeResult: BarcodeResult) = launch { + try { + event.postValue(Event.QrCodeScanInProgress) + val qrCode = vaccinationQRCodeValidator.validate(barcodeResult.text) + val certificate = vaccinationRepository.registerVaccination(qrCode) + event.postValue(Event.QrCodeScanSucceeded(certificate.certificateId)) + } catch (e: Throwable) { + errorEvent.postValue(e) + } + } + + fun setCameraDeniedPermanently(denied: Boolean) { + Timber.d("setCameraDeniedPermanently(denied=$denied)") + cameraSettings.isCameraDeniedPermanently.update { denied } + } + + sealed class Event { + object QrCodeScanInProgress : Event() + data class QrCodeScanSucceeded(val certificateId: String) : Event() + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<VaccinationQrCodeScanViewModel> +} diff --git a/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml b/Corona-Warn-App/src/main/res/layout/fragment_scan_qr_code.xml similarity index 71% rename from Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml rename to Corona-Warn-App/src/main/res/layout/fragment_scan_qr_code.xml index 80845bfcf2153083902983d8bd6d40c65797ab08..2b4f586ab41d81c60a490a7912cf8af3caabf93d 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_scan_check_in_qr_code.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_scan_qr_code.xml @@ -8,7 +8,7 @@ android:transitionName="shared_element_container"> <com.journeyapps.barcodescanner.BarcodeView - android:id="@+id/check_in_qr_code_scan_preview" + android:id="@+id/qr_code_scan_preview" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" @@ -21,7 +21,7 @@ </com.journeyapps.barcodescanner.BarcodeView> <com.journeyapps.barcodescanner.ViewfinderView - android:id="@+id/check_in_qr_code_scan_viewfinder_view" + android:id="@+id/qr_code_scan_viewfinder_view" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" @@ -32,19 +32,31 @@ app:zxing_viewfinder_laser_visibility="false" /> <TextView - android:id="@+id/check_in_qr_code_scan_body" + android:id="@+id/qr_code_scan_body" style="@style/qrCodeScanBody" android:layout_width="@dimen/scan_qr_code_viewfinder_size" android:layout_height="wrap_content" android:layout_marginTop="@dimen/scan_qr_code_viewfinder_center_offset" android:text="@string/qr_code_scan_body" - app:layout_constraintEnd_toEndOf="@id/check_in_qr_code_scan_preview" - app:layout_constraintStart_toStartOf="@id/check_in_qr_code_scan_preview" - app:layout_constraintTop_toBottomOf="@id/check_in_qr_code_scan_guideline_center" /> + app:layout_constraintEnd_toEndOf="@id/qr_code_scan_preview" + app:layout_constraintStart_toStartOf="@id/qr_code_scan_preview" + app:layout_constraintTop_toBottomOf="@id/qr_code_scan_guideline_center" /> + <com.google.android.material.progressindicator.CircularProgressIndicator + android:id="@+id/qr_code_scan_spinner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:indeterminate="true" + app:indicatorColor="@android:color/white" + app:indicatorSize="64dp" + app:trackColor="@android:color/transparent" + app:layout_constraintEnd_toEndOf="@id/qr_code_scan_body" + app:layout_constraintStart_toStartOf="@id/qr_code_scan_body" + app:layout_constraintTop_toBottomOf="@id/qr_code_scan_body" /> <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/check_in_qr_code_scan_toolbar" + android:id="@+id/qr_code_scan_toolbar" style="@style/CWAToolbar.BackArrow.Transparent" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -56,7 +68,7 @@ app:titleTextColor="@color/colorQrCodeScanToolbar"> <ToggleButton - android:id="@+id/check_in_qr_code_scan_torch" + android:id="@+id/qr_code_scan_torch" android:layout_width="@dimen/icon_size_button" android:layout_height="@dimen/icon_size_button" android:layout_gravity="end" @@ -69,10 +81,10 @@ </com.google.android.material.appbar.MaterialToolbar> <androidx.constraintlayout.widget.Guideline - android:id="@+id/check_in_qr_code_scan_guideline_center" + android:id="@+id/qr_code_scan_guideline_center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.5" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index a7b1ec503cbafafe0dd89f6dfbd92f7d83a77c0b..c5c8028c76f5195eed471e00dcd9e218df7341a5 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -76,6 +76,9 @@ <action android:id="@+id/action_mainFragment_to_submissionTestResultKeysSharedFragment" app:destination="@id/submissionTestResultKeysSharedFragment" /> + <action + android:id="@+id/action_mainFragment_to_vaccinationNavGraph" + app:destination="@id/vaccination_nav_graph" /> </fragment> <fragment @@ -809,4 +812,5 @@ android:name="testType" app:argType="de.rki.coronawarnapp.coronatest.type.CoronaTest$Type" /> </fragment> + </navigation> diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml index b202c89c6ed2888b5fa2ea166927fa59979fbca3..494e80cbeffcbfbe77cf03a109ffc93dcc0a64e6 100644 --- a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml @@ -59,7 +59,7 @@ android:id="@+id/scanCheckInQrCodeFragment" android:name="de.rki.coronawarnapp.ui.presencetracing.attendee.scan.ScanCheckInQrCodeFragment" android:label="ScanCheckInQrCodeFragment" - tools:layout="@layout/fragment_scan_check_in_qr_code" /> + tools:layout="@layout/fragment_scan_qr_code" /> <fragment android:id="@+id/checkInsFragment" @@ -97,4 +97,4 @@ app:destination="@id/checkInOnboardingFragment" /> </fragment> -</navigation> \ No newline at end of file +</navigation> diff --git a/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml index 5046d21d072fa7808d9479585e23f6eba1546063..a0c4e7a1a2e0edc2b0cd07f9ab73c611727e6800 100644 --- a/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/vaccination_nav_graph.xml @@ -3,7 +3,19 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/vaccination_nav_graph" - app:startDestination="@id/vaccinationDetailsFragment"> + app:startDestination="@id/vaccinationQrCodeScanFragment"> + + <fragment + android:id="@+id/vaccinationQrCodeScanFragment" + android:name="de.rki.coronawarnapp.vaccination.ui.scan.VaccinationQrCodeScanFragment" + android:label="VaccinationQrCodeScanFragment" + tools:layout="@layout/fragment_scan_qr_code"> + <action + android:id="@+id/action_vaccinationQrCodeScanFragment_to_vaccinationDetailsFragment" + app:destination="@id/vaccinationDetailsFragment" + app:popUpTo="@id/vaccinationQrCodeScanFragment" + app:popUpToInclusive="true" /> + </fragment> <fragment android:id="@+id/vaccinationListFragment" @@ -28,4 +40,4 @@ android:name="vaccinationCertificateId" app:argType="string" /> </fragment> -</navigation> \ No newline at end of file +</navigation> diff --git a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml index f40c3a6354017fdc23dc5a49a98b16df70254b22..54a86d4e31bfee48a76a4a53be35ab30e9d65206 100644 --- a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml @@ -70,4 +70,15 @@ <string name="vaccination_card_status_vaccination_name">SARS-CoV-2 Impfschutz</string> <!-- XTXT: Homescreen vaccination status card vaccination status label --> <string name="vaccination_card_status_vaccination_incomplete">Unvollständiger Impfschutz</string> -</resources> \ No newline at end of file + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_invalid">Dieser QR-Code ist kein gültiges Impfzertifikat.</string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_not_yet_supported">Dieses Impfzertifikat wird in Ihrer App-Version noch nicht unterstützt. Bitte aktualisieren Sie Ihre App oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_scan_again">Das Impfzertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_already_registered">Das Impfzertifikat ist bereits in Ihrer App registriert.</string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_different_person">Die persönlichen Daten dieses Impfzertifikats stimmen nicht mit denen der bereits registrierten Zertifikate überein. Sie können in der App nur Zertifikate einer Person registrieren.</string> + +</resources> diff --git a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml index 5bc2d27d18e5875c29761268899bf0e59917d19f..ca5e6519bde032ed0c29a0bd443f0668a6f3a82c 100644 --- a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml +++ b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml @@ -70,4 +70,15 @@ <string name="vaccination_card_status_vaccination_name">SARS-CoV-2 Impfschutz</string> <!-- XTXT: Homescreen card vaccination status label --> <string name="vaccination_card_status_vaccination_incomplete">Unvollständiger Impfschutz</string> -</resources> \ No newline at end of file + + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_invalid"></string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_not_yet_supported"></string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_scan_again"></string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_already_registered"></string> + <!-- XTXT: Vaccination QR code scan error message--> + <string name="error_vc_different_person"></string> +</resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt index 1ebc70a0d42ab513589d5828a09942991b112b62..9f5ac095f2ea6562847c9fc8d7b241183cc6fcd8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt @@ -1,13 +1,16 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import com.google.gson.Gson import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED -import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS import de.rki.coronawarnapp.vaccination.core.qrcode.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import de.rki.coronawarnapp.vaccination.decoder.Base45Decoder import de.rki.coronawarnapp.vaccination.decoder.ZLIBDecompressor import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.joda.time.LocalDate import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -16,7 +19,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() { private val base45Decoder = Base45Decoder() private val ZLIBDecompressor = ZLIBDecompressor() private val healthCertificateCOSEDecoder = HealthCertificateCOSEDecoder() - private val vaccinationCertificateV1Decoder = VaccinationCertificateV1Parser() + private val vaccinationCertificateV1Decoder = VaccinationCertificateV1Parser(Gson()) private val extractor = VaccinationQRCodeExtractor( base45Decoder, @@ -36,10 +39,47 @@ class VaccinationQRCodeExtractorTest : BaseTest() { } @Test - fun `valid encoding but not a health certificate fails with HC_CBOR_DECODING_FAILED`() { + fun `happy path extraction with data`() { + val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3) + + with(qrCode.parsedData.header) { + issuer shouldBe "AT" + issuedAt shouldBe Instant.ofEpochSecond(1620392021) + expiresAt shouldBe Instant.ofEpochSecond(1620564821) + } + + with(qrCode.parsedData.vaccinationCertificate) { + with(nameData) { + familyName shouldBe "Musterfrau-Gößinger" + familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER" + givenName shouldBe "Gabriele" + givenNameStandardized shouldBe "GABRIELE" + } + dob shouldBe "1998-02-26" + dateOfBirth shouldBe LocalDate.parse("1998-02-26") + version shouldBe "1.0.0" + + with(vaccinationDatas[0]) { + uniqueCertificateIdentifier shouldBe "urn:uvci:01:AT:10807843F94AEE0EE5093FBC254BD813P" + countryOfVaccination shouldBe "AT" + doseNumber shouldBe 1 + dt shouldBe "2021-02-18" + certificateIssuer shouldBe "BMSGPK Austria" + marketAuthorizationHolderId shouldBe "ORG-100030215" + medicalProductId shouldBe "EU/1/20/1528" + totalSeriesOfDoses shouldBe 2 + targetId shouldBe "840539006" + vaccineId shouldBe "1119305005" + vaccinatedAt shouldBe LocalDate.parse("2021-02-18") + } + } + } + + @Test + fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() { shouldThrow<InvalidHealthCertificateException> { extractor.extract(VaccinationQrCodeTestData.validEncoded) - }.errorCode shouldBe HC_CBOR_DECODING_FAILED + }.errorCode shouldBe VC_HC_CWT_NO_ISS } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java index 2bd0706ce46fc8e82fb55ff016797971bf83fc1c..7db2205367ff15e5183ffaedb939a161273f4b55 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQrCodeTestData.java @@ -5,4 +5,5 @@ public class VaccinationQrCodeTestData { static public String validVaccinationQrCode2 = "HC1:NCFOXN%TS3DHZN4HAF*PQFKKGTNA.Q/R8WRU2FC6L9N*CH PC.IU:N AJPJPC%OQHIZC4.OI1RM8ZA.A53XHMKN4NN3F85QNCY0O%0VZ001HOC9JU0D0HT0HO1PM:K$$09B9LW4T*8+DC%H0PZBITH$*SBAKYE9*FJTJAHD4UDADPSDJIM4KF/B0C2SFIH:9$GCQOS62PR6WPHN6D7LLK*2HG%89UV-0LZ 2ZJJ4FF86O:HO73SM1IO-O.Z80GHS-O:S9UZ4+FJE 4Y3LL/II 07LPMIH-O9XZQSH9R$FXQGDVBK*RZP3:*DG1W7SGT$7S%RMSG2UQYI9*FGCPAXRQ3E2N+E .1:L7O:7X/5Q+MSA7G6MBYO+JQLHP71RJW63X7VUONC6V35HW6SZ6FT5D75W9AV88E34+V4YC5/HQWOQ6$S4N4N31SHPO3Q0E447H9VAK:6.5G$N3ZF7W2SBJT7QG+8UJII3MACIBG2U76MGX3$YB.S7PIJRVOBTN6DTEUIOS7ZKJJEL%.B PT2LO36KT8SP50M/O$4"; static public String validEncoded = "6BFB 9B8OYK3DR3D92BSQAQAHSOMEQ3%1GEVQT4H4O8G3.13G$H6+DH.157SWEV21SD7F2OPY1O-9LRFG0NGCUEPS5LLKJ:1CEJTLA2SADI887A/P3UHL20FTA9ZTRPSVUXO19LEZBQF3VJE$77D5FFC91ZFKCPP%90VS09P2QDQBCMY7-AE0/RW1R:ICP76XRS5UGC82WDNRJ9R7SX331MI9C7WNE5ZL1795NTA/P-35.N65O65ZQ8SU2:KY:C9K9PKD6+K%DI$YQ-9A:CKZ+5HPQNIF7N3K UEU6GEKHCO03MC%QN+LN+C5TTB1B94EC$38QC5O5DP262N:X7JYR/XH/A8%-1KZFTODRY3I 859G-IS9TMY4JM21TAV$N2NK3%BW8K7GI6%O8DUKUT036EF$8:32RBK*0IHJISK5SLTT21KYE7 U/316$I08A/XBU4IZYAGD3UVOJQI2YH3JMXHS1IPE%FOJN$HOV%B3FWCDCP65/%RKP2W2M4A9X7GETNASOXZ0Q/Q5LUNMJ QH+-2:4FW$33+4 +AY7GV-15/717GXY4H4O.:RM/USWV70PV8NGL5XP15NQ3K217GC:1WQEJNBK1RU6J.4K9/J%VQOHA+EW I0YMQ 0"; static public String certificateMissing = "HC1:NCFNA0%00FFWTWGVLKJ99K83X4C8DTTMMX*4P8B3XK2F3$8JVJG2F3$%IQJG/IC6TAY50.FK6ZK6:ETPCBEC8ZKW.CNWE.Y92OAGY82+8UB8-R7/0A1OA1C9K09UIAW.CE$E7%E7WE KEVKER EB39W4N*6K3/D5$CMPCG/DA8DBB85IAAY8WY8I3DA8D0EC*KE: CZ CO/EZKEZ96446C56GVC*JC1A6NA73W5KF6TF627BSKL*8F.MLCM6$-I99MG$8THRJSCJVM/*V:0EY1QU 77*D9KR$SKIP5S-I2-RA1CC06+CHPYQX96*SUF3WZ36NM3XPK1P8.MAFZ6SHB"; + static public String validVaccinationQrCode3 = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH%TAIOOW%I-1W0658WA/UAN9AAT4V22F/8X*G3M9JUPY0BX/KR96R/S09T./0LWTKD33236J3TA3M*4VV2 73-E3ND3DAJ-43%*48YIB73A*G3W19UEBY5:PI0EGSP4*2D$43B+2SEB7:I/2DY73CIBC:G 7376BXBJBAJ UNFMJCRN0H3PQN*E33H3OA70M3FMJIJN523S+0B/S7-SN2H N37J3JFTULJ5CB3ZCIATULV:SNS8F-67N%21Q21$48X2+36D-I/2DBAJDAJCNB-43SZ4RZ4E%5B/9OK53:UCT16DEZIE IE9.M CVCT1+9V*QERU1MK93P5 U02Y9.G9/G9F:QQ28R3U6/V.*NT*QM.SY$N-P1S29 34S0BYBRC.UYS1U%O6QKN*Q5-QFRMLNKNM8JI0EUGP$I/XK$M8-L9KDI:ZH2E4EVS6O0FVAQNJT:EZ6Q%D0*T1.XSDYV0.VI2OKSNODA.BOD:C.OTXS02:M5OGJIF4LHJW7FFJ2NLGFL/EE%CJF+KM%V$AUS:H+NARLK IBMMG"; }