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 4b7f88440901b41d38e243161bfd088e54e4ac6e..7ea2e16565672eb475f993f182df41a03fd66880 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 @@ -7,7 +7,7 @@ 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.submission.TestRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragmentArgs import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentViewModel @@ -31,7 +31,7 @@ class SubmissionConsentFragmentTest : BaseUITest() { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var tekHistoryProvider: TEKHistoryProvider - @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator private lateinit var viewModel: SubmissionConsentViewModel @@ -48,7 +48,7 @@ class SubmissionConsentFragmentTest : BaseUITest() { interoperabilityRepository, TestDispatcherProvider(), tekHistoryProvider, - qrCodeRegistrationStateProcessor, + testRegistrationStateProcessor, submissionRepository, qrCodeValidator ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt index 68b8a1b80a3d8ae83fad80d050cced8549a2e6b0..1f1b0dac59fa9341c3a57ea7c9c2362955e1b324 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragmentTest.kt @@ -9,9 +9,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode -import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -36,13 +36,13 @@ class RequestCovidCertificateFragmentTest : BaseUITest() { every { viewModel.birthDate } returns MutableLiveData(null) every { viewModel.registrationState } returns MutableLiveData( - QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE) + TestRegistrationStateProcessor.State.Idle ) setupMockViewModel( object : RequestCovidCertificateViewModel.Factory { override fun create( - coronaTestQrCode: CoronaTestQRCode, + testRegistrationRequest: TestRegistrationRequest, coronaTestConsent: Boolean, deleteOldTest: Boolean ): RequestCovidCertificateViewModel = viewModel diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt index 4eaae484d689e3f167712fb188037a219e6ddaf3..c744a067c8a4c9463485f31d30ddd7b9bacaafcd 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt @@ -65,16 +65,16 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto qrcodeScanViewfinder.setCameraPreview(binding.qrcodeScanPreview) } - viewModel.pcrtState.observe2(this) { - binding.pcrtData.text = it.getNiceTextForHumans() + viewModel.pcrtState.observe2(this) { state -> + binding.pcrtData.text = state.getNiceTextForHumans() } binding.apply { pcrtDeleteAction.setOnClickListener { viewModel.deletePCRT() } pcrtRefreshAction.setOnClickListener { viewModel.refreshPCRT() } } - viewModel.ratState.observe2(this) { - binding.ratData.text = it.getNiceTextForHumans() + viewModel.ratState.observe2(this) { state -> + binding.ratData.text = state.getNiceTextForHumans() } binding.apply { ratDeleteAction.setOnClickListener { viewModel.deleteRAT() } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt index 544158248957931dfca9e3df2390b194c2ff719f..79c8f84ce15a08b94c5843b4461ae055b17d398a 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt @@ -6,12 +6,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.latestPCRT -import de.rki.coronawarnapp.coronatest.latestRAT import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.coronatest.type.CoronaTest -import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest -import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -28,17 +24,17 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val errorEvents = SingleLiveEvent<Throwable>() - val pcrtState = coronaTestRepository.latestPCRT.map { - PCRTState( - coronaTest = it - ) - }.asLiveData(context = dispatcherProvider.Default) + val pcrtState = coronaTestRepository.coronaTests + .map { tests -> tests.filter { it.type == CoronaTest.Type.PCR } } + .map { pcrTests -> + PCRTState(coronaTests = pcrTests) + }.asLiveData(context = dispatcherProvider.Default) - val ratState = coronaTestRepository.latestRAT.map { - RATState( - coronaTest = it - ) - }.asLiveData(context = dispatcherProvider.Default) + val ratState = coronaTestRepository.coronaTests + .map { tests -> tests.filter { it.type == CoronaTest.Type.RAPID_ANTIGEN } } + .map { raTests -> + RATState(coronaTests = raTests) + }.asLiveData(context = dispatcherProvider.Default) val testsInContactDiary = contactDiaryRepository.testResults.map { it.foldIndexed(StringBuilder()) { id, buffer, item -> @@ -58,12 +54,12 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( fun deletePCRT() = launch { try { - val pcrTest = coronaTestRepository.latestPCRT.first() - if (pcrTest == null) { - Timber.d("No PCR test to delete") - return@launch - } - coronaTestRepository.removeTest(pcrTest.identifier) + Timber.i("Deleting PCR tests.") + coronaTestRepository.coronaTests.first() + .filter { it.type == CoronaTest.Type.PCR } + .forEach { test -> + coronaTestRepository.removeTest(test.identifier) + } } catch (e: Exception) { Timber.e(e, "Failed to delete PCR test.") errorEvents.postValue(e) @@ -80,14 +76,14 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( } } - fun deleteRAT() = launch { + fun deleteRAT(): Unit = launch { try { - val raTest = coronaTestRepository.latestRAT.first() - if (raTest == null) { - Timber.d("No RA test to delete") - return@launch - } - coronaTestRepository.removeTest(raTest.identifier) + Timber.i("Deleting RA tests.") + coronaTestRepository.coronaTests.first() + .filter { it.type == CoronaTest.Type.RAPID_ANTIGEN } + .forEach { test -> + coronaTestRepository.removeTest(test.identifier) + } } catch (e: Exception) { Timber.e(e, "Failed to delete RA test.") errorEvents.postValue(e) @@ -105,28 +101,34 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( } data class PCRTState( - val coronaTest: PCRCoronaTest? + val coronaTests: Collection<CoronaTest> ) { fun getNiceTextForHumans(): String { - return coronaTest - ?.toString() - ?.replace("PCRCoronaTest(", "") - ?.replace(",", ",\n") - ?.trimEnd { it == ')' } - ?: "No PCR test registered." + if (coronaTests.isEmpty()) { + return "No PCR test registered." + } + return coronaTests.joinToString("\n") { test -> + test.toString() + .replace("PCRCoronaTest(", "") + .replace(",", ",\n") + .trimEnd { it == ')' } + } } } data class RATState( - val coronaTest: RACoronaTest? + val coronaTests: Collection<CoronaTest> ) { fun getNiceTextForHumans(): String { - return coronaTest - ?.toString() - ?.replace("RACoronaTest(", "") - ?.replace(",", ",\n") - ?.trimEnd { it == ')' } - ?: "No rapid antigen test registered." + if (coronaTests.isEmpty()) { + return "No rapid antigen test registered." + } + return coronaTests.joinToString("\n") { test -> + test.toString() + .replace("RACoronaTest(", "") + .replace(",", ",\n") + .trimEnd { it == ')' } + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt index 9af108afeaf5050149af78eed0d289005709cea8..4919100b5ad5edf116f4e680d7ec58cf1de25039 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.coronatest import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration @@ -10,6 +11,8 @@ import de.rki.coronawarnapp.coronatest.storage.CoronaTestStorage import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor import de.rki.coronawarnapp.coronatest.type.TestIdentifier +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.HotDataFlow @@ -72,22 +75,65 @@ class CoronaTestRepository @Inject constructor( private fun getProcessor(type: CoronaTest.Type) = processors.single { it.type == type } - suspend fun registerTest(request: TestRegistrationRequest): CoronaTest { - Timber.tag(TAG).i("registerTest(request=%s)", request) + /** + * Default preconditions prevent duplicate test registration, + * and registration of an already redeemed test. + * If pre and post-condition are not met an [IllegalStateException] is thrown. + * + * @return the new test that was registered (or an exception is thrown) + */ + suspend fun registerTest( + request: TestRegistrationRequest, + preCondition: ((Collection<CoronaTest>) -> Boolean) = { currentTests -> + if (currentTests.any { it.type == request.type }) { + throw DuplicateCoronaTestException("There is already a test of this type: ${request.type}.") + } + true + }, + postCondition: ((CoronaTest) -> Boolean) = { newTest -> + if (newTest.isRedeemed) { + Timber.w("Replacement test was already redeemed, removing it, will not use.") + throw AlreadyRedeemedException(newTest) + } + true + } + ): CoronaTest { + Timber.tag(TAG).i( + "registerTest(request=%s, preCondition=%s, postCondition=%s)", + request, preCondition, postCondition + ) // We check early, if there is no processor, crash early, "should" never happen though... val processor = getProcessor(request.type) val currentTests = internalData.updateBlocking { - if (values.any { it.type == request.type }) { - throw DuplicateCoronaTestException("There is already a test of this type: ${request.type}.") + if (!preCondition(values)) { + throw IllegalStateException("PreCondition for current tests not fullfilled.") } - val test = processor.create(request).also { + val existing = values.singleOrNull { it.type == request.type } + + val newTest = processor.create(request).also { Timber.tag(TAG).i("New test created: %s", it) } - toMutableMap().apply { this[test.identifier] = test } + if (!postCondition(newTest)) { + throw IllegalStateException("PostCondition for new tests not fullfilled.") + } + + if (existing != null) { + Timber.tag(TAG).w("We already have a test of this type, removing old test: %s", request) + try { + getProcessor(existing.type).onRemove(existing) + } catch (e: Exception) { + e.report(ExceptionCategory.INTERNAL) + } + } + + toMutableMap().apply { + existing?.let { remove(it.identifier) } + this[newTest.identifier] = newTest + } } return currentTests[request.identifier]!! diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt index 99ab06763c99b475ee7f25a631f09bac318dac55..5e0ec73e6dbf2d1dda8d64bc83c15bf05faf8bca 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt @@ -1,9 +1,10 @@ package de.rki.coronawarnapp.coronatest +import android.os.Parcelable import de.rki.coronawarnapp.coronatest.type.CoronaTest import org.joda.time.LocalDate -interface TestRegistrationRequest { +interface TestRegistrationRequest : Parcelable { val type: CoronaTest.Type val identifier: String val isDccSupportedByPoc: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt new file mode 100644 index 0000000000000000000000000000000000000000..40ceeef924d23b21513c4e4ccede47ba3b86cf52 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/errors/AlreadyRedeemedException.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.coronatest.errors + +import de.rki.coronawarnapp.coronatest.type.CoronaTest + +class AlreadyRedeemedException( + coronaTest: CoronaTest +) : IllegalArgumentException("Test was already redeemed ${coronaTest.identifier}") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 9215b9f36c135c5fa7d96017bd02f82c05932c8c..8d75bf3d4be2413a59a57b76081bdb7152996d8c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.submission import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.CoronaTest @@ -20,7 +21,6 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -@Suppress("LongParameterList") @Singleton class SubmissionRepository @Inject constructor( @AppScope private val scope: CoroutineScope, @@ -92,6 +92,32 @@ class SubmissionRepository @Inject constructor( return coronaTest } + /** + * Attempt to register a new test, but if it is already redeemed, keep the previous test. + */ + suspend fun tryReplaceTest(request: TestRegistrationRequest): CoronaTest { + Timber.tag(TAG).v("tryReplaceTest(request=%s)", request) + + val coronaTest = coronaTestRepository.registerTest( + request = request, + preCondition = { currentTests -> + if (currentTests.any { it.type == request.type }) { + Timber.tag(TAG).i("Test type already exists, will try to replace.") + } + true + }, + postCondition = { newTest -> + if (newTest.isRedeemed) { + Timber.w("Replacement test was already redeemed, removing it, will not use.") + throw AlreadyRedeemedException(newTest) + } + true + } + ) + Timber.d("Registered test %s -> %s", request, coronaTest) + return coronaTest + } + suspend fun reset() { Timber.tag(TAG).v("reset()") tekHistoryStorage.clear() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..dd34a6e48cd3a1970ed050989c295124129eba03 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessor.kt @@ -0,0 +1,104 @@ +package de.rki.coronawarnapp.submission + +import android.content.Context +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.exception.ExceptionCategory +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.exception.reporting.report +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +class TestRegistrationStateProcessor @Inject constructor( + private val submissionRepository: SubmissionRepository +) { + + private val mutex = Mutex() + + sealed class State { + object Idle : State() + object Working : State() + data class TestRegistered(val test: CoronaTest) : State() + + data class Error(val exception: Exception) : State() { + fun getDialogBuilder(context: Context): MaterialAlertDialogBuilder { + val builder = MaterialAlertDialogBuilder(context).apply { + setCancelable(true) + } + + return when (exception) { + is AlreadyRedeemedException -> builder.apply { + setTitle(R.string.submission_error_dialog_web_tan_redeemed_title) + setMessage(R.string.submission_error_dialog_web_tan_redeemed_body) + setPositiveButton(android.R.string.ok) { _, _ -> + /* dismiss */ + } + } + is BadRequestException -> builder.apply { + setTitle(R.string.submission_qr_code_scan_invalid_dialog_headline) + setMessage(R.string.submission_qr_code_scan_invalid_dialog_body) + setPositiveButton(android.R.string.ok) { _, _ -> + /* dismiss */ + } + } + is CwaClientError, is CwaServerError -> builder.apply { + setTitle(R.string.submission_error_dialog_web_generic_error_title) + setMessage(R.string.submission_error_dialog_web_generic_network_error_body) + setPositiveButton(android.R.string.ok) { _, _ -> + /* dismiss */ + } + } + is CwaWebException -> builder.apply { + setTitle(R.string.submission_error_dialog_web_generic_error_title) + setMessage(R.string.submission_error_dialog_web_generic_error_body) + setPositiveButton(android.R.string.ok) { _, _ -> + /* dismiss */ + } + } + else -> exception.toErrorDialogBuilder(context) + } + } + } + } + + private val stateInternal = MutableStateFlow<State>(State.Idle) + val state: Flow<State> = stateInternal + + suspend fun startRegistration( + request: TestRegistrationRequest, + isSubmissionConsentGiven: Boolean, + allowReplacement: Boolean, + ): CoronaTest? = mutex.withLock { + return try { + stateInternal.value = State.Working + + val coronaTest = if (allowReplacement) { + submissionRepository.tryReplaceTest(request) + } else { + submissionRepository.registerTest(request) + } + + if (isSubmissionConsentGiven) { + submissionRepository.giveConsentToSubmission(type = coronaTest.type) + } + stateInternal.value = State.TestRegistered(test = coronaTest) + coronaTest + } catch (err: Exception) { + stateInternal.value = State.Error(exception = err) + if (err !is CwaWebException) { + err.report(ExceptionCategory.INTERNAL) + } + null + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt index 2635a01a5d5cdcfec0d8b5f48f392d1616777c46..40f21e02a4f5008abcd635abf7e1b24a3281d478 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateFragment.kt @@ -1,30 +1,22 @@ package de.rki.coronawarnapp.ui.submission.covidcertificate import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.View +import androidx.activity.addCallback import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.dialog.MaterialAlertDialogBuilder import de.rki.coronawarnapp.NavGraphDirections import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode -import de.rki.coronawarnapp.coronatest.server.CoronaTestResult -import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR -import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.databinding.FragmentRequestCovidCertificateBinding 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 -import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State import de.rki.coronawarnapp.util.TimeAndDateExtensions.toDayFormat import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate @@ -33,7 +25,6 @@ import de.rki.coronawarnapp.util.ui.viewBinding import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import org.joda.time.LocalDate -import timber.log.Timber import javax.inject.Inject class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid_certificate), AutoInject { @@ -43,7 +34,7 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid factoryProducer = { viewModelFactory }, constructorCall = { factory, _ -> factory as RequestCovidCertificateViewModel.Factory - factory.create(args.coronaTestQrCode, args.coronaTestConsent, args.deleteOldTest) + factory.create(args.testRegistrationRequest, args.coronaTestConsent, args.deleteOldTest) } ) private val binding by viewBinding<FragmentRequestCovidCertificateBinding>() @@ -51,7 +42,7 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { - val isPCR = args.coronaTestQrCode is CoronaTestQRCode.PCR + val isPCR = args.testRegistrationRequest.type == CoronaTest.Type.PCR birthDateGroup.isVisible = isPCR privacyCard.pcrExtraBullet.isVisible = isPCR @@ -59,7 +50,9 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid if (text.toString().isEmpty()) viewModel.birthDateChanged(null) } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) { showCloseDialog() } toolbar.setNavigationOnClickListener { showCloseDialog() } + agreeButton.setOnClickListener { viewModel.onAgreeGC() } disagreeButton.setOnClickListener { viewModel.onDisagreeGC() } dateInputEdit.setOnClickListener { openDatePicker() } @@ -68,70 +61,60 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid viewModel.events.observe(viewLifecycleOwner) { event -> when (event) { Back -> popBackStack() - ToDispatcherScreen -> doNavigate( + + ToDispatcherScreen -> RequestCovidCertificateFragmentDirections .actionRequestCovidCertificateFragmentToDispatcherFragment() - ) - ToHomeScreen -> doNavigate( + .run { doNavigate(this) } + + ToHomeScreen -> RequestCovidCertificateFragmentDirections.actionRequestCovidCertificateFragmentToHomeFragment() - ) + .run { doNavigate(this) } } } viewModel.birthDate.observe(viewLifecycleOwner) { date -> agreeButton.isEnabled = !isPCR || date != null } - viewModel.registrationError.observe(viewLifecycleOwner) { DialogHelper.showDialog(buildErrorDialog(it)) } viewModel.registrationState.observe(viewLifecycleOwner) { state -> handleRegistrationState(state) } - viewModel.showRedeemedTokenWarning.observe(viewLifecycleOwner) { DialogHelper.showDialog(redeemDialog()) } - viewModel.removalError.observe(viewLifecycleOwner) { it.toErrorDialogBuilder(requireContext()).show() } } - private fun handleRegistrationState(state: QrCodeRegistrationStateProcessor.RegistrationState) { - when (state.apiRequestState) { - ApiRequestState.STARTED -> binding.apply { - progressBar.show() - agreeButton.isInvisible = true - disagreeButton.isInvisible = true + private fun handleRegistrationState(state: State) { + val isWorking = state is State.Working + binding.apply { + if (isWorking) progressBar.show() else progressBar.hide() + agreeButton.isInvisible = isWorking + disagreeButton.isInvisible = isWorking + } + when (state) { + State.Idle, + State.Working -> { + // Handled above } - else -> binding.apply { - progressBar.hide() - agreeButton.isInvisible = false - disagreeButton.isInvisible = false + is State.Error -> { + state.getDialogBuilder(requireContext()).apply { + if (state.exception is BadRequestException) { + setPositiveButton(R.string.submission_qr_code_scan_invalid_dialog_button_positive) { _, _ -> + viewModel.navigateBack() + } + setNegativeButton(R.string.submission_qr_code_scan_invalid_dialog_button_negative) { _, _ -> + viewModel.navigateToDispatcherScreen() + } + setOnCancelListener { viewModel.navigateToDispatcherScreen() } + } else { + setOnDismissListener { viewModel.navigateToDispatcherScreen() } + } + }.show() } - } - - when (state.test?.testResult) { - CoronaTestResult.PCR_POSITIVE -> - NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = PCR) - - CoronaTestResult.PCR_OR_RAT_PENDING -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) - - CoronaTestResult.PCR_NEGATIVE, - CoronaTestResult.PCR_INVALID, - CoronaTestResult.PCR_REDEEMED -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = PCR) - - CoronaTestResult.RAT_POSITIVE -> - NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = RAPID_ANTIGEN) - - CoronaTestResult.RAT_NEGATIVE, - CoronaTestResult.RAT_INVALID, - CoronaTestResult.RAT_PENDING, - CoronaTestResult.RAT_REDEEMED -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = RAPID_ANTIGEN) - null -> { - Timber.w("Successful API request, but test was null?") - return + is State.TestRegistered -> when { + state.test.isPositive -> + NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type) + .run { doNavigate(this) } + + else -> + NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) + .run { doNavigate(this) } } - }.run { doNavigate(this) } + } } - private fun redeemDialog(): DialogHelper.DialogInstance = 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 - ) - private fun showCloseDialog() = MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.request_gc_dialog_title) .setMessage(R.string.request_gc_dialog_message) @@ -151,39 +134,4 @@ class RequestCovidCertificateFragment : Fragment(R.layout.fragment_request_covid } } .show(childFragmentManager, "RequestGreenCertificateFragment.MaterialDatePicker") - - private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance = - when (exception) { - is BadRequestException -> createInvalidScanDialog() - 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, - { viewModel.navigateToDispatcherScreen() } - ) - 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, - { viewModel.navigateToDispatcherScreen() } - ) - } - - private fun createInvalidScanDialog() = 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, - { viewModel.navigateBack() }, - { viewModel.navigateToDispatcherScreen() }, - { viewModel.navigateToDispatcherScreen() } - ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt index ff0e272e71fbba2a1b6de47be1defa317be6f66c..2e99ed97093486051fb3d51dd253caeb715471ec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModel.kt @@ -5,33 +5,24 @@ import androidx.lifecycle.MutableLiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import kotlinx.coroutines.flow.first import org.joda.time.LocalDate -import timber.log.Timber class RequestCovidCertificateViewModel @AssistedInject constructor( - @Assisted private val coronaTestQrCode: CoronaTestQRCode, + @Assisted private val testRegistrationRequest: TestRegistrationRequest, @Assisted("coronaTestConsent") private val coronaTestConsent: Boolean, @Assisted("deleteOldTest") private val deleteOldTest: Boolean, - private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor, + private val registrationStateProcessor: TestRegistrationStateProcessor, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, - private val submissionRepository: SubmissionRepository, - private val coronaTestRepository: CoronaTestRepository, ) : CWAViewModel() { - // Test registration LiveData - val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning - val registrationState = qrCodeRegistrationStateProcessor.registrationState - val registrationError = qrCodeRegistrationStateProcessor.registrationError - val removalError = SingleLiveEvent<Throwable>() + val registrationState = registrationStateProcessor.state.asLiveData2() private val birthDateData = MutableLiveData<LocalDate>(null) val birthDate: LiveData<LocalDate> = birthDateData @@ -58,38 +49,28 @@ class RequestCovidCertificateViewModel @AssistedInject constructor( } private fun registerAndMaybeDelete(dccConsent: Boolean) = launch { - if (deleteOldTest) removeOldTest() - registerWithDccConsent(dccConsent) - } - - private suspend fun registerWithDccConsent(dccConsent: Boolean) { - val consentedQrCode = when (coronaTestQrCode) { - is CoronaTestQRCode.PCR -> coronaTestQrCode.copy( + val consentedQrCode = when (testRegistrationRequest) { + is CoronaTestQRCode.PCR -> testRegistrationRequest.copy( dateOfBirth = birthDateData.value, isDccConsentGiven = dccConsent ) - is CoronaTestQRCode.RapidAntigen -> coronaTestQrCode.copy(isDccConsentGiven = dccConsent) + is CoronaTestQRCode.RapidAntigen -> testRegistrationRequest.copy(isDccConsentGiven = dccConsent) + else -> testRegistrationRequest } - qrCodeRegistrationStateProcessor.startQrCodeRegistration(consentedQrCode, coronaTestConsent) - if (coronaTestConsent) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(consentedQrCode.type) - } + registrationStateProcessor.startRegistration( + request = consentedQrCode, + isSubmissionConsentGiven = coronaTestConsent, + allowReplacement = deleteOldTest + ) - private suspend fun removeOldTest() { - try { - submissionRepository.testForType(coronaTestQrCode.type).first()?.let { - coronaTestRepository.removeTest(it.identifier) - } ?: Timber.e("Test for type ${coronaTestQrCode.type} is not found") - } catch (e: Exception) { - Timber.d(e, "removeOldTest failed") - removalError.postValue(e) - } + if (coronaTestConsent) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(consentedQrCode.type) } @AssistedFactory interface Factory : CWAViewModelFactory<RequestCovidCertificateViewModel> { fun create( - coronaTestQrCode: CoronaTestQRCode, + testRegistrationRequest: TestRegistrationRequest, @Assisted("coronaTestConsent") coronaTestConsent: Boolean, @Assisted("deleteOldTest") deleteOldTest: Boolean ): RequestCovidCertificateViewModel diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt index 623f30a36ff938ee643c382133385a2ebe348b8c..f2b01f53a3c88b9ad1c6fb223465843dc8910274 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragment.kt @@ -6,17 +6,12 @@ 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.bugreporting.ui.toErrorDialogBuilder -import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.databinding.FragmentSubmissionDeletionWarningBinding -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.submission.TestRegistrationStateProcessor.State 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.SingleLiveEvent import de.rki.coronawarnapp.util.ui.doNavigate @@ -38,7 +33,7 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_ factoryProducer = { viewModelFactory }, constructorCall = { factory, _ -> factory as SubmissionDeletionWarningViewModel.Factory - factory.create(args.coronaTestQrCode, args.coronaTestTan, args.isConsentGiven) + factory.create(args.testRegistrationRequest, args.isConsentGiven) } ) private val binding: FragmentSubmissionDeletionWarningBinding by viewBinding() @@ -65,57 +60,38 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_ } viewModel.registrationState.observe2(this) { state -> - binding.submissionQrCodeScanSpinner.isVisible = state.isFetching - binding.continueButton.isVisible = !state.isFetching && state.coronaTest == null - } - viewModel.registrationError.observe2(this) { - showErrorDialog(it) - doNavigate( - SubmissionDeletionWarningFragmentDirections - .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment() - ) - } + val isWorking = state is State.Working + binding.apply { + submissionQrCodeScanSpinner.isVisible = isWorking + continueButton.isVisible = !isWorking + } + when (state) { + State.Idle, + State.Working -> { + // Handled above + } + is State.Error -> { + state.getDialogBuilder(requireContext()).show() + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment() + .run { doNavigate(this) } + } + is State.TestRegistered -> when { + state.test.isPositive -> + NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type) + .run { doNavigate(this) } - viewModel.routeToScreen.observe2(this) { - Timber.d("Navigating to %s", it) - doNavigate(it) - } - } + else -> + NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) + .run { doNavigate(this) } + } + } - private fun showErrorDialog(exception: Throwable) = when (exception) { - is InvalidQRCodeException -> DialogHelper.DialogInstance( - context = requireActivity(), - title = R.string.submission_error_dialog_web_tan_redeemed_title, - message = R.string.submission_error_dialog_web_tan_redeemed_body, - cancelable = true, - positiveButton = R.string.submission_error_dialog_web_tan_redeemed_button_positive, - positiveButtonFunction = { /* dismiss */ }, - ).run { DialogHelper.showDialog(this) } - is BadRequestException -> DialogHelper.DialogInstance( - context = requireActivity(), - title = R.string.submission_qr_code_scan_invalid_dialog_headline, - message = R.string.submission_qr_code_scan_invalid_dialog_body, - cancelable = true, - positiveButton = R.string.submission_qr_code_scan_invalid_dialog_button_positive, - positiveButtonFunction = { /* dismiss */ }, - ).run { DialogHelper.showDialog(this) } - is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance( - context = requireActivity(), - title = R.string.submission_error_dialog_web_generic_error_title, - message = R.string.submission_error_dialog_web_generic_network_error_body, - cancelable = true, - positiveButton = R.string.submission_error_dialog_web_generic_error_button_positive, - positiveButtonFunction = { /* dismiss */ }, - ).run { DialogHelper.showDialog(this) } - is CwaWebException -> DialogHelper.DialogInstance( - context = requireActivity(), - title = R.string.submission_error_dialog_web_generic_error_title, - message = R.string.submission_error_dialog_web_generic_error_body, - cancelable = true, - positiveButton = R.string.submission_error_dialog_web_generic_error_button_positive, - positiveButtonFunction = { /* dismiss */ }, - ).run { DialogHelper.showDialog(this) } - else -> exception.toErrorDialogBuilder(requireContext()).show() + viewModel.routeToScreen.observe2(this) { + Timber.d("Navigating to %s", it) + doNavigate(it) + } + } } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt index 5db57987c07498586d0921592400b48e558619e8..7f479939cbfc28be5c9039e69e2abaf997bc222d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt @@ -1,153 +1,81 @@ package de.rki.coronawarnapp.ui.submission.deletionwarning -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.navigation.NavDirections import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.coronatest.CoronaTestRepository -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode -import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN import de.rki.coronawarnapp.coronatest.type.CoronaTest -import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import kotlinx.coroutines.flow.first import timber.log.Timber class SubmissionDeletionWarningViewModel @AssistedInject constructor( - @Assisted private val coronaTestQrCode: CoronaTestQRCode?, - @Assisted private val coronaTestQrTan: CoronaTestTAN?, + @Assisted private val testRegistrationRequest: TestRegistrationRequest, @Assisted private val isConsentGiven: Boolean, - private val submissionRepository: SubmissionRepository, - private val coronaTestRepository: CoronaTestRepository, + private val registrationStateProcessor: TestRegistrationStateProcessor, ) : CWAViewModel() { val routeToScreen = SingleLiveEvent<NavDirections>() - private val mutableRegistrationState = MutableLiveData(RegistrationState()) - val registrationState: LiveData<RegistrationState> = mutableRegistrationState - val registrationError = SingleLiveEvent<Throwable>() + val registrationState = registrationStateProcessor.state.asLiveData2() - private fun getRegistrationType(): RegistrationType = if (coronaTestQrCode != null) { - RegistrationType.QR - } else { - RegistrationType.TAN - } - - // If there is no qrCode, it must be a TAN, and TANs are always PCR - internal fun getTestType(): CoronaTest.Type = coronaTestQrCode?.type ?: CoronaTest.Type.PCR + internal fun getTestType(): CoronaTest.Type = testRegistrationRequest.type fun deleteExistingAndRegisterNewTest() = launch { - when { - coronaTestQrTan != null -> deleteExistingAndRegisterNewTestWitTAN() - else -> deleteExistingAndRegisterNewTestWithQrCode() - } - } - - private suspend fun deleteExistingAndRegisterNewTestWithQrCode() = try { - requireNotNull(coronaTestQrCode) { "QR Code was unavailable" } - if (coronaTestQrCode.isDccSupportedByPoc) { + if (testRegistrationRequest.isDccSupportedByPoc) { SubmissionDeletionWarningFragmentDirections .actionSubmissionDeletionWarningFragmentToRequestCovidCertificateFragment( - coronaTestQrCode = coronaTestQrCode, + testRegistrationRequest = testRegistrationRequest, coronaTestConsent = isConsentGiven, deleteOldTest = true ).run { routeToScreen.postValue(this) } } else { - removeAndRegisterNew(coronaTestQrCode) + removeAndRegisterNew(testRegistrationRequest) } - } catch (e: Exception) { - Timber.e(e, "Error during test registration via QR code") - mutableRegistrationState.postValue(RegistrationState(isFetching = false)) - registrationError.postValue(e) } - private suspend fun removeAndRegisterNew( - coronaTestQrCode: CoronaTestQRCode - ) { - // Remove existing test and wait until that is done - submissionRepository.testForType(coronaTestQrCode.type).first()?.let { - coronaTestRepository.removeTest(it.identifier) - } ?: Timber.w("Test we will replace with QR was already removed?") - - mutableRegistrationState.postValue(RegistrationState(isFetching = true)) - val coronaTest = submissionRepository.registerTest(coronaTestQrCode) + private suspend fun removeAndRegisterNew(request: TestRegistrationRequest) { + val newTest = registrationStateProcessor.startRegistration( + request = request, + isSubmissionConsentGiven = isConsentGiven, + allowReplacement = true + ) - if (coronaTest.isRedeemed) { - Timber.d("New test was already redeemed, removing it again: %s", coronaTest) - // This does not wait until the test is removed, - // the exception handling should navigate the user to a new screen anyways - submissionRepository.removeTestFromDevice(type = coronaTest.type) - throw InvalidQRCodeException("Test is already redeemed") + if (newTest == null) { + Timber.w("Test registration failed.") + return + } else { + Timber.d("Continuing with our new CoronaTest: %s", newTest) } - if (isConsentGiven) submissionRepository.giveConsentToSubmission(type = coronaTestQrCode.type) - - continueWithNewTest(coronaTest) - mutableRegistrationState.postValue(RegistrationState(coronaTest = coronaTest)) - } - - private suspend fun deleteExistingAndRegisterNewTestWitTAN() = try { - requireNotNull(coronaTestQrTan) { "TAN was unavailable" } - - submissionRepository.testForType(CoronaTest.Type.PCR).first()?.let { - coronaTestRepository.removeTest(it.identifier) - } ?: Timber.w("Test we will replace with TAN was already removed?") - - mutableRegistrationState.postValue(RegistrationState(isFetching = true)) - - val coronaTest = submissionRepository.registerTest(coronaTestQrTan) - continueWithNewTest(coronaTest) - - mutableRegistrationState.postValue(RegistrationState(coronaTest = coronaTest)) - } catch (e: Exception) { - Timber.e(e, "Error during test registration via TAN") - mutableRegistrationState.postValue(RegistrationState(isFetching = false)) - registrationError.postValue(e) - } - - fun onCancelButtonClick() { - SubmissionDeletionWarningFragmentDirections - .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment() - .run { routeToScreen.postValue(this) } - } + when (request) { + is CoronaTestTAN -> + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(newTest.type) - private fun continueWithNewTest(coronaTest: CoronaTest) { - Timber.d("Continuing with our new CoronaTest: %s", coronaTest) - val testType = coronaTestQrCode!!.type - when (getRegistrationType()) { - RegistrationType.QR -> if (coronaTest.isPositive) { + else -> if (newTest.isPositive) { SubmissionDeletionWarningFragmentDirections - .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(testType) + .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment(newTest.type) } else { SubmissionDeletionWarningFragmentDirections - .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(testType) + .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment(newTest.type) } - - RegistrationType.TAN -> - SubmissionDeletionWarningFragmentDirections - .actionSubmissionDeletionFragmentToSubmissionTestResultNoConsentFragment(getTestType()) }.run { routeToScreen.postValue(this) } } - data class RegistrationState( - val isFetching: Boolean = false, - val coronaTest: CoronaTest? = null, - ) - - sealed class RegistrationType { - object TAN : RegistrationType() - object QR : RegistrationType() + fun onCancelButtonClick() { + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment() + .run { routeToScreen.postValue(this) } } @AssistedFactory interface Factory : CWAViewModelFactory<SubmissionDeletionWarningViewModel> { fun create( - coronaTestQrCode: CoronaTestQRCode?, - coronaTestTan: CoronaTestTAN?, + testRegistrationRequest: TestRegistrationRequest, isConsentGiven: Boolean ): SubmissionDeletionWarningViewModel } 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 deleted file mode 100644 index 19ec160f5228e0ec0336abb76c5afdf7f7c9a180..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/QrCodeRegistrationStateProcessor.kt +++ /dev/null @@ -1,60 +0,0 @@ -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.type.CoronaTest -import de.rki.coronawarnapp.exception.ExceptionCategory -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: 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.isRedeemed) { - 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 e5fd022f5390e06bdbdb6a4a1c91e76dcb3429e4..3423316e4783ec40651d668214bef499ba4880cc 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 @@ -10,14 +10,8 @@ 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.submission.TestRegistrationStateProcessor.State import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject @@ -65,8 +59,8 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) doNavigate( NavGraphDirections .actionToSubmissionDeletionWarningFragment( - it.consentGiven, - it.coronaTestQRCode + testRegistrationRequest = it.coronaTestQRCode, + isConsentGiven = it.consentGiven, ) ) } @@ -76,56 +70,36 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) 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.qrCodeError.observe2(this) { + showInvalidQrCodeDialog() } viewModel.registrationState.observe2(this) { state -> - binding.progressSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED - binding.submissionConsentButton.isEnabled = when (state.apiRequestState) { - ApiRequestState.STARTED -> false - else -> true + val isWorking = state is State.Working + binding.apply { + progressSpinner.isVisible = isWorking + submissionConsentButton.isEnabled = !isWorking } - - 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 - ) - ) - } - } + when (state) { + State.Idle, + State.Working -> { + // Handled above + } + is State.Error -> { + state.getDialogBuilder(requireContext()).show() + popBackStack() + } + is State.TestRegistered -> when { + state.test.isPositive -> + NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type) + .run { doNavigate(this) } + + else -> + NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) + .run { doNavigate(this) } } } } - - viewModel.registrationError.observe2(this) { - DialogHelper.showDialog(buildErrorDialog(it)) - } } override fun onResume() { @@ -149,49 +123,15 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) R.string.submission_qr_code_scan_invalid_dialog_button_negative, true, positiveButtonFunction = {}, - negativeButtonFunction = ::navigateHome + negativeButtonFunction = { + popBackStack() + Unit + } ) 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 f6876b62aeebd0f5662569e1a26073a0fd7e8e66..4bd68d5045e1c50daba3de2ac99f2ab086d84fdd 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 @@ -9,7 +9,7 @@ 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.submission.TestRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -22,17 +22,14 @@ class SubmissionConsentViewModel @AssistedInject constructor( interoperabilityRepository: InteroperabilityRepository, dispatcherProvider: DispatcherProvider, private val tekHistoryProvider: TEKHistoryProvider, - private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor, + private val registrationStateProcessor: TestRegistrationStateProcessor, private val submissionRepository: SubmissionRepository, private val qrCodeValidator: CoronaTestQrCodeValidator ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() - val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>() - - val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning - val registrationState = qrCodeRegistrationStateProcessor.registrationState - val registrationError = qrCodeRegistrationStateProcessor.registrationError + val qrCodeError = SingleLiveEvent<Exception>() + val registrationState = registrationStateProcessor.state.asLiveData2() val countries = interoperabilityRepository.countryList .asLiveData(context = dispatcherProvider.Default) @@ -76,24 +73,27 @@ class SubmissionConsentViewModel @AssistedInject constructor( } private suspend fun validateAndRegister(qrCodeString: String) { - try { - val coronaTestQRCode = qrCodeValidator.validate(qrCodeString) - 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) - } + val coronaTestQRCode = try { + qrCodeValidator.validate(qrCodeString) } catch (err: InvalidQRCodeException) { - Timber.i(err) - qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID) + Timber.i(err, "Failed to validate QRCode") + qrCodeError.postValue(err) + return + } + + val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first() + + if (coronaTest != null) { + SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode( + coronaTestQRCode, + consentGiven = true + ).run { routeToScreen.postValue(this) } + } else { + registrationStateProcessor.startRegistration( + request = coronaTestQRCode, + isSubmissionConsentGiven = true, + allowReplacement = false + ) } } 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 6839584b08883c9d03fcb3e5b107c8123a8bf331..6484865f7cb2706c9559ad71214401bec7d2b5fc 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 @@ -7,19 +7,15 @@ import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs +import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.Type +import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeScanBinding 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 +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor.State import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject @@ -30,7 +26,6 @@ import de.rki.coronawarnapp.util.ui.popBackStack import de.rki.coronawarnapp.util.ui.viewBinding import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted -import timber.log.Timber import javax.inject.Inject /** @@ -72,7 +67,10 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co when (it) { is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromQrCode -> { NavGraphDirections - .actionToSubmissionDeletionWarningFragment(it.consentGiven, it.coronaTestQRCode) + .actionToSubmissionDeletionWarningFragment( + testRegistrationRequest = it.coronaTestQRCode, + isConsentGiven = it.consentGiven, + ) .run { doNavigate(this) } } is SubmissionNavigationEvents.NavigateToDispatcher -> navigateToDispatchScreen() @@ -83,57 +81,44 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co } } - viewModel.qrCodeValidationState.observe2(this) { - if (QrCodeRegistrationStateProcessor.ValidationState.INVALID == it) { - DialogHelper.showDialog(createInvalidScanDialog()) - } - } - - viewModel.registrationError.observe2(this) { - DialogHelper.showDialog(buildErrorDialog(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) - goBack() + viewModel.qrCodeErrorEvent.observe2(this) { + showInvalidQrCodeDialog() } viewModel.registrationState.observe2(this) { state -> - when (state.apiRequestState) { - ApiRequestState.STARTED -> binding.submissionQrCodeScanSpinner.show() - else -> binding.submissionQrCodeScanSpinner.hide() + if (state is State.Working) { + binding.submissionQrCodeScanSpinner.show() + } else { + binding.submissionQrCodeScanSpinner.hide() } - when (state.test?.testResult) { - CoronaTestResult.PCR_POSITIVE -> - NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = Type.PCR) - - CoronaTestResult.PCR_OR_RAT_PENDING -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) - - CoronaTestResult.PCR_NEGATIVE, - CoronaTestResult.PCR_INVALID, - CoronaTestResult.PCR_REDEEMED -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = Type.PCR) - - CoronaTestResult.RAT_POSITIVE -> - NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = Type.RAPID_ANTIGEN) - - CoronaTestResult.RAT_NEGATIVE, - CoronaTestResult.RAT_INVALID, - CoronaTestResult.RAT_PENDING, - CoronaTestResult.RAT_REDEEMED -> - NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = Type.RAPID_ANTIGEN) - null -> { - Timber.w("Successful API request, but test was null?") - return@observe2 + when (state) { + State.Idle, + State.Working -> { + // Handled above + } + is State.Error -> { + when (state.exception) { + is BadRequestException -> showInvalidQrCodeDialog() + else -> { + state.getDialogBuilder(requireContext()).apply { + when (state.exception) { + is AlreadyRedeemedException -> setOnDismissListener { goBack() } + else -> setOnDismissListener { navigateToDispatchScreen() } + } + }.show() + } + } } - }.run { doNavigate(this) } + is State.TestRegistered -> when { + state.test.isPositive -> + NavGraphDirections.actionToSubmissionTestResultAvailableFragment(testType = state.test.type) + .run { doNavigate(this) } + + else -> + NavGraphDirections.actionSubmissionTestResultPendingFragment(testType = state.test.type) + .run { doNavigate(this) } + } + } } } @@ -143,45 +128,23 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co } } - private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance { - return when (exception) { - is BadRequestException -> createInvalidScanDialog() - 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, - ::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 - ) - } - } - private fun navigateToDispatchScreen() = doNavigate( SubmissionQRCodeScanFragmentDirections.actionSubmissionQRCodeScanFragmentToSubmissionDispatcherFragment() ) - private fun createInvalidScanDialog() = 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, - { startDecode() }, - { viewModel.onBackPressed() }, - { viewModel.onBackPressed() } - ) + private fun showInvalidQrCodeDialog() { + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.submission_qr_code_scan_invalid_dialog_headline) + setMessage(R.string.submission_qr_code_scan_invalid_dialog_body) + setPositiveButton(R.string.submission_qr_code_scan_invalid_dialog_button_positive) { _, _ -> + startDecode() + } + setNegativeButton(R.string.submission_qr_code_scan_invalid_dialog_button_negative) { _, _ -> + viewModel.onBackPressed() + } + setOnCancelListener { viewModel.onBackPressed() } + }.show() + } override fun onRequestPermissionsResult( requestCode: Int, 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 b1e0a834805da9dffe95c759703f5ef3752ce5f9..b707a2e43b34b242a43543bb9e0f685559d7832c 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 @@ -7,7 +7,7 @@ import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.permission.CameraSettings @@ -21,21 +21,20 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, @Assisted private val isConsentGiven: Boolean, private val cameraSettings: CameraSettings, - private val qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor, + private val registrationStateProcessor: TestRegistrationStateProcessor, private val submissionRepository: SubmissionRepository, private val qrCodeValidator: CoronaTestQrCodeValidator, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val events = SingleLiveEvent<SubmissionNavigationEvents>() - val qrCodeValidationState = SingleLiveEvent<QrCodeRegistrationStateProcessor.ValidationState>() - val showRedeemedTokenWarning = qrCodeRegistrationStateProcessor.showRedeemedTokenWarning - val registrationState = qrCodeRegistrationStateProcessor.registrationState - val registrationError = qrCodeRegistrationStateProcessor.registrationError + val qrCodeErrorEvent = SingleLiveEvent<Exception>() + val registrationState = registrationStateProcessor.state.asLiveData2() fun registerCoronaTest(rawResult: String) = launch { try { val ctQrCode = qrCodeValidator.validate(rawResult) - qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.SUCCESS) + val coronaTest = submissionRepository.testForType(ctQrCode.type).first() when { coronaTest != null -> events.postValue( @@ -46,7 +45,12 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( ) else -> if (!ctQrCode.isDccSupportedByPoc) { - qrCodeRegistrationStateProcessor.startQrCodeRegistration(ctQrCode, isConsentGiven) + registrationStateProcessor.startRegistration( + request = ctQrCode, + isSubmissionConsentGiven = isConsentGiven, + allowReplacement = false + ) + if (isConsentGiven) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(ctQrCode.type) } else { events.postValue( @@ -56,7 +60,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( } } catch (err: InvalidQRCodeException) { Timber.d(err, "Invalid QrCode") - qrCodeValidationState.postValue(QrCodeRegistrationStateProcessor.ValidationState.INVALID) + qrCodeErrorEvent.postValue(err) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt index 97ea51f53dcb69910aeda363e1097fbcf3f10aff..f669290e7550a91070a4e39dff6d96c0224d0207 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanFragment.kt @@ -55,8 +55,8 @@ class SubmissionTanFragment : Fragment(R.layout.fragment_submission_tan), AutoIn is SubmissionNavigationEvents.NavigateToDeletionWarningFragmentFromTan -> doNavigate( SubmissionTanFragmentDirections.actionSubmissionTanFragmentToSubmissionDeletionWarningFragment( + testRegistrationRequest = it.coronaTestTan, isConsentGiven = it.consentGiven, - coronaTestTan = it.coronaTestTan ) ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt index eca763ca8350351466411c1cdbf9051ac9d5bde8..d132186293c7d25e07426910920b187be39ec3b6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.util.viewmodel import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -27,6 +28,8 @@ abstract class CWAViewModel constructor( Timber.tag(tag).v("Initialized") } + fun <T> Flow<T>.asLiveData2() = asLiveData(context = dispatcherProvider.Default) + /** * This launches a coroutine on another thread * Remember to switch to the main thread if you want to update the UI directly 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 8d879a4bb77ffea0650d57ebc6590f0d199bfeae..b4f78c8b26e030ee87f1dbbcb67c4ba22d99ece0 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -715,20 +715,13 @@ android:name="de.rki.coronawarnapp.ui.submission.deletionwarning.SubmissionDeletionWarningFragment" android:label="SubmissionDeletionWarningFragment" tools:layout="@layout/fragment_submission_deletion_warning"> + <argument + android:name="testRegistrationRequest" + app:argType="de.rki.coronawarnapp.coronatest.TestRegistrationRequest" /> <argument android:name="isConsentGiven" android:defaultValue="false" app:argType="boolean" /> - <argument - android:name="coronaTestQrCode" - android:defaultValue="@null" - app:argType="de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode" - app:nullable="true" /> - <argument - android:name="coronaTestTan" - android:defaultValue="@null" - app:argType="de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN" - app:nullable="true" /> <action android:id="@+id/action_submissionDeletionWarningFragment_to_submissionDispatcherFragment" app:popUpTo="@id/submissionDispatcherFragment" @@ -834,8 +827,8 @@ android:label="fragment_request_covid_certificate" tools:layout="@layout/fragment_request_covid_certificate"> <argument - android:name="coronaTestQrCode" - app:argType="de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode" /> + android:name="testRegistrationRequest" + app:argType="de.rki.coronawarnapp.coronatest.TestRegistrationRequest" /> <argument android:name="coronaTestConsent" android:defaultValue="false" @@ -848,7 +841,8 @@ <action android:id="@+id/action_requestCovidCertificateFragment_to_dispatcherFragment" - app:popUpTo="@id/submissionDispatcherFragment" + app:destination="@id/submissionDispatcherFragment" + app:popUpTo="@id/mainFragment" app:popUpToInclusive="false" /> <action diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt index 7eb566a58f9810a6458f0c3499b0d2d6243e4a1a..3ec942256f9095bacf839def1c1ed7a44e6b5f64 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryTest.kt @@ -1,7 +1,9 @@ package de.rki.coronawarnapp.coronatest import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.storage.CoronaTestStorage import de.rki.coronawarnapp.coronatest.type.CoronaTest @@ -9,6 +11,8 @@ import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.coronatest.type.pcr.PCRTestProcessor import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest import de.rki.coronawarnapp.coronatest.type.rapidantigen.RATestProcessor +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery @@ -29,20 +33,28 @@ class CoronaTestRepositoryTest : BaseTest() { @MockK lateinit var legacyMigration: PCRTestMigration @MockK lateinit var contactDiaryRepository: ContactDiaryRepository + @MockK lateinit var pcrProcessor: PCRTestProcessor + @MockK lateinit var raProcessor: RATestProcessor + private var coronaTestsInStorage = mutableSetOf<CoronaTest>() + private val pcrRegistrationRequest = CoronaTestQRCode.PCR( + qrCodeGUID = "pcr-guid" + ) private val pcrTest = PCRCoronaTest( - identifier = "pcr-identifier", + identifier = pcrRegistrationRequest.identifier, lastUpdatedAt = Instant.EPOCH, registeredAt = Instant.EPOCH, registrationToken = "token", testResult = CoronaTestResult.PCR_REDEEMED, ) - @MockK lateinit var pcrProcessor: PCRTestProcessor - @MockK lateinit var raProcessor: RATestProcessor + private val raRegistrationRequest = CoronaTestQRCode.RapidAntigen( + hash = "ra-hash", + createdAt = Instant.EPOCH + ) private val raTest = RACoronaTest( - identifier = "ra-identifier", + identifier = raRegistrationRequest.identifier, lastUpdatedAt = Instant.EPOCH, registeredAt = Instant.EPOCH, registrationToken = "token", @@ -54,9 +66,6 @@ class CoronaTestRepositoryTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - coronaTestsInStorage.add(pcrTest) - coronaTestsInStorage.add(raTest) - legacyMigration.apply { coEvery { startMigration() } returns emptySet() coEvery { finishMigration() } just Runs @@ -75,11 +84,13 @@ class CoronaTestRepositoryTest : BaseTest() { } pcrProcessor.apply { + coEvery { create(pcrRegistrationRequest) } returns pcrTest coEvery { updateSubmissionConsent(any(), any()) } answers { arg<PCRCoronaTest>(0) } every { type } returns CoronaTest.Type.PCR } raProcessor.apply { + coEvery { create(raRegistrationRequest) } returns raTest coEvery { updateSubmissionConsent(any(), any()) } answers { arg<RACoronaTest>(0) } every { type } returns CoronaTest.Type.RAPID_ANTIGEN } @@ -96,8 +107,31 @@ class CoronaTestRepositoryTest : BaseTest() { @Test fun `give submission consent`() = runBlockingTest2(ignoreActive = true) { + coronaTestsInStorage.add(pcrTest) + createInstance(this).updateSubmissionConsent(pcrTest.identifier, true) coVerify { pcrProcessor.updateSubmissionConsent(pcrTest, true) } } + + @Test + fun `test registration with default conditions`() = runBlockingTest2(ignoreActive = true) { + coronaTestsInStorage.clear() + val negativePcr = pcrTest.copy(testResult = CoronaTestResult.PCR_NEGATIVE) + coEvery { pcrProcessor.create(pcrRegistrationRequest) } returns negativePcr + val instance = createInstance(this) + + instance.registerTest(pcrRegistrationRequest) shouldBe negativePcr + } + + @Test + fun `test registration with default conditions and existing test`() = runBlockingTest2(ignoreActive = true) { + coronaTestsInStorage.add(pcrTest) + + val instance = createInstance(this) + + shouldThrow<DuplicateCoronaTestException> { + instance.registerTest(pcrRegistrationRequest) shouldBe pcrTest + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt index ae77a7e3ca94f391438e9a5f0aedc71cbcc48fe8..1a03d4997566f27f6f93997df101a9bfb352b7a4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt @@ -1,15 +1,30 @@ package de.rki.coronawarnapp.storage import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.errors.AlreadyRedeemedException +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.slot import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference class SubmissionRepositoryTest : BaseTest() { @@ -17,9 +32,29 @@ class SubmissionRepositoryTest : BaseTest() { @MockK lateinit var tekHistoryStorage: TEKHistoryStorage @MockK lateinit var coronaTestRepository: CoronaTestRepository + private val pcrRegistrationRequest = CoronaTestQRCode.PCR( + qrCodeGUID = "pcr-guid" + ) + private val pcrTest = PCRCoronaTest( + identifier = pcrRegistrationRequest.identifier, + lastUpdatedAt = Instant.EPOCH, + registeredAt = Instant.EPOCH, + registrationToken = "token", + testResult = CoronaTestResult.PCR_REDEEMED, + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this) + + coronaTestRepository.apply { + every { coronaTests } returns emptyFlow() + coEvery { registerTest(pcrRegistrationRequest, any(), any()) } returns pcrTest + } + + submissionSettings.apply { + every { symptoms } returns mockFlowPreference(null) + } } fun createInstance(scope: CoroutineScope) = SubmissionRepository( @@ -30,7 +65,23 @@ class SubmissionRepositoryTest : BaseTest() { ) @Test - fun todo() { - // TODO + fun `tryReplaceTest overrides register test conditions`() = runBlockingTest { + val precondition = slot<(Collection<CoronaTest>) -> Boolean>() + val postcondition = slot<(CoronaTest) -> Boolean>() + + val instance = createInstance(scope = this) + + instance.tryReplaceTest(pcrRegistrationRequest) + + coVerify { coronaTestRepository.registerTest(any(), capture(precondition), capture(postcondition)) } + + precondition.captured(emptyList()) shouldBe true + precondition.captured(listOf(pcrTest)) shouldBe true + + shouldThrow<AlreadyRedeemedException> { + postcondition.captured(pcrTest) + } + + postcondition.captured(pcrTest.copy(testResult = CoronaTestResult.PCR_NEGATIVE)) shouldBe true } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..238c38ddeea4605f50275a8b30763e8e7a18e9c7 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/TestRegistrationStateProcessorTest.kt @@ -0,0 +1,177 @@ +package de.rki.coronawarnapp.submission + +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.exception.http.BadRequestException +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TestRegistrationStateProcessorTest : BaseTest() { + + @MockK lateinit var submissionRepository: SubmissionRepository + @MockK lateinit var request: TestRegistrationRequest + @MockK lateinit var registeredTest: CoronaTest + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + submissionRepository.apply { + coEvery { registerTest(any()) } returns registeredTest + coEvery { tryReplaceTest(any()) } returns registeredTest + coEvery { giveConsentToSubmission(any()) } just Runs + } + + registeredTest.apply { + every { type } returns CoronaTest.Type.RAPID_ANTIGEN + } + + request.apply { + every { type } returns CoronaTest.Type.RAPID_ANTIGEN + } + } + + private fun createInstance() = TestRegistrationStateProcessor( + submissionRepository = submissionRepository + ) + + @Test + fun `register new test - with consent`() = runBlockingTest { + val instance = createInstance() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle + + instance.startRegistration( + request = request, + isSubmissionConsentGiven = true, + allowReplacement = false + ) + + advanceUntilIdle() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered( + test = registeredTest + ) + + coVerify { + submissionRepository.registerTest(request) + submissionRepository.giveConsentToSubmission(CoronaTest.Type.RAPID_ANTIGEN) + } + } + + @Test + fun `register new test - without consent`() = runBlockingTest { + val instance = createInstance() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle + + instance.startRegistration( + request = request, + isSubmissionConsentGiven = false, + allowReplacement = false + ) + + advanceUntilIdle() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered( + test = registeredTest + ) + + coVerify { + submissionRepository.registerTest(request) + } + coVerify(exactly = 0) { + submissionRepository.giveConsentToSubmission(any()) + } + } + + @Test + fun `replace test - with consent`() = runBlockingTest { + val instance = createInstance() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle + + instance.startRegistration( + request = request, + isSubmissionConsentGiven = true, + allowReplacement = true + ) + + advanceUntilIdle() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered( + test = registeredTest + ) + + coVerify { + submissionRepository.tryReplaceTest(request) + submissionRepository.giveConsentToSubmission(CoronaTest.Type.RAPID_ANTIGEN) + } + } + + @Test + fun `replace new test - without consent`() = runBlockingTest { + val instance = createInstance() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle + + instance.startRegistration( + request = request, + isSubmissionConsentGiven = false, + allowReplacement = true + ) + + advanceUntilIdle() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.TestRegistered( + test = registeredTest + ) + + coVerify { + submissionRepository.tryReplaceTest(request) + } + coVerify(exactly = 0) { + submissionRepository.giveConsentToSubmission(any()) + } + } + + @Test + fun `errors are mapped to state`() = runBlockingTest { + val instance = createInstance() + + val expectedException = BadRequestException("") + coEvery { submissionRepository.registerTest(any()) } throws expectedException + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Idle + + instance.startRegistration( + request = request, + isSubmissionConsentGiven = true, + allowReplacement = false + ) + + advanceUntilIdle() + + instance.state.first() shouldBe TestRegistrationStateProcessor.State.Error( + exception = expectedException + ) + + coVerify { + submissionRepository.registerTest(request) + } + coVerify(exactly = 0) { + submissionRepository.giveConsentToSubmission(any()) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt index 009f839c5c79d3bfed61a95225a0f6cea8335c89..7787f4a27c46facb49b5b4b1063951cab25bd446 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/covidcertificate/RequestCovidCertificateViewModelTest.kt @@ -1,13 +1,9 @@ package de.rki.coronawarnapp.ui.submission.covidcertificate -import androidx.lifecycle.MutableLiveData -import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.qrcode.QrCodeRegistrationStateProcessor -import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.submission.TestRegistrationStateProcessor import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -16,6 +12,7 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.joda.time.Instant import org.joda.time.LocalDate @@ -30,10 +27,8 @@ import testhelpers.extensions.getOrAwaitValue @ExtendWith(InstantExecutorExtension::class) internal class RequestCovidCertificateViewModelTest : BaseTest() { - @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector - @MockK lateinit var submissionRepository: SubmissionRepository - @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var coronaTest: CoronaTest private val date = LocalDate.parse( @@ -48,24 +43,27 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - qrCodeRegistrationStateProcessor.apply { - coEvery { startQrCodeRegistration(any(), any()) } just Runs - coEvery { registrationError } returns SingleLiveEvent() - coEvery { showRedeemedTokenWarning } returns SingleLiveEvent() - coEvery { registrationState } returns MutableLiveData() + testRegistrationStateProcessor.apply { + coEvery { startRegistration(any(), any(), any()) } returns mockk() + coEvery { state } returns flowOf(TestRegistrationStateProcessor.State.Idle) } - submissionRepository.apply { - coEvery { registerTest(any()) } returns coronaTest - coEvery { testForType(any()) } returns flowOf(coronaTest) - } - - coEvery { coronaTestRepository.removeTest(any()) } returns coronaTest - every { coronaTest.identifier } returns "identifier" every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs } + private fun createInstance( + coronaTestQRCode: CoronaTestQRCode = pcrQRCode, + coronTestConsent: Boolean = true, + deleteOldTest: Boolean = false + ) = RequestCovidCertificateViewModel( + testRegistrationRequest = coronaTestQRCode, + coronaTestConsent = coronTestConsent, + deleteOldTest = deleteOldTest, + registrationStateProcessor = testRegistrationStateProcessor, + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + ) + @Test fun birthDateChanged() { createInstance().apply { @@ -81,11 +79,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onAgreeGC() coVerify { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), - any() + testRegistrationStateProcessor.startRegistration( + request = pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), + isSubmissionConsentGiven = any(), + allowReplacement = true ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } @@ -99,17 +96,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onAgreeGC() coVerify { - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), - any() + testRegistrationStateProcessor.startRegistration( + request = pcrQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), + isSubmissionConsentGiven = any(), + allowReplacement = false ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } - - coVerify(exactly = 0) { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - } } } @@ -119,11 +112,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onDisagreeGC() coVerify { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - pcrQRCode.copy(isDccConsentGiven = false), - any() + testRegistrationStateProcessor.startRegistration( + request = pcrQRCode.copy(isDccConsentGiven = false), + isSubmissionConsentGiven = any(), + allowReplacement = true ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } @@ -136,17 +128,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onDisagreeGC() coVerify { - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - pcrQRCode.copy(isDccConsentGiven = false), - any() + testRegistrationStateProcessor.startRegistration( + request = pcrQRCode.copy(isDccConsentGiven = false), + isSubmissionConsentGiven = any(), + allowReplacement = false ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } - - coVerify(exactly = 0) { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - } } } @@ -156,11 +144,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onAgreeGC() coVerify { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - ratQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), - any() + testRegistrationStateProcessor.startRegistration( + request = ratQRCode.copy(isDccConsentGiven = true, dateOfBirth = date), + isSubmissionConsentGiven = any(), + allowReplacement = true ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } @@ -173,17 +160,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onAgreeGC() coVerify { - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - ratQRCode.copy(isDccConsentGiven = true), - any() + testRegistrationStateProcessor.startRegistration( + request = ratQRCode.copy(isDccConsentGiven = true), + isSubmissionConsentGiven = any(), + allowReplacement = false ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } - - coVerify(exactly = 0) { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - } } } @@ -193,11 +176,10 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onDisagreeGC() coVerify { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - ratQRCode.copy(isDccConsentGiven = false), - any() + testRegistrationStateProcessor.startRegistration( + request = ratQRCode.copy(isDccConsentGiven = false), + isSubmissionConsentGiven = any(), + allowReplacement = true ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } @@ -210,17 +192,13 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { onDisagreeGC() coVerify { - qrCodeRegistrationStateProcessor.startQrCodeRegistration( - ratQRCode.copy(isDccConsentGiven = false), - any() + testRegistrationStateProcessor.startRegistration( + request = ratQRCode.copy(isDccConsentGiven = false), + isSubmissionConsentGiven = any(), + allowReplacement = false ) analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } - - coVerify(exactly = 0) { - submissionRepository.testForType(any()) - coronaTestRepository.removeTest(any()) - } } } @@ -287,18 +265,4 @@ internal class RequestCovidCertificateViewModelTest : BaseTest() { } } } - - private fun createInstance( - coronaTestQRCode: CoronaTestQRCode = pcrQRCode, - coronTestConsent: Boolean = true, - deleteOldTest: Boolean = false - ) = RequestCovidCertificateViewModel( - coronaTestQrCode = coronaTestQRCode, - coronaTestConsent = coronTestConsent, - deleteOldTest = deleteOldTest, - coronaTestRepository = coronaTestRepository, - submissionRepository = submissionRepository, - qrCodeRegistrationStateProcessor = qrCodeRegistrationStateProcessor, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector - ) } 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 41eb4280816b12a78fe0c6ba1e830ae58188aeb9..36730890014ffbc629c94692b9501c5c47c2d550 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,16 +1,13 @@ package de.rki.coronawarnapp.ui.submission.qrcode.consent -import androidx.lifecycle.MutableLiveData import com.google.android.gms.common.api.ApiException 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.submission.TestRegistrationStateProcessor 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 @@ -21,6 +18,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -34,7 +32,7 @@ class SubmissionConsentViewModelTest { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var tekHistoryProvider: TEKHistoryProvider - @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator lateinit var viewModel: SubmissionConsentViewModel @@ -46,16 +44,17 @@ class SubmissionConsentViewModelTest { MockKAnnotations.init(this) every { interoperabilityRepository.countryList } returns MutableStateFlow(countryList) coEvery { submissionRepository.giveConsentToSubmission(any()) } just Runs - coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent() - coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData( - QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE) - ) - coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent() + + testRegistrationStateProcessor.apply { + every { state } returns flowOf(TestRegistrationStateProcessor.State.Idle) + coEvery { startRegistration(any(), any(), any()) } returns mockk() + } + viewModel = SubmissionConsentViewModel( interoperabilityRepository, dispatcherProvider = TestDispatcherProvider(), tekHistoryProvider, - qrCodeRegistrationStateProcessor, + testRegistrationStateProcessor, submissionRepository, qrCodeValidator ) 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 18b2c8425196c280a168f20a048ac15bb3e5bd9a..977907d7f68634f32c0825e343ba360c85f6be81 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,17 +1,13 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan -import androidx.lifecycle.MutableLiveData 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.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.submission.SubmissionRepository -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.submission.TestRegistrationStateProcessor 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.Runs @@ -19,8 +15,10 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach @@ -37,7 +35,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var cameraSettings: CameraSettings @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator - @MockK lateinit var qrCodeRegistrationStateProcessor: QrCodeRegistrationStateProcessor + @MockK lateinit var testRegistrationStateProcessor: TestRegistrationStateProcessor @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @BeforeEach @@ -45,18 +43,18 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { MockKAnnotations.init(this) every { submissionRepository.testForType(any()) } returns MutableStateFlow<CoronaTest?>(null) - coEvery { qrCodeRegistrationStateProcessor.showRedeemedTokenWarning } returns SingleLiveEvent() - coEvery { qrCodeRegistrationStateProcessor.registrationState } returns MutableLiveData( - QrCodeRegistrationStateProcessor.RegistrationState(ApiRequestState.IDLE) - ) - coEvery { qrCodeRegistrationStateProcessor.registrationError } returns SingleLiveEvent() + + testRegistrationStateProcessor.apply { + every { state } returns flowOf(TestRegistrationStateProcessor.State.Idle) + coEvery { startRegistration(any(), any(), any()) } returns mockk() + } } private fun createViewModel() = SubmissionQRCodeScanViewModel( isConsentGiven = true, dispatcherProvider = TestDispatcherProvider(), cameraSettings = cameraSettings, - qrCodeRegistrationStateProcessor = qrCodeRegistrationStateProcessor, + registrationStateProcessor = testRegistrationStateProcessor, submissionRepository = submissionRepository, qrCodeValidator = qrCodeValidator, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector @@ -74,22 +72,22 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { val invalidQrCode = "https://no-guid-here" every { qrCodeValidator.validate(validQrCode) } returns coronaTestQRCode - every { qrCodeValidator.validate(invalidQrCode) } throws InvalidQRCodeException() + + val expectedError = InvalidQRCodeException() + every { qrCodeValidator.validate(invalidQrCode) } throws expectedError val viewModel = createViewModel() + viewModel.qrCodeErrorEvent.observeForever {} // start - viewModel.qrCodeValidationState.value = ValidationState.STARTED - - viewModel.qrCodeValidationState.value shouldBe ValidationState.STARTED + viewModel.qrCodeErrorEvent.value shouldBe null viewModel.registerCoronaTest(validQrCode) - viewModel.qrCodeValidationState.observeForever {} - viewModel.qrCodeValidationState.value shouldBe ValidationState.SUCCESS + viewModel.qrCodeErrorEvent.value shouldBe null // invalid guid viewModel.registerCoronaTest(invalidQrCode) - viewModel.qrCodeValidationState.value shouldBe ValidationState.INVALID + viewModel.qrCodeErrorEvent.value shouldBe expectedError } @Test @@ -107,7 +105,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { qrCodeValidator.validate(any()) } returns coronaTestQRCode every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs - coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs createViewModel().registerCoronaTest(rawResult = "") @@ -124,7 +121,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { qrCodeValidator.validate(any()) } returns coronaTestQRCode every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs - coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs createViewModel().registerCoronaTest(rawResult = "") @@ -145,7 +141,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { qrCodeValidator.validate(any()) } returns coronaTestQRCode every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs - coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs createViewModel().registerCoronaTest(rawResult = "") @@ -168,7 +163,6 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { qrCodeValidator.validate(any()) } returns coronaTestQRCode every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven(any()) } just Runs - coEvery { qrCodeRegistrationStateProcessor.startQrCodeRegistration(any(), any()) } just Runs createViewModel().registerCoronaTest(rawResult = "") verify(exactly = 0) {