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 7748ab27cde652375f87387997882dea0eec59a9..27bc93242d927d3caea09fffa5fafb3a18dde9df 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 @@ -48,11 +48,9 @@ class SubmissionConsentFragmentTest : BaseUITest() { every { interoperabilityRepository.countryList } returns flowOf() viewModel = SubmissionConsentViewModel( - submissionRepository, interoperabilityRepository, TestDispatcherProvider(), tekHistoryProvider, - analyticsKeySubmissionCollector ) setupMockViewModel( object : SubmissionConsentViewModel.Factory { diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeScanFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeScanFragmentTest.kt index 06ab62aa36eba132f077b81be1b302b906204f0a..e516c0b18e863220d12b08eb4ed205ff83fb7468 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeScanFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeScanFragmentTest.kt @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanFragment +import de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanFragmentArgs import de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanViewModel import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK @@ -19,12 +20,14 @@ class SubmissionQrCodeScanFragmentTest : BaseUITest() { @MockK lateinit var viewModel: SubmissionQRCodeScanViewModel + private var fragmentArgs = SubmissionQRCodeScanFragmentArgs(isConsentGiven = true).toBundle() + @Before fun setup() { MockKAnnotations.init(this, relaxed = true) setupMockViewModel( object : SubmissionQRCodeScanViewModel.Factory { - override fun create(): SubmissionQRCodeScanViewModel = viewModel + override fun create(isConsentGiven: Boolean): SubmissionQRCodeScanViewModel = viewModel } ) } @@ -36,7 +39,9 @@ class SubmissionQrCodeScanFragmentTest : BaseUITest() { @Test fun launch_fragment() { - launchFragment2<SubmissionQRCodeScanFragment>() + launchFragment2<SubmissionQRCodeScanFragment>( + fragmentArgs = fragmentArgs + ) } } 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 8a918bfad256615ff081bfd5d0748bb8a06e4d43..10d580e8001ffd7781360be6bb0e3312c3d50cdc 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 @@ -3,19 +3,43 @@ package de.rki.coronawarnapp.ui.submission.deletionwarning import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +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.ui.submission.ApiRequestState +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 +import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider -import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import javax.inject.Inject class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_deletion_warning), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: SubmissionDeletionWarningFragmentViewModel by cwaViewModels { viewModelFactory } + + private val args by navArgs<SubmissionDeletionWarningFragmentArgs>() + val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + + private val viewModel: SubmissionDeletionWarningViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as SubmissionDeletionWarningViewModel.Factory + factory.create(args.coronaTestQrCode, args.isConsentGiven) + } + ) private val binding: FragmentSubmissionDeletionWarningBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -23,14 +47,121 @@ class SubmissionDeletionWarningFragment : Fragment(R.layout.fragment_submission_ binding.apply { - cancelButton.setOnClickListener { /* TODO */ } + when (args.coronaTestQrCode.type) { + CoronaTest.Type.PCR -> { + headline.text = getString(R.string.submission_deletion_warning_headline_pcr_test) + body.text = getString(R.string.submission_deletion_warning_body_pcr_test) + } - continueButton.setOnClickListener { /* TODO */ } + CoronaTest.Type.RAPID_ANTIGEN -> { + headline.text = getString(R.string.submission_deletion_warning_headline_antigen_test) + body.text = getString(R.string.submission_deletion_warning_body_antigen_test) + } + } + + continueButton.setOnClickListener { + viewModel.deleteExistingAndRegisterNewTest() + } - toolbar.apply { - setNavigationOnClickListener { /* TODO */ } + toolbar.setNavigationOnClickListener { + viewModel.onCancelButtonClick() } } + + 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) + + navigateToDispatchScreen() + } + + viewModel.registrationState.observe2(this) { state -> + binding.submissionQrCodeScanSpinner.isVisible = state.apiRequestState == ApiRequestState.STARTED + + if (ApiRequestState.SUCCESS == state.apiRequestState) { + if (state.testResult == CoronaTestResult.PCR_POSITIVE) { + viewModel.triggerNavigationToSubmissionTestResultAvailableFragment() + } else { + viewModel.triggerNavigationToSubmissionTestResultPendingFragment() + } + } + } + + viewModel.registrationError.observe2(this) { + DialogHelper.showDialog(buildErrorDialog(it)) + } + + viewModel.routeToScreen.observe2(this) { + when (it) { + SubmissionNavigationEvents.NavigateToConsent -> { + doNavigate( + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionConsentFragment() + ) + } + is SubmissionNavigationEvents.NavigateToResultAvailableScreen -> { + doNavigate( + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionTestResultAvailableFragment( + it.consentGiven + ) + ) + } + is SubmissionNavigationEvents.NavigateToResultPendingScreen -> { + doNavigate( + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionTestResultPendingFragment( + it.consentGiven + ) + ) + } + } + } + } + + private fun navigateToDispatchScreen() = + doNavigate( + SubmissionDeletionWarningFragmentDirections + .actionSubmissionDeletionWarningFragmentToSubmissionDispatcherFragment() + ) + + 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, + { /* startDecode() */ }, + ::navigateToDispatchScreen + ) + 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 + ) + } } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragmentViewModel.kt deleted file mode 100644 index a2e1e24371ff9b72a431b46e7077baf17205aeb3..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningFragmentViewModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.rki.coronawarnapp.ui.submission.deletionwarning - -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory - -class SubmissionDeletionWarningFragmentViewModel @AssistedInject constructor() : CWAViewModel() { - - @AssistedFactory - interface Factory : SimpleCWAViewModelFactory<SubmissionDeletionWarningFragmentViewModel> -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningModule.kt index ac2010cd75d8413c47987c5c1533d2294831b4a6..2f95413150901adf0f4b61e246110a459c6cc0b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningModule.kt @@ -12,8 +12,8 @@ abstract class SubmissionDeletionWarningModule { @Binds @IntoMap - @CWAViewModelKey(SubmissionDeletionWarningFragmentViewModel::class) + @CWAViewModelKey(SubmissionDeletionWarningViewModel::class) abstract fun submissionDeletionWarningFragmentVM( - factory: SubmissionDeletionWarningFragmentViewModel.Factory + factory: SubmissionDeletionWarningViewModel.Factory ): CWAViewModelFactory<out CWAViewModel> } 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 new file mode 100644 index 0000000000000000000000000000000000000000..a4b8f62e0f3a87066a0720f4b44c19dafedbe7c8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/deletionwarning/SubmissionDeletionWarningViewModel.kt @@ -0,0 +1,118 @@ +package de.rki.coronawarnapp.ui.submission.deletionwarning + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +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.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.TransactionException +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import timber.log.Timber + +class SubmissionDeletionWarningViewModel @AssistedInject constructor( + private val coronaTestRepository: CoronaTestRepository, + private val submissionRepository: SubmissionRepository, + @Assisted private val coronaTest: CoronaTestQRCode, + @Assisted private val isConsentGiven: Boolean, +) : CWAViewModel() { + + val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() + val showRedeemedTokenWarning = SingleLiveEvent<Unit>() + private val mutableRegistrationState = MutableLiveData(RegistrationState(ApiRequestState.IDLE)) + val registrationState: LiveData<RegistrationState> = mutableRegistrationState + val registrationError = SingleLiveEvent<CwaWebException>() + + fun deleteExistingAndRegisterNewTest() = launch { + coronaTestRepository.removeTest(coronaTest.identifier) + doDeviceRegistration(coronaTest) + } + + data class RegistrationState( + val apiRequestState: ApiRequestState, + val testResult: CoronaTestResult? = null + ) + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) { + try { + mutableRegistrationState.postValue(RegistrationState(ApiRequestState.STARTED)) + val coronaTest = submissionRepository.registerTest(coronaTestQRCode) + if (isConsentGiven) { + submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) + } + checkTestResult(coronaTest.testResult) + mutableRegistrationState.postValue( + RegistrationState( + ApiRequestState.SUCCESS, + coronaTest.testResult + ) + ) + } catch (err: CwaWebException) { + Timber.e(err, "Msg: ${err.message}") + mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + registrationError.postValue(err) + } catch (err: TransactionException) { + Timber.e(err, "Msg: ${err.message}") + if (err.cause is CwaWebException) { + registrationError.postValue(err.cause) + } else { + err.report(ExceptionCategory.INTERNAL) + } + mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + } catch (err: InvalidQRCodeException) { + Timber.e(err, "Msg: ${err.message}") + mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + deregisterTestFromDevice(coronaTestQRCode) + showRedeemedTokenWarning.postValue(Unit) + } catch (err: Exception) { + Timber.e(err, "Msg: ${err.message}") + mutableRegistrationState.postValue(RegistrationState(ApiRequestState.FAILED)) + err.report(ExceptionCategory.INTERNAL) + } + } + + fun onCancelButtonClick() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToConsent) + } + + fun triggerNavigationToSubmissionTestResultAvailableFragment() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToResultAvailableScreen(isConsentGiven)) + } + + fun triggerNavigationToSubmissionTestResultPendingFragment() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToResultPendingScreen(isConsentGiven)) + } + + private fun checkTestResult(testResult: CoronaTestResult) { + if (testResult == CoronaTestResult.PCR_REDEEMED) { + throw InvalidQRCodeException() + } + } + + private fun deregisterTestFromDevice(coronaTest: CoronaTestQRCode) { + launch { + Timber.d("deregisterTestFromDevice()") + + submissionRepository.removeTestFromDevice(type = coronaTest.type) + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) + } + } + + @AssistedFactory + interface Factory : CWAViewModelFactory<SubmissionDeletionWarningViewModel> { + fun create(coronaTest: CoronaTestQRCode, isConsentGiven: Boolean): SubmissionDeletionWarningViewModel + } +} 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 6458a16da1f8c82708ed7bfdc92a3a7e6db23193..89a9a4a17f525b7b027377aae1072e22fdef179d 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 @@ -32,9 +32,11 @@ class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent) } viewModel.routeToScreen.observe2(this) { when (it) { - is SubmissionNavigationEvents.NavigateToQRCodeScan -> doNavigate( - SubmissionConsentFragmentDirections.actionSubmissionConsentFragmentToSubmissionQRCodeScanFragment() - ) + is SubmissionNavigationEvents.NavigateToQRCodeScan -> + doNavigate( + SubmissionConsentFragmentDirections + .actionSubmissionConsentFragmentToSubmissionQRCodeScanFragment(isConsentGiven = true) + ) is SubmissionNavigationEvents.NavigateToDispatcher -> popBackStack() is SubmissionNavigationEvents.NavigateToDataPrivacy -> doNavigate( SubmissionConsentFragmentDirections.actionSubmissionConsentFragmentToInformationPrivacyFragment() 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 459ea67f482258a9e58aa664e74dec82f37587d0..e3387e7dc8426e7b8e5fac3908eabbe9dbb4a81c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt @@ -4,10 +4,8 @@ import androidx.lifecycle.asLiveData import com.google.android.gms.common.api.ApiException import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector 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.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -16,11 +14,10 @@ import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import timber.log.Timber class SubmissionConsentViewModel @AssistedInject constructor( - private val submissionRepository: SubmissionRepository, interoperabilityRepository: InteroperabilityRepository, dispatcherProvider: DispatcherProvider, private val tekHistoryProvider: TEKHistoryProvider, - private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + // private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() @@ -30,8 +27,7 @@ class SubmissionConsentViewModel @AssistedInject constructor( fun onConsentButtonClick() { // TODO Do we have a Test registered at this time? We need to forward the decission with navargs? -// submissionRepository.giveConsentToSubmission(type = CoronaTest.Type.PCR) - analyticsKeySubmissionCollector.reportAdvancedConsentGiven() + // analyticsKeySubmissionCollector.reportAdvancedConsentGiven() launch { try { val preAuthorized = tekHistoryProvider.preAuthorizeExposureKeyHistory() 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 f19296ecba2b0a05766d8ddd40b6ae8d4546ee30..2cd19a7a58c41fefd88049855e6ae57d00b7f95d 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 @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R @@ -26,18 +27,24 @@ import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider -import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import javax.inject.Inject /** * A simple [Fragment] subclass. */ -class SubmissionQRCodeScanFragment : - Fragment(R.layout.fragment_submission_qr_code_scan), - AutoInject { - +class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: SubmissionQRCodeScanViewModel by cwaViewModels { viewModelFactory } + + private val args by navArgs<SubmissionQRCodeScanFragmentArgs>() + + private val viewModel: SubmissionQRCodeScanViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as SubmissionQRCodeScanViewModel.Factory + factory.create(args.isConsentGiven) + } + ) private val binding: FragmentSubmissionQrCodeScanBinding by viewBindingLazy() private var showsPermissionDialog = false @@ -62,6 +69,18 @@ class SubmissionQRCodeScanFragment : submissionQrCodeScanViewfinderView.setCameraPreview(binding.submissionQrCodeScanPreview) } + viewModel.routeToScreen.observe2(this) { + when (it) { + is SubmissionNavigationEvents.NavigateToDeletionWarningFragment -> { + SubmissionQRCodeScanFragmentDirections + .actionSubmissionQRCodeScanFragmentToSubmissionDeletionWarningFragment( + args.isConsentGiven, + it.coronaTestQRCode + ) + } + } + } + viewModel.scanStatusValue.observe2(this) { if (ScanStatus.INVALID == it) { showInvalidScanDialog() @@ -85,16 +104,21 @@ class SubmissionQRCodeScanFragment : ApiRequestState.STARTED -> View.VISIBLE else -> View.GONE } + if (ApiRequestState.SUCCESS == state.apiRequestState) { if (state.testResult == CoronaTestResult.PCR_POSITIVE) { doNavigate( SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultAvailableFragment() + .actionSubmissionQRCodeScanFragmentToSubmissionTestResultAvailableFragment( + isConsentGiven = args.isConsentGiven + ) ) } else { doNavigate( SubmissionQRCodeScanFragmentDirections - .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment() + .actionSubmissionQRCodeScanFragmentToSubmissionTestResultPendingFragment( + isConsentGiven = args.isConsentGiven + ) ) } } 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 ff300dd094c8610d70ccd2ec42bc7231d4114c37..cfbc1dd68b74ddc3a486496d333aca1bfbe47ed5 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 @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan import androidx.annotation.VisibleForTesting import androidx.lifecycle.MutableLiveData +import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor @@ -9,7 +10,6 @@ import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException import de.rki.coronawarnapp.coronatest.server.CoronaTestResult -import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.http.CwaWebException @@ -18,29 +18,41 @@ import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.permission.CameraSettings import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.flow.first import timber.log.Timber class SubmissionQRCodeScanViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, private val submissionRepository: SubmissionRepository, private val cameraSettings: CameraSettings, + @Assisted private val isConsentGiven: Boolean, private val qrCodeValidator: CoronaTestQrCodeValidator -) : CWAViewModel() { +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() val showRedeemedTokenWarning = SingleLiveEvent<Unit>() val scanStatusValue = SingleLiveEvent<ScanStatus>() - fun validateTestGUID(rawResult: String) { + fun validateTestGUID(rawResult: String) = launch { try { val coronaTestQRCode = qrCodeValidator.validate(rawResult) // TODO this needs to be adapted to work for different types QRCodeCensor.lastGUID = coronaTestQRCode.registrationIdentifier scanStatusValue.postValue(ScanStatus.SUCCESS) - doDeviceRegistration(coronaTestQRCode) + + val coronaTest = submissionRepository.testForType(coronaTestQRCode.type).first() + + if (coronaTest != null) { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToDeletionWarningFragment(coronaTestQRCode)) + } else { + doDeviceRegistration(coronaTestQRCode) + } } catch (err: InvalidQRCodeException) { + Timber.e(err, "Failed to validate GUID") scanStatusValue.postValue(ScanStatus.INVALID) } } @@ -54,11 +66,13 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( ) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) = launch { + internal suspend fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) { try { registrationState.postValue(RegistrationState(ApiRequestState.STARTED)) val coronaTest = submissionRepository.registerTest(coronaTestQRCode) - submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) + if (isConsentGiven) { + submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) + } checkTestResult(coronaTest.testResult) registrationState.postValue(RegistrationState(ApiRequestState.SUCCESS, coronaTest.testResult)) } catch (err: CwaWebException) { @@ -73,7 +87,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) } catch (err: InvalidQRCodeException) { registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) - deregisterTestFromDevice() + deregisterTestFromDevice(coronaTestQRCode) showRedeemedTokenWarning.postValue(Unit) } catch (err: Exception) { registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) @@ -87,12 +101,11 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( } } - private fun deregisterTestFromDevice() { + private fun deregisterTestFromDevice(coronaTest: CoronaTestQRCode) { launch { Timber.d("deregisterTestFromDevice()") - submissionRepository.removeTestFromDevice(type = CoronaTest.Type.PCR) - + submissionRepository.removeTestFromDevice(type = coronaTest.type) routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) } } @@ -111,5 +124,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( } @AssistedFactory - interface Factory : SimpleCWAViewModelFactory<SubmissionQRCodeScanViewModel> + interface Factory : CWAViewModelFactory<SubmissionQRCodeScanViewModel> { + fun create(isConsentGiven: Boolean): SubmissionQRCodeScanViewModel + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt index de51d34bff749610e87ed4dd9aa3a9fdc8261440..df8b68eec89dbeee9b5a12d60c4e6bd2025bb9dc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.ui.submission.viewmodel import com.google.android.gms.common.api.ApiException +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode sealed class SubmissionNavigationEvents { object NavigateToContact : SubmissionNavigationEvents() @@ -12,5 +13,8 @@ sealed class SubmissionNavigationEvents { object NavigateToTAN : SubmissionNavigationEvents() object NavigateToConsent : SubmissionNavigationEvents() object NavigateToMainActivity : SubmissionNavigationEvents() + data class NavigateToResultPendingScreen(val consentGiven: Boolean) : SubmissionNavigationEvents() + data class NavigateToResultAvailableScreen(val consentGiven: Boolean) : SubmissionNavigationEvents() + data class NavigateToDeletionWarningFragment(val coronaTestQRCode: CoronaTestQRCode) : SubmissionNavigationEvents() data class ResolvePlayServicesException(val exception: ApiException) : SubmissionNavigationEvents() } diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml index 56a83c008188580c8de15fb748a69241c9a55ba3..27fdf7bca4367c65b6311fa1732c57cf836dd3b2 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_deletion_warning.xml @@ -18,6 +18,19 @@ app:navigationIcon="@drawable/ic_close" app:title="@string/submission_deletion_warning_title" /> + <ProgressBar + android:id="@+id/submission_qr_code_scan_spinner" + style="?android:attr/progressBarStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + <ScrollView android:layout_width="0dp" android:layout_height="0dp" @@ -94,26 +107,12 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_normal" android:layout_marginEnd="@dimen/spacing_normal" - android:layout_marginBottom="8dp" + android:layout_marginBottom="@dimen/spacing_normal" android:text="@string/submission_deletion_warning_continue_button" - app:layout_constraintBottom_toTopOf="@id/cancel_button" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - tools:text="@string/submission_deletion_warning_continue_button" /> - - <Button - android:id="@+id/cancel_button" - style="@style/buttonLight" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_normal" - android:layout_marginBottom="4dp" - android:text="@string/submission_deletion_warning_cancel_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - tools:text="@string/submission_deletion_warning_cancel_button" /> + tools:text="@string/submission_deletion_warning_continue_button" /> </androidx.constraintlayout.widget.ConstraintLayout> 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 5ff1d99cff64e4f55a40b3bcde41772a920a595b..accb01a0fdd40f3de852578e0a33e42533551831 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -289,6 +289,10 @@ android:name="de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment" android:label="SubmissionTestResultPendingFragment" tools:layout="@layout/fragment_submission_test_result_pending"> + <argument + android:name="isConsentGiven" + android:defaultValue="false" + app:argType="boolean" /> <argument android:name="skipInitialTestResultRefresh" android:defaultValue="false" @@ -348,7 +352,12 @@ <fragment android:id="@+id/submissionQRCodeScanFragment" android:name="de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanFragment" - android:label="SubmissionQRCodeScanFragment"> + android:label="SubmissionQRCodeScanFragment" + tools:layout="@layout/fragment_submission_qr_code_scan"> + <argument + android:name="isConsentGiven" + android:defaultValue="false" + app:argType="boolean" /> <action android:id="@+id/action_submissionQRCodeScanFragment_to_submissionDispatcherFragment" app:popUpTo="@id/submissionQRCodeScanFragment" @@ -368,6 +377,9 @@ app:destination="@id/submissionTestResultAvailableFragment" app:popUpTo="@id/mainFragment" app:popUpToInclusive="false" /> + <action + android:id="@+id/action_submissionQRCodeScanFragment_to_submissionDeletionWarningFragment" + app:destination="@id/submissionDeletionWarningFragment" /> </fragment> <fragment android:id="@+id/submissionResultReadyFragment" @@ -480,6 +492,10 @@ android:name="de.rki.coronawarnapp.ui.submission.resultavailable.SubmissionTestResultAvailableFragment" android:label="SubmissionTestResultAvailableFragment" tools:layout="@layout/fragment_submission_test_result_available"> + <argument + android:name="isConsentGiven" + android:defaultValue="false" + app:argType="boolean" /> <action android:id="@+id/action_submissionTestResultAvailableFragment_to_mainFragment" app:destination="@id/mainFragment" @@ -619,6 +635,43 @@ android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalFragment" android:label="DebugLogLegalFragment" tools:layout="@layout/bugreporting_legal_fragment" /> + <fragment + android:id="@+id/submissionDeletionWarningFragment" + android:name="de.rki.coronawarnapp.ui.submission.deletionwarning.SubmissionDeletionWarningFragment" + android:label="SubmissionDeletionWarningFragment" + tools:layout="@layout/fragment_submission_deletion_warning"> + <argument + android:name="isConsentGiven" + android:defaultValue="false" + app:argType="boolean" /> + <argument + android:name="coronaTestQrCode" + app:argType="de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode" /> + <action + android:id="@+id/action_submissionDeletionWarningFragment_to_submissionDispatcherFragment" + app:popUpTo="@id/submissionQRCodeScanFragment" + app:popUpToInclusive="true" /> + <action + android:id="@+id/action_submissionDeletionWarningFragment_to_submissionTestResultPendingFragment" + app:destination="@id/submissionTestResultPendingFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> + <action + android:id="@+id/action_submissionDeletionWarningFragment_to_submissionTestResultAvailableFragment" + app:destination="@id/submissionTestResultAvailableFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> + <action + android:id="@+id/action_submissionDeletionWarningFragment_to_submissionConsentFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" + app:destination="@id/submissionConsentFragment" /> + </fragment> <fragment android:id="@+id/checkInsConsentFragment" 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 8a3ef38c00f8fc402969356ee5b861921c3d73bc..73c696f80716aaa3db533ca76147bc8071b8bea8 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 @@ -43,11 +43,9 @@ class SubmissionConsentViewModelTest { every { submissionRepository.giveConsentToSubmission(any()) } just Runs every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven() } just Runs viewModel = SubmissionConsentViewModel( - submissionRepository, interoperabilityRepository, dispatcherProvider = TestDispatcherProvider(), tekHistoryProvider, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector ) } 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 02ed331db2b6e0bea748693db91616d7b058694b..78b2f8517bf46566e954672c23ff137bf253d5a0 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 @@ -15,11 +15,13 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify -import org.junit.Assert +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider import testhelpers.extensions.InstantExecutorExtension import testhelpers.preferences.mockFlowPreference @@ -30,15 +32,23 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { @MockK lateinit var cameraSettings: CameraSettings @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator + private val coronaTestFlow = MutableStateFlow<CoronaTest?>( + null + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this) + + every { submissionRepository.testForType(any()) } returns coronaTestFlow } private fun createViewModel() = SubmissionQRCodeScanViewModel( + TestDispatcherProvider(), submissionRepository, cameraSettings, - qrCodeValidator + isConsentGiven = true, + qrCodeValidator, ) @Test @@ -64,16 +74,17 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { QRCodeCensor.lastGUID = null viewModel.validateTestGUID(validQrCode) - viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.SUCCESS, it.value) } + viewModel.scanStatusValue.observeForever {} + viewModel.scanStatusValue.value shouldBe ScanStatus.SUCCESS QRCodeCensor.lastGUID = guid // invalid guid viewModel.validateTestGUID(invalidQrCode) - viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.INVALID, it.value) } + viewModel.scanStatusValue.value shouldBe ScanStatus.INVALID } @Test - fun `doDeviceRegistration calls TestResultDataCollector`() { + fun `doDeviceRegistration calls TestResultDataCollector`() = runBlockingTest { val viewModel = createViewModel() val mockResult = mockk<CoronaTestQRCode>().apply { every { registrationIdentifier } returns "guid"