From 1853cac14639ff3da91c2ac0df02e3a9acf5b52a Mon Sep 17 00:00:00 2001
From: Kolya Opahle <k.opahle@sap.com>
Date: Fri, 5 Jun 2020 17:13:47 +0200
Subject: [PATCH] Removing the registration fragment (#184)

* Added the TestAlreadyPairedException to the web request builder

* Added specific exceptions to all submission web requests

* Moved setting activity of DialogHelper to show call

* Re added the requestIDS to all listeners for later use

* Added Exception live data to view model

* Added error dialogs to RegisterDeviceFragment screen

* Added null check to networkResponse in VolleyError

* Added error dialogs to SubmissionTestResultFragment screen

* Fixed wrong view model call

* Added error dialogs to SubmissionResultPositiveOtherWarningFragment

* Added missing imports

* Revert "Added specific exceptions to all submission web requests"

This reverts commit 88d9027e24dd8b8c0baae7478c15cb23af403a0f.

* Revert "Added the TestAlreadyPairedException to the web request builder"

This reverts commit a4fb8272d89dd1e508a50002827fbc0dc7d86919.

* added specific error handing to services

* Switched error handlers to retrofit

* Extracted status code numbers to constants

* Added final strings for http error handling

* Removed RegisterDeviceFragment from app and added a Event helper

* Lint fixes

* removed error report for networking exceptions

* Added event wrapper to scan status to allow for rescanning of failed qrs

* linting

* Switched most view model LiveData to Event

Co-authored-by: marcmuschko <marc.muschko@sap.com>
---
 .../SubmissionQRCodeScanFragment.kt           |  63 +++++++++--
 .../SubmissionRegisterDeviceFragment.kt       | 100 ------------------
 ...ssionResultPositiveOtherWarningFragment.kt |  10 +-
 .../ui/submission/SubmissionTanFragment.kt    |  63 ++++++++++-
 .../SubmissionTestResultFragment.kt           |   8 +-
 .../ui/viewmodel/SubmissionViewModel.kt       |  57 ++++++----
 .../java/de/rki/coronawarnapp/util/Event.kt   |  28 +++++
 .../fragment_submission_register_device.xml   |  78 --------------
 .../src/main/res/navigation/nav_graph.xml     |  25 ++---
 9 files changed, 191 insertions(+), 241 deletions(-)
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt
 delete mode 100644 Corona-Warn-App/src/main/res/layout/fragment_submission_register_device.xml

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt
index 928c49fc3..bcee7773c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionQRCodeScanFragment.kt
@@ -5,18 +5,20 @@ import android.os.Bundle
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.Observer
+import androidx.fragment.app.activityViewModels
 import com.google.zxing.BarcodeFormat
 import com.journeyapps.barcodescanner.BarcodeResult
 import com.journeyapps.barcodescanner.DefaultDecoderFactory
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeScanBinding
+import de.rki.coronawarnapp.exception.TestAlreadyPairedException
 import de.rki.coronawarnapp.ui.BaseFragment
 import de.rki.coronawarnapp.ui.main.MainActivity
 import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
 import de.rki.coronawarnapp.util.CameraPermissionHelper
 import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.observeEvent
+import retrofit2.HttpException
 
 /**
  * A simple [BaseFragment] subclass.
@@ -28,7 +30,7 @@ class SubmissionQRCodeScanFragment : BaseFragment() {
         private val TAG: String? = SubmissionQRCodeScanFragment::class.simpleName
     }
 
-    private val viewModel: SubmissionViewModel by viewModels()
+    private val viewModel: SubmissionViewModel by activityViewModels()
     private var _binding: FragmentSubmissionQrCodeScanBinding? = null
     private val binding: FragmentSubmissionQrCodeScanBinding get() = _binding!!
 
@@ -55,6 +57,41 @@ class SubmissionQRCodeScanFragment : BaseFragment() {
         _binding = null
     }
 
+    private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance {
+        return when (exception) {
+            is TestAlreadyPairedException -> DialogHelper.DialogInstance(
+                requireActivity(),
+                R.string.submission_error_dialog_web_test_paired_title,
+                R.string.submission_error_dialog_web_test_paired_body,
+                R.string.submission_error_dialog_web_test_paired_button_positive,
+                null,
+                true,
+                ::navigateToDispatchScreen
+            )
+            is HttpException -> DialogHelper.DialogInstance(
+                requireActivity(),
+                R.string.submission_error_dialog_web_generic_error_title,
+                getString(
+                    R.string.submission_error_dialog_web_generic_network_error_body,
+                    exception.code()
+                ),
+                R.string.submission_error_dialog_web_generic_error_button_positive,
+                null,
+                true,
+                ::navigateToDispatchScreen
+            )
+            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,
+                ::navigateToDispatchScreen
+            )
+        }
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
@@ -72,7 +109,7 @@ class SubmissionQRCodeScanFragment : BaseFragment() {
             DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE))
         binding.submissionQrCodeScanViewfinderView.setCameraPreview(binding.submissionQrCodeScanPreview)
 
-        viewModel.scanStatus.observe(viewLifecycleOwner, Observer {
+        viewModel.scanStatus.observeEvent(viewLifecycleOwner, {
             if (ScanStatus.SUCCESS == it) {
                 showSuccessfulScanDialog()
             }
@@ -81,6 +118,19 @@ class SubmissionQRCodeScanFragment : BaseFragment() {
                 showInvalidScanDialog()
             }
         })
+
+        viewModel.registrationState.observeEvent(viewLifecycleOwner, {
+            if (ApiRequestState.SUCCESS == it) {
+                doNavigate(
+                    SubmissionQRCodeScanFragmentDirections
+                        .actionSubmissionQRCodeScanFragmentToSubmissionResultFragment()
+                )
+            }
+        })
+
+        viewModel.registrationError.observeEvent(viewLifecycleOwner, {
+            DialogHelper.showDialog(buildErrorDialog(it))
+        })
     }
 
     private fun navigateToDispatchScreen() =
@@ -98,10 +148,7 @@ class SubmissionQRCodeScanFragment : BaseFragment() {
             R.string.submission_qr_code_scan_successful_dialog_button_negative,
             true,
             {
-                doNavigate(
-                    SubmissionQRCodeScanFragmentDirections
-                        .actionSubmissionQRCodeScanFragmentToSubmissionRegisterDeviceFragment()
-                )
+                viewModel.doDeviceRegistration()
             },
             {
                 viewModel.deleteTestGUID()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt
deleted file mode 100644
index 6a49eba8c..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionRegisterDeviceFragment.kt
+++ /dev/null
@@ -1,100 +0,0 @@
-package de.rki.coronawarnapp.ui.submission
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Observer
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.databinding.FragmentSubmissionRegisterDeviceBinding
-import de.rki.coronawarnapp.exception.TestAlreadyPairedException
-import de.rki.coronawarnapp.ui.BaseFragment
-import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
-import de.rki.coronawarnapp.util.DialogHelper
-import retrofit2.HttpException
-
-class SubmissionRegisterDeviceFragment : BaseFragment() {
-    private val viewModel: SubmissionViewModel by activityViewModels()
-    private var _binding: FragmentSubmissionRegisterDeviceBinding? = null
-    private val binding: FragmentSubmissionRegisterDeviceBinding get() = _binding!!
-
-    override fun onCreateView(
-        inflater: LayoutInflater,
-        container: ViewGroup?,
-        savedInstanceState: Bundle?
-    ): View? {
-        _binding = FragmentSubmissionRegisterDeviceBinding.inflate(inflater)
-        binding.lifecycleOwner = this
-        return binding.root
-    }
-
-    override fun onDestroyView() {
-        super.onDestroyView()
-        _binding = null
-    }
-
-    private fun navigateToDispatchScreen() = doNavigate(
-        SubmissionRegisterDeviceFragmentDirections
-            .actionSubmissionRegisterDeviceFragmentToSubmissionDispatcherFragment()
-    )
-
-    private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance {
-        return when (exception) {
-            is TestAlreadyPairedException -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_test_paired_title,
-                R.string.submission_error_dialog_web_test_paired_body,
-                R.string.submission_error_dialog_web_test_paired_button_positive,
-                null,
-                true,
-                ::navigateToDispatchScreen
-            )
-            is HttpException -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                getString(
-                    R.string.submission_error_dialog_web_generic_network_error_body,
-                    exception.code()
-                ),
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateToDispatchScreen
-            )
-            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,
-                ::navigateToDispatchScreen
-            )
-        }
-    }
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-
-        viewModel.registrationState.observe(viewLifecycleOwner, Observer {
-            if (ApiRequestState.SUCCESS == it) {
-                doNavigate(
-                    SubmissionRegisterDeviceFragmentDirections
-                        .actionSubmissionRegisterDeviceFragmentToSubmissionResultFragment()
-                )
-            }
-        })
-
-        viewModel.registrationError.observe(viewLifecycleOwner, Observer {
-            if (it != null) {
-                DialogHelper.showDialog(buildErrorDialog(it))
-            }
-        })
-    }
-
-    override fun onResume() {
-        super.onResume()
-        viewModel.doDeviceRegistration()
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt
index 2322c093d..477722597 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionResultPositiveOtherWarningFragment.kt
@@ -5,7 +5,6 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Observer
 import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionPositiveOtherWarningBinding
@@ -16,6 +15,7 @@ import de.rki.coronawarnapp.ui.BaseFragment
 import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
 import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel
 import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.observeEvent
 import retrofit2.HttpException
 
 class SubmissionResultPositiveOtherWarningFragment : BaseFragment(),
@@ -117,13 +117,11 @@ class SubmissionResultPositiveOtherWarningFragment : BaseFragment(),
         super.onViewCreated(view, savedInstanceState)
         setButtonOnClickListener()
 
-        submissionViewModel.submissionError.observe(viewLifecycleOwner, Observer {
-            if (it != null) {
-                DialogHelper.showDialog(buildErrorDialog(it))
-            }
+        submissionViewModel.submissionError.observeEvent(viewLifecycleOwner, {
+            DialogHelper.showDialog(buildErrorDialog(it))
         })
 
-        submissionViewModel.submissionState.observe(viewLifecycleOwner, Observer {
+        submissionViewModel.submissionState.observeEvent(viewLifecycleOwner, {
             if (it == ApiRequestState.SUCCESS) {
                 doNavigate(
                     SubmissionResultPositiveOtherWarningFragmentDirections
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt
index c01259274..56b4c4608 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTanFragment.kt
@@ -5,8 +5,14 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.fragment.app.activityViewModels
+import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionTanBinding
+import de.rki.coronawarnapp.exception.TestAlreadyPairedException
 import de.rki.coronawarnapp.ui.BaseFragment
+import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
+import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.observeEvent
+import retrofit2.HttpException
 
 /**
  * Fragment for TAN entry
@@ -14,6 +20,7 @@ import de.rki.coronawarnapp.ui.BaseFragment
 class SubmissionTanFragment : BaseFragment() {
 
     private val viewModel: SubmissionTanViewModel by activityViewModels()
+    private val submissionViewModel: SubmissionViewModel by activityViewModels()
     private var _binding: FragmentSubmissionTanBinding? = null
     private val binding: FragmentSubmissionTanBinding get() = _binding!!
 
@@ -34,18 +41,64 @@ class SubmissionTanFragment : BaseFragment() {
         _binding = null
     }
 
+    private fun buildErrorDialog(exception: Exception): DialogHelper.DialogInstance {
+        return when (exception) {
+            is TestAlreadyPairedException -> DialogHelper.DialogInstance(
+                requireActivity(),
+                R.string.submission_error_dialog_web_test_paired_title,
+                R.string.submission_error_dialog_web_test_paired_body,
+                R.string.submission_error_dialog_web_test_paired_button_positive,
+                null,
+                true,
+                ::navigateToDispatchScreen
+            )
+            is HttpException -> DialogHelper.DialogInstance(
+                requireActivity(),
+                R.string.submission_error_dialog_web_generic_error_title,
+                getString(
+                    R.string.submission_error_dialog_web_generic_network_error_body,
+                    exception.code()
+                ),
+                R.string.submission_error_dialog_web_generic_error_button_positive,
+                null,
+                true,
+                ::navigateToDispatchScreen
+            )
+            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,
+                ::navigateToDispatchScreen
+            )
+        }
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
         binding.submissionTanInput.listener = { tan -> viewModel.tan.value = tan }
         binding.submissionTanButtonEnter.setOnClickListener { storeTanAndContinue() }
-        binding.submissionTanHeader.headerButtonBack.buttonIcon.setOnClickListener { close() }
-    }
+        binding.submissionTanHeader.headerButtonBack.buttonIcon.setOnClickListener { navigateToDispatchScreen() }
 
-    private fun close() {
-        doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToMainFragment())
+        submissionViewModel.registrationState.observeEvent(viewLifecycleOwner, {
+            if (ApiRequestState.SUCCESS == it) {
+                doNavigate(
+                    SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionResultFragment()
+                )
+            }
+        })
+
+        submissionViewModel.registrationError.observeEvent(viewLifecycleOwner, {
+            DialogHelper.showDialog(buildErrorDialog(it))
+        })
     }
 
+    private fun navigateToDispatchScreen() =
+        doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionDispatcherFragment())
+
     private fun storeTanAndContinue() {
         // verify input format
         if (viewModel.isValidTanFormat.value != true)
@@ -54,6 +107,6 @@ class SubmissionTanFragment : BaseFragment() {
         // store locally
         viewModel.storeTeletan()
 
-        doNavigate(SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionRegisterDeviceFragment())
+        submissionViewModel.doDeviceRegistration()
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt
index 737e8f6e3..917c796df 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt
@@ -5,13 +5,13 @@ import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.fragment.app.activityViewModels
-import androidx.lifecycle.Observer
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultBinding
 import de.rki.coronawarnapp.ui.BaseFragment
 import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
 import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel
 import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.observeEvent
 import retrofit2.HttpException
 
 /**
@@ -79,10 +79,8 @@ class SubmissionTestResultFragment : BaseFragment() {
         super.onViewCreated(view, savedInstanceState)
         setButtonOnClickListener()
 
-        submissionViewModel.uiStateError.observe(viewLifecycleOwner, Observer {
-            if (it != null) {
-                DialogHelper.showDialog(buildErrorDialog(it))
-            }
+        submissionViewModel.uiStateError.observeEvent(viewLifecycleOwner, {
+            DialogHelper.showDialog(buildErrorDialog(it))
         })
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
index bac24dc8d..39431e54f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
@@ -4,39 +4,38 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.report
 import de.rki.coronawarnapp.service.submission.SubmissionService
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.ui.submission.ScanStatus
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.Event
 import kotlinx.coroutines.launch
 import java.util.Date
 
 class SubmissionViewModel : ViewModel() {
-    private val _scanStatus = MutableLiveData(ScanStatus.STARTED)
+    private val _scanStatus = MutableLiveData(Event(ScanStatus.STARTED))
 
-    private val _registrationState = MutableLiveData(ApiRequestState.IDLE)
-    private val _registrationError = MutableLiveData<Exception?>(null)
+    private val _registrationState = MutableLiveData(Event(ApiRequestState.IDLE))
+    private val _registrationError = MutableLiveData<Event<Exception>>(null)
 
     private val _uiStateState = MutableLiveData(ApiRequestState.IDLE)
-    private val _uiStateError = MutableLiveData<Exception?>(null)
+    private val _uiStateError = MutableLiveData<Event<Exception>>(null)
 
-    private val _submissionState = MutableLiveData(ApiRequestState.IDLE)
-    private val _submissionError = MutableLiveData<Exception?>(null)
+    private val _submissionState = MutableLiveData(Event(ApiRequestState.IDLE))
+    private val _submissionError = MutableLiveData<Event<Exception>>(null)
 
-    val scanStatus: LiveData<ScanStatus> = _scanStatus
+    val scanStatus: LiveData<Event<ScanStatus>> = _scanStatus
 
-    val registrationState: LiveData<ApiRequestState> = _registrationState
-    val registrationError: LiveData<Exception?> = _registrationError
+    val registrationState: LiveData<Event<ApiRequestState>> = _registrationState
+    val registrationError: LiveData<Event<Exception>> = _registrationError
 
     val uiStateState: LiveData<ApiRequestState> = _uiStateState
-    val uiStateError: LiveData<Exception?> = _uiStateError
+    val uiStateError: LiveData<Event<Exception>> = _uiStateError
 
-    val submissionState: LiveData<ApiRequestState> = _submissionState
-    val submissionError: LiveData<Exception?> = _submissionError
+    val submissionState: LiveData<Event<ApiRequestState>> = _submissionState
+    val submissionError: LiveData<Event<Exception>> = _submissionError
 
     val deviceRegistered get() = LocalData.registrationToken() != null
 
@@ -46,14 +45,14 @@ class SubmissionViewModel : ViewModel() {
         SubmissionRepository.deviceUIState
 
     fun submitDiagnosisKeys() =
-        executeRequestWithState(
+        executeRequestWithStateForEvent(
             SubmissionService::asyncSubmitExposureKeys,
             _submissionState,
             _submissionError
         )
 
     fun doDeviceRegistration() =
-        executeRequestWithState(
+        executeRequestWithStateForEvent(
             SubmissionService::asyncRegisterDevice,
             _registrationState,
             _registrationError
@@ -70,9 +69,9 @@ class SubmissionViewModel : ViewModel() {
         if (SubmissionService.containsValidGUID(scanResult)) {
             val guid = SubmissionService.extractGUID(scanResult)
             SubmissionService.storeTestGUID(guid)
-            _scanStatus.value = ScanStatus.SUCCESS
+            _scanStatus.value = Event(ScanStatus.SUCCESS)
         } else {
-            _scanStatus.value = ScanStatus.INVALID
+            _scanStatus.value = Event(ScanStatus.INVALID)
         }
     }
 
@@ -90,7 +89,7 @@ class SubmissionViewModel : ViewModel() {
     private fun executeRequestWithState(
         apiRequest: suspend () -> Unit,
         state: MutableLiveData<ApiRequestState>,
-        exceptionLiveData: MutableLiveData<Exception?>? = null
+        exceptionLiveData: MutableLiveData<Event<Exception>>? = null
     ) {
         state.value = ApiRequestState.STARTED
         viewModelScope.launch {
@@ -98,9 +97,25 @@ class SubmissionViewModel : ViewModel() {
                 apiRequest()
                 state.value = ApiRequestState.SUCCESS
             } catch (err: Exception) {
-                exceptionLiveData?.value = err
+                exceptionLiveData?.value = Event(err)
                 state.value = ApiRequestState.FAILED
-                err.report(ExceptionCategory.INTERNAL)
+            }
+        }
+    }
+
+    private fun executeRequestWithStateForEvent(
+        apiRequest: suspend () -> Unit,
+        state: MutableLiveData<Event<ApiRequestState>>,
+        exceptionLiveData: MutableLiveData<Event<Exception>>? = null
+    ) {
+        state.value = Event(ApiRequestState.STARTED)
+        viewModelScope.launch {
+            try {
+                apiRequest()
+                state.value = Event(ApiRequestState.SUCCESS)
+            } catch (err: Exception) {
+                exceptionLiveData?.value = Event(err)
+                state.value = Event(ApiRequestState.FAILED)
             }
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt
new file mode 100644
index 000000000..586d04069
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/Event.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.util
+
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Observer
+
+/**
+ * Helper to supply live data that can only be observed once
+ */
+open class Event<out T>(private val eventContent: T) {
+    private var handled = false
+
+    fun getContent(): T? {
+        return if (handled) {
+            null
+        } else {
+            handled = true
+            eventContent
+        }
+    }
+}
+
+inline fun <T> LiveData<Event<T>>.observeEvent(
+    owner: LifecycleOwner,
+    crossinline onEvent: (T) -> Unit
+) {
+    observe(owner, Observer { it?.getContent()?.let(onEvent) })
+}
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_register_device.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_register_device.xml
deleted file mode 100644
index 359284ebf..000000000
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_register_device.xml
+++ /dev/null
@@ -1,78 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools">
-
-    <ScrollView
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:fillViewport="true"
-        tools:context=".ui.submission.SubmissionRegisterDeviceFragment">
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content">
-
-
-            <TextView
-                android:id="@+id/submission_register_device_headline"
-                style="@style/textTitle"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:text="@string/submission_register_device_headline"
-                app:layout_constraintEnd_toStartOf="@+id/submission_register_device_guideline_end"
-                app:layout_constraintStart_toStartOf="@+id/submission_register_device_guideline_start"
-                app:layout_constraintTop_toTopOf="@+id/submission_register_device_guideline_top" />
-
-            <TextView
-                android:id="@+id/submission_register_device_text"
-                style="@style/textMultiline"
-                android:layout_width="0dp"
-                android:layout_height="wrap_content"
-                android:text="@string/submission_register_device_body"
-                app:layout_constraintEnd_toStartOf="@+id/submission_register_device_guideline_end"
-                app:layout_constraintStart_toStartOf="@+id/submission_register_device_guideline_start"
-                app:layout_constraintTop_toBottomOf="@+id/submission_register_device_headline" />
-
-            <ProgressBar
-                android:id="@+id/submission_register_device_spinner"
-                style="?android:attr/progressBarStyle"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                app:layout_constraintBottom_toTopOf="@+id/submission_register_device_guideline_bottom"
-                app:layout_constraintEnd_toStartOf="@+id/submission_register_device_guideline_end"
-                app:layout_constraintStart_toStartOf="@+id/submission_register_device_guideline_start"
-                app:layout_constraintTop_toBottomOf="@+id/submission_register_device_text" />
-
-            <androidx.constraintlayout.widget.Guideline
-                android:id="@+id/submission_register_device_guideline_start"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="vertical"
-                app:layout_constraintGuide_begin="@dimen/guideline_start" />
-
-            <androidx.constraintlayout.widget.Guideline
-                android:id="@+id/submission_register_device_guideline_bottom"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                app:layout_constraintGuide_end="@dimen/guideline_bottom" />
-
-            <androidx.constraintlayout.widget.Guideline
-                android:id="@+id/submission_register_device_guideline_end"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="vertical"
-                app:layout_constraintGuide_end="@dimen/guideline_end" />
-
-            <androidx.constraintlayout.widget.Guideline
-                android:id="@+id/submission_register_device_guideline_top"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal"
-                app:layout_constraintGuide_begin="@dimen/guideline_top" />
-
-        </androidx.constraintlayout.widget.ConstraintLayout>
-
-    </ScrollView>
-</layout>
\ No newline at end of file
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 c62db62a7..6ae74c712 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -242,13 +242,15 @@
         android:label="fragment_submission_tan"
         tools:layout="@layout/fragment_submission_tan">
         <action
-            android:id="@+id/action_submissionTanFragment_to_mainFragment"
+            android:id="@+id/action_submissionTanFragment_to_submissionDispatcherFragment"
             app:destination="@id/submissionDispatcherFragment"
             app:popUpTo="@id/submissionDispatcherFragment"
             app:popUpToInclusive="true" />
         <action
-            android:id="@+id/action_submissionTanFragment_to_submissionRegisterDeviceFragment"
-            app:destination="@id/submissionRegisterDeviceFragment" />
+            android:id="@+id/action_submissionTanFragment_to_submissionResultFragment"
+            app:destination="@id/submissionResultFragment"
+            app:popUpTo="@id/submissionResultFragment"
+            app:popUpToInclusive="true" />
     </fragment>
 
     <fragment
@@ -282,22 +284,9 @@
             app:popUpTo="@id/submissionDispatcherFragment"
             app:popUpToInclusive="true" />
         <action
-            android:id="@+id/action_submissionQRCodeScanFragment_to_submissionRegisterDeviceFragment"
-            app:destination="@id/submissionRegisterDeviceFragment" />
-    </fragment>
-    <fragment
-        android:id="@+id/submissionRegisterDeviceFragment"
-        android:name="de.rki.coronawarnapp.ui.submission.SubmissionRegisterDeviceFragment"
-        android:label="SubmissionRegisterDeviceFragment">
-        <action
-            android:id="@+id/action_submissionRegisterDeviceFragment_to_submissionResultFragment"
+            android:id="@+id/action_submissionQRCodeScanFragment_to_submissionResultFragment"
             app:destination="@id/submissionResultFragment"
-            app:popUpTo="@id/mainFragment" />
-        <action
-            android:id="@+id/action_submissionRegisterDeviceFragment_to_submissionDispatcherFragment"
-            app:destination="@id/submissionDispatcherFragment"
-            app:popUpTo="@id/submissionDispatcherFragment"
-            app:popUpToInclusive="true" />
+            app:popUpTo="@id/submissionResultFragment" />
     </fragment>
     <fragment
         android:id="@+id/submissionDoneFragment"
-- 
GitLab