From 944958fd6c3e4f55d3f91405b4a549219ca710c7 Mon Sep 17 00:00:00 2001 From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com> Date: Wed, 21 Apr 2021 17:45:42 +0200 Subject: [PATCH] RAT registration via link (EXPOSUREAPP-6313) (#2886) * add intent filter * deep linking * refactor qr code registration flow * merge 2.1 * klint * state adaptation * fix tests * add new test results * disable button * add state * adapt how registration states are resolved * review comments Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> --- .../SubmissionConsentFragmentTest.kt | 9 +- Corona-Warn-App/src/main/AndroidManifest.xml | 8 +- .../qrcode/CoronaTestQRCodeValidator.kt | 16 ++- .../coronatest/qrcode/PcrQrCodeExtractor.kt | 2 +- .../qrcode/RapidAntigenQrCodeExtractor.kt | 2 +- .../coronatest/type/CoronaTest.kt | 2 + .../coronatest/type/pcr/PCRCoronaTest.kt | 1 + .../type/rapidantigen/RACoronaTest.kt | 2 + .../rki/coronawarnapp/ui/main/MainActivity.kt | 13 +- .../attendee/checkins/CheckInsFragment.kt | 4 +- .../scan/ScanCheckInQrCodeFragment.kt | 2 +- .../organizer/list/TraceLocationsFragment.kt | 2 +- .../coronawarnapp/ui/submission/ScanStatus.kt | 5 - .../QrCodeRegistrationStateProcessor.kt | 74 ++++++++++ .../consent/SubmissionConsentFragment.kt | 128 ++++++++++++++++++ .../consent/SubmissionConsentViewModel.kt | 70 ++++++++-- .../scan/SubmissionQRCodeScanFragment.kt | 61 ++++----- .../scan/SubmissionQRCodeScanViewModel.kt | 101 +++----------- .../layout/fragment_submission_consent.xml | 11 ++ .../src/main/res/navigation/nav_graph.xml | 50 ++++--- .../consent/SubmissionConsentViewModelTest.kt | 19 ++- .../scan/SubmissionQRCodeScanViewModelTest.kt | 45 +++--- 22 files changed, 431 insertions(+), 196 deletions(-) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/ScanStatus.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt index 27bc93242..49ee918c4 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionConsentFragmentTest.kt @@ -3,10 +3,11 @@ package de.rki.coronawarnapp.ui.submission import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector -import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentViewModel import io.mockk.MockKAnnotations @@ -31,7 +32,8 @@ class SubmissionConsentFragmentTest : BaseUITest() { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var tekHistoryProvider: TEKHistoryProvider - @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator @Rule @JvmField @@ -51,6 +53,9 @@ class SubmissionConsentFragmentTest : BaseUITest() { interoperabilityRepository, TestDispatcherProvider(), tekHistoryProvider, + qrCodeRegistrationStateProcessor, + submissionRepository, + qrCodeValidator ) setupMockViewModel( object : SubmissionConsentViewModel.Factory { diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index 4ff4927e2..f115cd32b 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -79,14 +79,20 @@ <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> - <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="e.coronawarn.app" android:scheme="https" /> + + <data + android:host="s.coronawarn.app" + android:pathPattern=".*" + android:scheme="https" /> + </intent-filter> + </activity> <activity android:name=".ui.main.MainActivity" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt index a3a54f940..55ddd315a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt @@ -6,22 +6,24 @@ import javax.inject.Inject @Reusable class CoronaTestQrCodeValidator @Inject constructor( - ratExtractor: RapidAntigenQrCodeExtractor, + raExtractor: RapidAntigenQrCodeExtractor, pcrExtractor: PcrQrCodeExtractor ) { - - private val extractors = setOf(ratExtractor, pcrExtractor) + private val extractors = setOf(raExtractor, pcrExtractor) fun validate(rawString: String): CoronaTestQRCode { - return extractors - .find { it.canHandle(rawString) } + return findExtractor(rawString) ?.extract(rawString) ?.also { Timber.i("Extracted data from QR code is $it") } ?: throw InvalidQRCodeException() } + + private fun findExtractor(rawString: String): QrCodeExtractor<CoronaTestQRCode>? { + return extractors.find { it.canHandle(rawString) } + } } -interface QrCodeExtractor { +interface QrCodeExtractor<T> { fun canHandle(rawString: String): Boolean - fun extract(rawString: String): CoronaTestQRCode + fun extract(rawString: String): T } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt index 924bd4e89..53d57593a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.coronatest.qrcode import java.util.regex.Pattern import javax.inject.Inject -class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor { +class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor<CoronaTestQRCode> { override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix, ignoreCase = true) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt index deffb83ef..dfe2f8611 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt @@ -12,7 +12,7 @@ import java.util.regex.Matcher import java.util.regex.Pattern import javax.inject.Inject -class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor { +class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<CoronaTestQRCode> { private val prefix: String = "https://s.coronawarn.app?v=1#" private val prefix2: String = "https://s.coronawarn.app/?v=1#" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt index 4fac9bbf2..2765647ac 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt @@ -20,6 +20,8 @@ interface CoronaTest { val isPositive: Boolean + val isPending: Boolean + val testResultReceivedAt: Instant? val testResult: CoronaTestResult diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt index 77957e449..710f2aa34 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt @@ -45,6 +45,7 @@ data class PCRCoronaTest( override val type: CoronaTest.Type = CoronaTest.Type.PCR override val isPositive: Boolean = testResult == CoronaTestResult.PCR_POSITIVE + override val isPending: Boolean = testResult == CoronaTestResult.PCR_OR_RAT_PENDING override val isSubmissionAllowed: Boolean = isPositive && !isSubmitted diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt index 75216e437..77fdd42ba 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt @@ -67,6 +67,8 @@ data class RACoronaTest( } override val isPositive: Boolean = testResult == CoronaTestResult.RAT_POSITIVE + override val isPending: Boolean = + testResult == CoronaTestResult.PCR_OR_RAT_PENDING || testResult == CoronaTestResult.RAT_PENDING override val isSubmissionAllowed: Boolean = isPositive && !isSubmitted diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index 7d6fd9921..9a6eda1d2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -16,6 +16,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import de.rki.coronawarnapp.NavGraphDirections import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler import de.rki.coronawarnapp.contactdiary.ui.overview.ContactDiaryOverviewFragmentDirections @@ -24,6 +25,7 @@ import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsS import de.rki.coronawarnapp.ui.base.startActivitySafely import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragment import de.rki.coronawarnapp.ui.setupWithNavController2 +import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.ConnectivityHelper @@ -178,9 +180,14 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { } private fun navigateByIntentUri(intent: Intent?) { - val uri = intent?.data ?: return - Timber.i("Uri:$uri") - navController.navigate(CheckInsFragment.createCheckInUri(uri.toString())) + val uriString = intent?.data?.toString() ?: return + Timber.i("Uri:$uriString") + when { + CheckInsFragment.canHandle(uriString) -> + navController.navigate(CheckInsFragment.createDeepLink(uriString)) + SubmissionConsentFragment.canHandle(uriString) -> + navController.navigate(NavGraphDirections.actionSubmissionConsentFragment(uriString)) + } } /** 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 d99a60911..42a5a8a55 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 @@ -249,7 +249,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } companion object { - fun createCheckInUri(rootUri: String, cleanHistory: Boolean = false): Uri { + fun createDeepLink(rootUri: String, cleanHistory: Boolean = false): Uri { val encodedUrl = try { URLEncoder.encode(rootUri, Charsets.UTF_8.name()) } catch (e: Exception) { @@ -258,5 +258,7 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } return "coronawarnapp://check-ins/$encodedUrl/?cleanHistory=$cleanHistory".toUri() } + + fun canHandle(rootUri: String): Boolean = rootUri.startsWith("https://e.coronawarn.app") } } 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 184924415..767df359c 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 @@ -62,7 +62,7 @@ class ScanCheckInQrCodeFragment : is ScanCheckInQrCodeNavigation.ScanResultNavigation -> { Timber.i(navEvent.uri) findNavController().navigate( - CheckInsFragment.createCheckInUri(navEvent.uri), + CheckInsFragment.createDeepLink(navEvent.uri), NavOptions.Builder() .setPopUpTo(R.id.checkInsFragment, true) .build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/list/TraceLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/list/TraceLocationsFragment.kt index 98f9779fa..360f678ba 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/list/TraceLocationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/list/TraceLocationsFragment.kt @@ -116,7 +116,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ is TraceLocationEvent.SelfCheckIn -> { findNavController().navigate( - CheckInsFragment.createCheckInUri(it.traceLocation.locationUrl, true), + CheckInsFragment.createDeepLink(it.traceLocation.locationUrl, true), NavOptions.Builder() .setPopUpTo(R.id.checkInsFragment, true) .build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/ScanStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/ScanStatus.kt deleted file mode 100644 index b3bd73f8a..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/ScanStatus.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.rki.coronawarnapp.ui.submission - -enum class ScanStatus { - STARTED, INVALID, SUCCESS -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt new file mode 100644 index 000000000..0328165d7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt @@ -0,0 +1,74 @@ +package de.rki.coronawarnapp.ui.submission.qrcode + +import androidx.lifecycle.MutableLiveData +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.TransactionException +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import timber.log.Timber +import javax.inject.Inject + +class QrCodeRegistrationStateProcessor @Inject constructor( + private val submissionRepository: SubmissionRepository +) { + + data class RegistrationState( + val apiRequestState: ApiRequestState, + val test: CoronaTest? = null + ) + + val showRedeemedTokenWarning = SingleLiveEvent<Unit>() + val registrationState = MutableLiveData(RegistrationState(ApiRequestState.IDLE)) + val registrationError = SingleLiveEvent<CwaWebException>() + + suspend fun startQrCodeRegistration(coronaTestQRCode: CoronaTestQRCode, isConsentGiven: Boolean) = + try { + registrationState.postValue(RegistrationState(ApiRequestState.STARTED)) + val coronaTest = submissionRepository.registerTest(coronaTestQRCode) + if (isConsentGiven) { + submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) + } + checkTestResult(coronaTestQRCode, coronaTest) + registrationState.postValue( + RegistrationState( + ApiRequestState.SUCCESS, + coronaTest + ) + ) + } catch (err: CwaWebException) { + registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + registrationError.postValue(err) + } catch (err: TransactionException) { + if (err.cause is CwaWebException) { + registrationError.postValue(err.cause) + } else { + err.report(ExceptionCategory.INTERNAL) + } + registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + } catch (err: InvalidQRCodeException) { + registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + Timber.d("deregisterTestFromDevice()") + submissionRepository.removeTestFromDevice(type = coronaTestQRCode.type) + showRedeemedTokenWarning.postValue(Unit) + } catch (err: Exception) { + registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + err.report(ExceptionCategory.INTERNAL) + } + + private fun checkTestResult(request: CoronaTestQRCode, test: CoronaTest) { + if (test.testResult == CoronaTestResult.PCR_REDEEMED) { + throw InvalidQRCodeException("CoronaTestResult already redeemed ${request.registrationIdentifier}") + } + } + + enum class ValidationState { + STARTED, INVALID, SUCCESS + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt index 89a9a4a17..b25f5d676 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt @@ -5,10 +5,21 @@ import android.content.Intent import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.NavGraphDirections import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.databinding.FragmentSubmissionConsentBinding +import de.rki.coronawarnapp.exception.http.BadRequestException +import de.rki.coronawarnapp.exception.http.CwaClientError +import de.rki.coronawarnapp.exception.http.CwaServerError +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor.ValidationState import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -23,10 +34,14 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory private val viewModel: SubmissionConsentViewModel by cwaViewModels { viewModelFactory } private val binding: FragmentSubmissionConsentBinding by viewBindingLazy() + private val navArgs by navArgs<SubmissionConsentFragmentArgs>() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.viewModel = viewModel + navArgs.qrCode?.let { + viewModel.qrCode = it + } binding.submissionConsentHeader.headerButtonBack.buttonIcon.setOnClickListener { viewModel.onBackButtonClick() } @@ -46,11 +61,71 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) requireActivity(), REQUEST_USER_RESOLUTION ) + is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode -> { + doNavigate( + NavGraphDirections + .actionToSubmissionDeletionWarningFragment( + it.consentGiven, + it.coronaTestQRCode + ) + ) + } } } viewModel.countries.observe2(this) { binding.countries = it } + + viewModel.showRedeemedTokenWarning.observe2(this) { + val dialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_tan_redeemed_title, + R.string.submission_error_dialog_web_tan_redeemed_body, + R.string.submission_error_dialog_web_tan_redeemed_button_positive + ) + + DialogHelper.showDialog(dialog) + popBackStack() + } + + viewModel.qrCodeValidationState.observe2(this) { + if (ValidationState.INVALID == it) { + showInvalidQrCodeDialog() + } + } + + viewModel.registrationState.observe2(this) { state -> + binding.progressSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED + binding.submissionConsentButton.isEnabled = when (state.apiRequestState) { + ApiRequestState.STARTED -> false + else -> true + } + + if (ApiRequestState.SUCCESS == state.apiRequestState) { + when (state.test?.type) { + CoronaTest.Type.PCR -> throw UnsupportedOperationException() + CoronaTest.Type.RAPID_ANTIGEN -> { + when { + state.test.isPositive -> + doNavigate( + NavGraphDirections.actionToSubmissionTestResultAvailableFragment( + CoronaTest.Type.RAPID_ANTIGEN + ) + ) + else -> doNavigate( + NavGraphDirections.actionSubmissionTestResultPendingFragment( + testType = CoronaTest.Type.RAPID_ANTIGEN + ) + ) + } + } + } + } + } + + viewModel.registrationError.observe2(this) { + DialogHelper.showDialog(buildErrorDialog(it)) + } } override fun onResume() { @@ -65,7 +140,60 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) } } + private fun showInvalidQrCodeDialog() { + val invalidScanDialogInstance = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_qr_code_scan_invalid_dialog_headline, + R.string.submission_qr_code_scan_invalid_dialog_body, + R.string.submission_qr_code_scan_invalid_dialog_button_positive, + R.string.submission_qr_code_scan_invalid_dialog_button_negative, + true, + positiveButtonFunction = {}, + negativeButtonFunction = ::navigateHome + ) + + DialogHelper.showDialog(invalidScanDialogInstance) + } + + private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance { + return when (exception) { + is BadRequestException -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_qr_code_scan_invalid_dialog_headline, + R.string.submission_qr_code_scan_invalid_dialog_body, + R.string.submission_qr_code_scan_invalid_dialog_button_positive, + R.string.submission_qr_code_scan_invalid_dialog_button_negative, + true, + { }, + ::navigateHome + ) + is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_network_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateHome + ) + else -> DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_generic_error_title, + R.string.submission_error_dialog_web_generic_error_body, + R.string.submission_error_dialog_web_generic_error_button_positive, + null, + true, + ::navigateHome + ) + } + } + + private fun navigateHome() { + popBackStack() + } + companion object { private const val REQUEST_USER_RESOLUTION = 3000 + fun canHandle(rootUri: String): Boolean = rootUri.startsWith("https://s.coronawarn.app") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt index e3387e7dc..69b7c6e66 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt @@ -4,35 +4,48 @@ import androidx.lifecycle.asLiveData import com.google.android.gms.common.api.ApiException import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.first import timber.log.Timber class SubmissionConsentViewModel @AssistedInject constructor( interoperabilityRepository: InteroperabilityRepository, dispatcherProvider: DispatcherProvider, private val tekHistoryProvider: TEKHistoryProvider, - // private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor, + private val submissionRepository: SubmissionRepository, + private val qrCodeValidator: CoronaTestQrCodeValidator ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() + val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>() + + val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning + val registrationState = qrCodeRegistrationStateProcessor.registrationState + val registrationError = qrCodeRegistrationStateProcessor.registrationError val countries = interoperabilityRepository.countryList .asLiveData(context = dispatcherProvider.Default) + var qrCode: String? = null + fun onConsentButtonClick() { - // TODO Do we have a Test registered at this time? We need to forward the decission with navargs? - // analyticsKeySubmissionCollector.reportAdvancedConsentGiven() launch { try { val preAuthorized = tekHistoryProvider.preAuthorizeExposureKeyHistory() - // Routes to QR code screen either user has already granted permission or it is older Api - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRCodeScan) + // Proceed anyway, either user has already granted permission or it is older Api + proceed() Timber.i("Pre-authorized:$preAuthorized") } catch (exception: Exception) { if (exception is ApiException && @@ -42,12 +55,51 @@ class SubmissionConsentViewModel @AssistedInject constructor( routeToScreen.postValue(SubmissionNavigationEvents.ResolvePlayServicesException(exception)) } else { Timber.d(exception, "Pre-auth failed with unrecoverable exception") - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRCodeScan) + proceed() } } } } + private fun proceed() { + qrCode.let { + if (it == null) + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRCodeScan) + else + processQrCode(it) + } + } + + private fun processQrCode(qrCodeString: String) { + launch { + validateAndRegister(qrCodeString) + } + } + + private suspend fun validateAndRegister(qrCodeString: String) { + try { + val coronaTestQRCode = qrCodeValidator.validate(qrCodeString) + // TODO this needs to be adapted to work for different types + QRCodeCensor.lastGUID = coronaTestQRCode.registrationIdentifier + qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.SUCCESS) + val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first() + + if (coronaTest != null) { + routeToScreen.postValue( + SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode( + coronaTestQRCode, + consentGiven = true + ) + ) + } else { + qrCodeRegistrationStateProcessor.startQrCodeRegistration(coronaTestQRCode, true) + } + } catch (err: InvalidQRCodeException) { + Timber.i(err) + qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID) + } + } + fun onBackButtonClick() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToDispatcher) } @@ -58,8 +110,8 @@ class SubmissionConsentViewModel @AssistedInject constructor( fun giveGoogleConsentResult(accepted: Boolean) { Timber.i("User allowed Google consent:$accepted") - // Navigate to QR code scan anyway regardless of consent result - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRCodeScan) + // Navigate regardless of consent result + proceed() } @AssistedFactory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt index f14ed9c02..6b0571bb8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory +import de.rki.coronawarnapp.NavGraphDirections import de.rki.coronawarnapp.R import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.CoronaTest @@ -17,15 +18,15 @@ import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.exception.http.CwaClientError import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.exception.http.CwaWebException -import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.ui.submission.ScanStatus +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents 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.observe2 +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.cwaViewModelsAssisted @@ -74,22 +75,25 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co when (it) { is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode -> { doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionDeletionWarningFragment( + NavGraphDirections + .actionToSubmissionDeletionWarningFragment( it.consentGiven, it.coronaTestQRCode ) ) } + is SubmissionNavigationEvents.NavigateToDispatcher -> + navigateToDispatchScreen() + is SubmissionNavigationEvents.NavigateToConsent -> + goBack() } } - viewModel.scanStatusValue.observe2(this) { - if (ScanStatus.INVALID == it) { + viewModel.qrCodeValidationState.observe2(this) { + if (QrCodeRegistrationStateProcessor.ValidationState.INVALID == it) { showInvalidScanDialog() } } - viewModel.showRedeemedTokenWarning.observe2(this) { val dialog = DialogHelper.DialogInstance( requireActivity(), @@ -108,26 +112,24 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co else -> View.GONE } if (ApiRequestState.SUCCESS == state.apiRequestState) { - when (state.testResult) { + when (state.test?.testResult) { CoronaTestResult.PCR_POSITIVE -> doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultAvailableFragment( - testType = CoronaTest.Type.PCR - ) + NavGraphDirections + .actionToSubmissionTestResultAvailableFragment(testType = CoronaTest.Type.PCR) ) CoronaTestResult.PCR_OR_RAT_PENDING -> { - if (state.testType == CoronaTest.Type.RAPID_ANTIGEN) { + if (state.test.type == CoronaTest.Type.RAPID_ANTIGEN) { doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment( + NavGraphDirections + .actionSubmissionTestResultPendingFragment( testType = CoronaTest.Type.RAPID_ANTIGEN ) ) } else { doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment( + NavGraphDirections + .actionSubmissionTestResultPendingFragment( testType = CoronaTest.Type.PCR ) ) @@ -137,15 +139,15 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co CoronaTestResult.PCR_INVALID, CoronaTestResult.PCR_REDEEMED -> doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment( + NavGraphDirections + .actionSubmissionTestResultPendingFragment( testType = CoronaTest.Type.PCR ) ) CoronaTestResult.RAT_POSITIVE -> doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultAvailableFragment( + NavGraphDirections + .actionToSubmissionTestResultAvailableFragment( testType = CoronaTest.Type.RAPID_ANTIGEN ) ) @@ -154,8 +156,8 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co CoronaTestResult.RAT_PENDING, CoronaTestResult.RAT_REDEEMED -> doNavigate( - SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment( + NavGraphDirections + .actionSubmissionTestResultPendingFragment( testType = CoronaTest.Type.RAPID_ANTIGEN ) ) @@ -166,20 +168,11 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co viewModel.registrationError.observe2(this) { DialogHelper.showDialog(buildErrorDialog(it)) } - - viewModel.routeToScreen.observe2(this) { - when (it) { - is SubmissionNavigationEvents.NavigateToDispatcher -> - navigateToDispatchScreen() - is SubmissionNavigationEvents.NavigateToConsent -> - goBack() - } - } } private fun startDecode() { binding.submissionQrCodeScanPreview.decodeSingle { - viewModel.validateTestGUID(it.text) + viewModel.onQrCodeAvailable(it.text) } } @@ -313,7 +306,7 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co DialogHelper.showDialog(cameraPermissionRationaleDialogInstance) } - private fun goBack() = (activity as MainActivity).goBack() + private fun goBack() = popBackStack() private fun requestCameraPermission() = requestPermissions( arrayOf(Manifest.permission.CAMERA), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt index eae290615..d07af3d2d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt @@ -1,23 +1,13 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.MutableLiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException -import de.rki.coronawarnapp.coronatest.server.CoronaTestResult -import de.rki.coronawarnapp.coronatest.type.CoronaTest -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.http.CwaWebException -import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.ui.submission.ScanStatus +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.permission.CameraSettings @@ -29,26 +19,31 @@ import timber.log.Timber class SubmissionQRCodeScanViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, - private val submissionRepository: SubmissionRepository, private val cameraSettings: CameraSettings, + private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor, @Assisted private val isConsentGiven: Boolean, + private val submissionRepository: SubmissionRepository, private val qrCodeValidator: CoronaTestQrCodeValidator ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() - val showRedeemedTokenWarning = SingleLiveEvent<Unit>() - val scanStatusValue = SingleLiveEvent<ScanStatus>() + val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning + val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>() + val registrationState = qrCodeRegistrationStateProcessor.registrationState + val registrationError = qrCodeRegistrationStateProcessor.registrationError - fun validateTestGUID(rawResult: String) = launch { - Timber.d("validateTestGUID(rawResult=$rawResult)") + fun onQrCodeAvailable(rawResult: String) { + launch { + startQrCodeRegistration(rawResult, isConsentGiven) + } + } + + suspend fun startQrCodeRegistration(rawResult: String, isConsentGiven: Boolean) { try { val coronaTestQRCode = qrCodeValidator.validate(rawResult) - Timber.d("validateTestGUID() coronaTestQRCode=%s", coronaTestQRCode) // TODO this needs to be adapted to work for different types QRCodeCensor.lastGUID = coronaTestQRCode.registrationIdentifier - scanStatusValue.postValue(ScanStatus.SUCCESS) - + qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.SUCCESS) val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first() - Timber.d("validateTestGUID() existingTest=%s", coronaTest) if (coronaTest != null) { routeToScreen.postValue( @@ -58,72 +53,10 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( ) ) } else { - doDeviceRegistration(coronaTestQRCode) + qrCodeRegistrationStateProcessor.startQrCodeRegistration(coronaTestQRCode, isConsentGiven) } } catch (err: InvalidQRCodeException) { - Timber.e(err, "Failed to validate GUID") - scanStatusValue.postValue(ScanStatus.INVALID) - } - } - - val registrationState = MutableLiveData(RegistrationState(ApiRequestState.IDLE)) - val registrationError = SingleLiveEvent<CwaWebException>() - - data class RegistrationState( - val apiRequestState: ApiRequestState, - val testResult: CoronaTestResult? = null, - val testType: CoronaTest.Type? = null - ) - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal suspend fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) { - try { - Timber.d("doDeviceRegistration(coronaTestQRCode=%s)", coronaTestQRCode) - registrationState.postValue(RegistrationState(ApiRequestState.STARTED)) - val coronaTest = submissionRepository.registerTest(coronaTestQRCode) - Timber.d("doDeviceRegistration() coronaTest=$coronaTest") - if (isConsentGiven) { - submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) - } - checkTestResult(coronaTestQRCode, coronaTest) - registrationState.postValue( - RegistrationState( - ApiRequestState.SUCCESS, - coronaTest.testResult, - coronaTestQRCode.type - ) - ) - } catch (err: CwaWebException) { - registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) - registrationError.postValue(err) - } catch (err: TransactionException) { - if (err.cause is CwaWebException) { - registrationError.postValue(err.cause) - } else { - err.report(ExceptionCategory.INTERNAL) - } - registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) - } catch (err: InvalidQRCodeException) { - registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) - deregisterTestFromDevice(coronaTestQRCode) - showRedeemedTokenWarning.postValue(Unit) - } catch (err: Exception) { - registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) - err.report(ExceptionCategory.INTERNAL) - } - } - - private fun checkTestResult(request: CoronaTestQRCode, test: CoronaTest) { - if (test.testResult == CoronaTestResult.PCR_REDEEMED) { - throw InvalidQRCodeException("CoronaTestResult already redeemed ${request.registrationIdentifier}") - } - } - - private fun deregisterTestFromDevice(coronaTest: CoronaTestQRCode) { - launch { - Timber.d("deregisterTestFromDevice(coronaTest=%s)", coronaTest) - submissionRepository.removeTestFromDevice(type = coronaTest.type) - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) + qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID) } } diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml index e176c094a..c1cce928f 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml @@ -155,6 +155,17 @@ <include layout="@layout/merge_guidelines_side" /> + <ProgressBar + android:id="@+id/progress_spinner" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> </layout> 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 b1a3f0bb1..bed4a20ba 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -240,7 +240,6 @@ app:destination="@id/surveyConsentFragment" /> </fragment> - <fragment android:id="@+id/statisticsExplanationFragment" android:name="de.rki.coronawarnapp.statistics.ui.StatisticsExplanationFragment" @@ -374,25 +373,29 @@ android:id="@+id/action_submissionQRCodeScanFragment_to_submissionDispatcherFragment" app:popUpTo="@id/submissionQRCodeScanFragment" app:popUpToInclusive="true" /> - <action - android:id="@+id/action_submissionQRCodeScanFragment_to_submissionTestResultPendingFragment" - app:destination="@id/submissionTestResultPendingFragment" - app:popUpTo="@id/mainFragment" - app:popUpToInclusive="false"> - <argument - android:name="skipInitialTestResultRefresh" - android:defaultValue="true" - app:argType="boolean" /> - </action> - <action - android:id="@+id/action_submissionQRCodeScanFragment_to_submissionTestResultAvailableFragment" - app:destination="@id/submissionTestResultAvailableFragment" - app:popUpTo="@id/mainFragment" - app:popUpToInclusive="false" /> - <action - android:id="@+id/action_submissionQRCodeScanFragment_to_submissionDeletionWarningFragment" - app:destination="@id/submissionDeletionWarningFragment" /> </fragment> + + <action + android:id="@+id/action_submissionTestResultPendingFragment" + app:destination="@id/submissionTestResultPendingFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> + + <action + android:id="@+id/action_to_submissionTestResultAvailableFragment" + app:destination="@id/submissionTestResultAvailableFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> + + <action + android:id="@+id/action_to_submissionDeletionWarningFragment" + app:destination="@id/submissionDeletionWarningFragment" /> + <fragment android:id="@+id/submissionResultReadyFragment" android:name="de.rki.coronawarnapp.ui.submission.resultready.SubmissionResultReadyFragment" @@ -458,8 +461,17 @@ <action android:id="@+id/action_submissionConsentFragment_to_informationPrivacyFragment" app:destination="@id/informationPrivacyFragment" /> + <argument + android:name="qrCode" + android:defaultValue="@null" + app:argType="string" + app:nullable="true" /> </fragment> + <action + android:id="@+id/action_submissionConsentFragment" + app:destination="@id/submissionConsentFragment" /> + <fragment android:id="@+id/submissionTestResultConsentGivenFragment" android:name="de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenFragment" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt index 73c696f80..c3f66c812 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt @@ -1,12 +1,16 @@ package de.rki.coronawarnapp.ui.submission.qrcode.consent +import androidx.lifecycle.MutableLiveData import com.google.android.gms.common.api.ApiException -import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.nearby.modules.tekhistory.TEKHistoryProvider import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.ui.SingleLiveEvent import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -30,7 +34,8 @@ class SubmissionConsentViewModelTest { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var tekHistoryProvider: TEKHistoryProvider - @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator lateinit var viewModel: SubmissionConsentViewModel @@ -41,11 +46,18 @@ class SubmissionConsentViewModelTest { MockKAnnotations.init(this) every { interoperabilityRepository.countryList } returns MutableStateFlow(countryList) every { submissionRepository.giveConsentToSubmission(any()) } just Runs - every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven() } just Runs + coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent() + coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData( + QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE) + ) + coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent() viewModel = SubmissionConsentViewModel( interoperabilityRepository, dispatcherProvider = TestDispatcherProvider(), tekHistoryProvider, + qrCodeRegistrationStateProcessor, + submissionRepository, + qrCodeValidator ) } @@ -76,7 +88,6 @@ class SubmissionConsentViewModelTest { @Test fun `giveGoogleConsentResult when user Allows routes to QR Code scan`() { - every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven() } just Runs viewModel.giveGoogleConsentResult(true) viewModel.routeToScreen.value shouldBe SubmissionNavigationEvents.NavigateToQRCodeScan } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt index 78b2f8517..0f81834e2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt @@ -1,22 +1,24 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan +import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.ScanStatus +import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor +import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor.ValidationState import de.rki.coronawarnapp.util.permission.CameraSettings +import de.rki.coronawarnapp.util.ui.SingleLiveEvent 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.flow.MutableStateFlow -import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -31,6 +33,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var cameraSettings: CameraSettings @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator + @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor private val coronaTestFlow = MutableStateFlow<CoronaTest?>( null @@ -41,14 +44,20 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { MockKAnnotations.init(this) every { submissionRepository.testForType(any()) } returns coronaTestFlow + coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent() + coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData( + QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE) + ) + coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent() } private fun createViewModel() = SubmissionQRCodeScanViewModel( TestDispatcherProvider(), - submissionRepository, cameraSettings, + qrCodeRegistrationStateProcessor, isConsentGiven = true, - qrCodeValidator, + submissionRepository, + qrCodeValidator ) @Test @@ -64,34 +73,24 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { qrCodeValidator.validate(validQrCode) } returns coronaTestQRCode every { qrCodeValidator.validate(invalidQrCode) } throws InvalidQRCodeException() + val viewModel = createViewModel() // start - viewModel.scanStatusValue.value = ScanStatus.STARTED + viewModel.qrCodeValidationState.value = ValidationState.STARTED - viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED + viewModel.qrCodeValidationState.value shouldBe ValidationState.STARTED QRCodeCensor.lastGUID = null - viewModel.validateTestGUID(validQrCode) - viewModel.scanStatusValue.observeForever {} - viewModel.scanStatusValue.value shouldBe ScanStatus.SUCCESS + viewModel.onQrCodeAvailable(validQrCode) + viewModel.qrCodeValidationState.observeForever {} + viewModel.qrCodeValidationState.value shouldBe ValidationState.SUCCESS QRCodeCensor.lastGUID = guid // invalid guid - viewModel.validateTestGUID(invalidQrCode) - viewModel.scanStatusValue.value shouldBe ScanStatus.INVALID - } - - @Test - fun `doDeviceRegistration calls TestResultDataCollector`() = runBlockingTest { - val viewModel = createViewModel() - val mockResult = mockk<CoronaTestQRCode>().apply { - every { registrationIdentifier } returns "guid" - } - val mockTest = mockk<CoronaTest>() - coEvery { submissionRepository.registerTest(any()) } returns mockTest - viewModel.doDeviceRegistration(mockResult) + viewModel.onQrCodeAvailable(invalidQrCode) + viewModel.qrCodeValidationState.value shouldBe ValidationState.INVALID } @Test -- GitLab