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