diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt index 2914a06a81f61e586bf6a1a2ef1b8e0a1c5bf88a..b7c535d6e3aff991e1e08a40d8a5560e19adb1ff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInEvent.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins import de.rki.coronawarnapp.presencetracing.checkins.CheckIn import de.rki.coronawarnapp.presencetracing.checkins.qrcode.VerifiedTraceLocation -import de.rki.coronawarnapp.util.ui.LazyString sealed class CheckInEvent { @@ -12,8 +11,6 @@ sealed class CheckInEvent { data class ConfirmCheckIn(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent() - data class InvalidQrCode(val errorText: LazyString) : CheckInEvent() - data class ConfirmCheckInWithoutHistory(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent() data class EditCheckIn(val checkInId: Long, val position: Int) : CheckInEvent() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt index cb181fa24ae6d1ed5d384b4009ae8b64a601149f..435473778403981e83be238abe1825ed20e72138 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsFragment.kt @@ -32,7 +32,6 @@ import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator import de.rki.coronawarnapp.util.lists.diffutil.update import de.rki.coronawarnapp.util.onScroll import de.rki.coronawarnapp.util.tryHumanReadableError -import de.rki.coronawarnapp.util.ui.LazyString import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -98,8 +97,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag ) } - is CheckInEvent.InvalidQrCode -> showInvalidQrCodeInformation(event.errorText) - is CheckInEvent.ConfirmCheckInWithoutHistory -> doNavigate( CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragmentCleanHistory( verifiedTraceLocation = event.verifiedTraceLocation @@ -134,15 +131,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } } - private fun showInvalidQrCodeInformation(lazyErrorText: LazyString) { - MaterialAlertDialogBuilder(requireContext()).apply { - val errorText = lazyErrorText.get(context) - setTitle(R.string.trace_location_attendee_invalid_qr_code_dialog_title) - setMessage(getString(R.string.trace_location_attendee_invalid_qr_code_dialog_message, errorText)) - setPositiveButton(R.string.trace_location_attendee_invalid_qr_code_dialog_positive_button) { _, _ -> } - }.show() - } - private fun updateViews(items: List<CheckInsItem>) { checkInsAdapter.update(items) binding.apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt index 9f89c87ea3221601957124d4f6b740dd8565a084..49ff5d5c58b886f892112fdbc136c6aa3a3cb786 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/CheckInsViewModel.kt @@ -20,8 +20,6 @@ import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.intervalFlow import de.rki.coronawarnapp.util.ui.SingleLiveEvent -import de.rki.coronawarnapp.util.ui.toLazyString -import de.rki.coronawarnapp.util.ui.toResolvingString import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineScope @@ -85,7 +83,7 @@ class CheckInsViewModel @AssistedInject constructor( } fun onRemoveAllCheckIns() { - Timber.d("onRemovaAllCheckIns()") + Timber.d("onRemoveAllCheckIns()") events.postValue(CheckInEvent.ConfirmRemoveAll) } @@ -142,23 +140,15 @@ class CheckInsViewModel @AssistedInject constructor( } private fun verifyUri(uri: String) = launch { - try { - Timber.i("uri: $uri") - val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) - when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { - is TraceLocationVerifier.VerificationResult.Valid -> events.postValue( - if (cleanHistory) - CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation) - else - CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation) - ) - is TraceLocationVerifier.VerificationResult.Invalid -> - events.postValue(CheckInEvent.InvalidQrCode(verifyResult.errorTextRes.toResolvingString())) - } - } catch (e: Exception) { - Timber.d(e, "TraceLocation verification failed") - val msg = e.message ?: "QR-Code was invalid" - events.postValue(CheckInEvent.InvalidQrCode(msg.toLazyString())) + Timber.i("uri: $uri") + val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) + when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { + is TraceLocationVerifier.VerificationResult.Valid -> events.postValue( + if (cleanHistory) + CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation) + else + CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation) + ) } } 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 47ddf2f674d5fc0ba24113a654e00d04e7677198..d1729d68ba80b14e6a4e88f43cabee7be92906cb 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 @@ -8,6 +8,7 @@ import android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT import androidx.fragment.app.Fragment import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialContainerTransform import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory @@ -17,6 +18,7 @@ import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragmen 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.LazyString import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.popBackStack import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -68,6 +70,7 @@ class ScanCheckInQrCodeFragment : .build() ) } + is ScanCheckInQrCodeNavigation.InvalidQrCode -> showInvalidQrCodeInformation(navEvent.errorText) } } } @@ -126,6 +129,20 @@ class ScanCheckInQrCodeFragment : DialogHelper.showDialog(permissionDeniedDialog) } + private fun showInvalidQrCodeInformation(lazyErrorText: LazyString) { + MaterialAlertDialogBuilder(requireContext()).apply { + val errorText = lazyErrorText.get(context) + setTitle(R.string.trace_location_attendee_invalid_qr_code_dialog_title) + setMessage(getString(R.string.trace_location_attendee_invalid_qr_code_dialog_message, errorText)) + setPositiveButton(R.string.trace_location_attendee_invalid_qr_code_dialog_positive_button) { _, _ -> + startDecode() + } + setNegativeButton(R.string.trace_location_attendee_invalid_qr_code_dialog_negative_button) { _, _ -> + popBackStack() + } + }.show() + } + private fun showCameraPermissionRationaleDialog() { val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance( requireActivity(), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeNavigation.kt index 688adc324d9c5747fea6216fad480c5f1ba6eb77..89b0b0571f8fa499817d09f235d1649477d6274b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeNavigation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeNavigation.kt @@ -1,6 +1,9 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.scan +import de.rki.coronawarnapp.util.ui.LazyString + sealed class ScanCheckInQrCodeNavigation { object BackNavigation : ScanCheckInQrCodeNavigation() + data class InvalidQrCode(val errorText: LazyString) : ScanCheckInQrCodeNavigation() data class ScanResultNavigation(val uri: String) : ScanCheckInQrCodeNavigation() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModel.kt index c9227d4a526c385bb47403fa1e8a01c8b934c5b9..119e05465c7e07f5dc6deb40404804c7be2c8b17 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModel.kt @@ -3,25 +3,45 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.scan import com.journeyapps.barcodescanner.BarcodeResult import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QRCodeUriParser +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.TraceLocationVerifier import de.rki.coronawarnapp.util.permission.CameraSettings import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.ui.toLazyString +import de.rki.coronawarnapp.util.ui.toResolvingString import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import timber.log.Timber class ScanCheckInQrCodeViewModel @AssistedInject constructor( - private val cameraSettings: CameraSettings + private val qrCodeUriParser: QRCodeUriParser, + private val cameraSettings: CameraSettings, + private val traceLocationVerifier: TraceLocationVerifier ) : CWAViewModel() { val events = SingleLiveEvent<ScanCheckInQrCodeNavigation>() fun onNavigateUp() { - events.value = ScanCheckInQrCodeNavigation.BackNavigation + events.postValue(ScanCheckInQrCodeNavigation.BackNavigation) } - fun onScanResult(barcodeResult: BarcodeResult) { - events.value = ScanCheckInQrCodeNavigation.ScanResultNavigation( - barcodeResult.result.text - ) + fun onScanResult(barcodeResult: BarcodeResult) = launch { + try { + Timber.i("uri: $barcodeResult.result.text") + val qrCodePayload = qrCodeUriParser.getQrCodePayload(barcodeResult.result.text) + when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { + is TraceLocationVerifier.VerificationResult.Invalid -> + events.postValue( + ScanCheckInQrCodeNavigation.InvalidQrCode( + verifyResult.errorTextRes.toResolvingString() + ) + ) + else -> events.postValue(ScanCheckInQrCodeNavigation.ScanResultNavigation(barcodeResult.result.text)) + } + } catch (e: Exception) { + Timber.d(e, "TraceLocation verification failed") + val msg = e.message ?: "QR-Code was invalid" + events.postValue(ScanCheckInQrCodeNavigation.InvalidQrCode(msg.toLazyString())) + } } fun setCameraDeniedPermanently(denied: Boolean) { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModelTest.kt index 991ff951b66a1fbd02790442638020e2d403135c..065b85b7f36cc75781f53b78208ff919ec348c62 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/scan/ScanCheckInQrCodeViewModelTest.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.ui.presencetracing.attendee.scan -import com.google.zxing.Result import com.journeyapps.barcodescanner.BarcodeResult +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.QRCodeUriParser +import de.rki.coronawarnapp.presencetracing.checkins.qrcode.TraceLocationVerifier +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.ui.presencetracing.attendee.scan.ScanCheckInQrCodeNavigation.ScanResultNavigation import de.rki.coronawarnapp.util.permission.CameraSettings import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations +import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -21,13 +26,17 @@ import testhelpers.preferences.mockFlowPreference class ScanCheckInQrCodeViewModelTest : BaseTest() { private lateinit var viewModel: ScanCheckInQrCodeViewModel + @MockK lateinit var qrCodeUriParser: QRCodeUriParser @MockK lateinit var cameraSettings: CameraSettings + @MockK lateinit var traceLocationVerifier: TraceLocationVerifier @BeforeEach fun setup() { MockKAnnotations.init(this) viewModel = ScanCheckInQrCodeViewModel( - cameraSettings + qrCodeUriParser, + cameraSettings, + traceLocationVerifier ) } @@ -38,15 +47,21 @@ class ScanCheckInQrCodeViewModelTest : BaseTest() { } @Test - fun `onScanResult results in navigation url`() { - val mockedResult = mockk<BarcodeResult>().apply { - every { result } returns mockk<Result>().apply { - every { text } returns "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + fun `onScanResult results in navigation url`() = runBlockingTest { + val codeContent = "https://coronawarn.app/E1/SOME_PATH_GOES_HERE" + val expectedOutcome: ScanCheckInQrCodeNavigation = ScanResultNavigation(codeContent) + val validationPassed = mockk<TraceLocationVerifier.VerificationResult.Valid>() + val mockedResult = mockk<BarcodeResult> { + every { result } returns mockk { + every { text } returns codeContent } } + val qrCodePayload = mockk<TraceLocationOuterClass.QRCodePayload>() + coEvery { qrCodeUriParser.getQrCodePayload(any()) } returns qrCodePayload + every { traceLocationVerifier.verifyTraceLocation(qrCodePayload) } returns validationPassed + viewModel.onScanResult(mockedResult) - viewModel.events.getOrAwaitValue() shouldBe - ScanCheckInQrCodeNavigation.ScanResultNavigation("https://coronawarn.app/E1/SOME_PATH_GOES_HERE") + viewModel.events.getOrAwaitValue() shouldBe expectedOutcome } @Test