diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml index 58d6e0bd796160ee52a7719df0972f20daabb9e5..2612e39508725c63da6d9b213cc75a3b3f6c3d47 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/nav_graph.xml @@ -224,6 +224,10 @@ android:name="de.rki.coronawarnapp.ui.submission.SubmissionTestResultFragment" android:label="fragment_submission_result" tools:layout="@layout/fragment_submission_test_result"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="false" + app:argType="boolean" /> <action android:id="@+id/action_submissionResultFragment_to_mainFragment" app:destination="@id/mainFragment" @@ -239,16 +243,16 @@ android:name="de.rki.coronawarnapp.ui.submission.SubmissionTanFragment" android:label="fragment_submission_tan" tools:layout="@layout/fragment_submission_tan"> - <action - android:id="@+id/action_submissionTanFragment_to_submissionDispatcherFragment" - app:destination="@id/submissionDispatcherFragment" - app:popUpTo="@id/submissionDispatcherFragment" - app:popUpToInclusive="true" /> <action android:id="@+id/action_submissionTanFragment_to_submissionResultFragment" app:destination="@id/submissionResultFragment" app:popUpTo="@id/submissionResultFragment" - app:popUpToInclusive="true" /> + app:popUpToInclusive="true"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> </fragment> <fragment @@ -284,7 +288,12 @@ <action android:id="@+id/action_submissionQRCodeScanFragment_to_submissionResultFragment" app:destination="@id/submissionResultFragment" - app:popUpTo="@id/submissionResultFragment" /> + app:popUpTo="@id/submissionResultFragment"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> </fragment> <fragment android:id="@+id/submissionDoneFragment" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/Playbook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/Playbook.kt index 66ab1f957b9edcb3af994ca120a3aba2276ecc19..5d37af5df193ba0d77768999d4a90205957bb71c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/Playbook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/Playbook.kt @@ -14,7 +14,7 @@ interface Playbook { suspend fun initialRegistration( key: String, keyType: KeyType - ): String /* registration token */ + ): Pair<String, TestResult> /* registration token & test result*/ suspend fun testResult( registrationToken: String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/PlaybookImpl.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/PlaybookImpl.kt index 3012a11394711c4d974fc4613431790e217c13de..e5b3d78caed3af5e366be0dfa662c3150642897e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/PlaybookImpl.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/playbook/PlaybookImpl.kt @@ -20,22 +20,36 @@ class PlaybookImpl( private val uid = UUID.randomUUID().toString() private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO) - override suspend fun initialRegistration(key: String, keyType: KeyType): String { + override suspend fun initialRegistration( + key: String, + keyType: KeyType + ): Pair<String, TestResult> { Timber.i("[$uid] New Initial Registration Playbook") // real registration - val (registrationToken, exception) = + val (registrationToken, registrationException) = executeCapturingExceptions { webRequestBuilder.asyncGetRegistrationToken(key, keyType) } - // fake verification - ignoreExceptions { webRequestBuilder.asyncFakeVerification() } + // if the registration succeeded continue with the real test result retrieval + // if it failed, execute a dummy request to satisfy the required playbook pattern + val (testResult, testResultException) = if (registrationToken != null) { + executeCapturingExceptions { webRequestBuilder.asyncGetTestResult(registrationToken) } + } else { + ignoreExceptions { webRequestBuilder.asyncFakeVerification() } + null to null + } // fake submission ignoreExceptions { webRequestBuilder.asyncFakeSubmission() } coroutineScope.launch { followUpPlaybooks() } - return registrationToken ?: propagateException(exception) + // if registration and test result retrieval succeeded, return the result + if (registrationToken != null && testResult != null) + return registrationToken to TestResult.fromInt(testResult) + + // else propagate the exception of either the first or the second step + propagateException(registrationException, testResultException) } override suspend fun testResult(registrationToken: String): TestResult { @@ -137,7 +151,7 @@ class PlaybookImpl( } } - private fun propagateException(exception: Exception?): Nothing { - throw exception ?: IllegalStateException() + private fun propagateException(vararg exceptions: Exception?): Nothing { + throw exceptions.filterNotNull().firstOrNull() ?: IllegalStateException() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt index 10dc0ec346f03304eb595d526ccbc11c328d0402..defce20ed3566931a323c6756791038c801482da 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt @@ -7,6 +7,7 @@ import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.http.playbook.BackgroundNoise import de.rki.coronawarnapp.http.playbook.PlaybookImpl import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.worker.BackgroundWorkScheduler @@ -27,7 +28,7 @@ object SubmissionService { } private suspend fun asyncRegisterDeviceViaGUID(guid: String) { - val registrationToken = + val (registrationToken, testResult) = PlaybookImpl(WebRequestBuilder.getInstance()).initialRegistration( guid, KeyType.GUID @@ -35,10 +36,11 @@ object SubmissionService { LocalData.registrationToken(registrationToken) deleteTestGUID() + SubmissionRepository.updateTestResult(testResult) } private suspend fun asyncRegisterDeviceViaTAN(tan: String) { - val registrationToken = + val (registrationToken, testResult) = PlaybookImpl(WebRequestBuilder.getInstance()).initialRegistration( tan, KeyType.TELETAN @@ -46,6 +48,7 @@ object SubmissionService { LocalData.registrationToken(registrationToken) deleteTeleTAN() + SubmissionRepository.updateTestResult(testResult) } suspend fun asyncSubmitExposureKeys(keys: List<TemporaryExposureKey>) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt index eb969441593d73076b13fb71e42a27490959829e..e241304e6a6a5bbeaca97ae517faf5ed3d4ae7a7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt @@ -13,8 +13,9 @@ object SubmissionRepository { val testResultReceivedDate = MutableLiveData(Date()) val deviceUIState = MutableLiveData(DeviceUIState.UNPAIRED) + private val testResult = MutableLiveData<TestResult?>(null) - suspend fun refreshUIState() { + suspend fun refreshUIState(refreshTestResult: Boolean) { var uiState = DeviceUIState.UNPAIRED if (LocalData.numberOfSuccessfulSubmissions() == 1) { @@ -25,7 +26,10 @@ object SubmissionRepository { LocalData.isAllowedToSubmitDiagnosisKeys() == true -> { DeviceUIState.PAIRED_POSITIVE } - else -> fetchTestResult() + refreshTestResult -> fetchTestResult() + else -> { + deriveUiState(testResult.value) + } } } } @@ -35,32 +39,42 @@ object SubmissionRepository { private suspend fun fetchTestResult(): DeviceUIState { try { val testResult = SubmissionService.asyncRequestTestResult() - if (testResult == TestResult.POSITIVE) { - LocalData.isAllowedToSubmitDiagnosisKeys(true) - } + updateTestResult(testResult) + return deriveUiState(testResult) + } catch (err: NoRegistrationTokenSetException) { + return DeviceUIState.UNPAIRED + } + } - val initialTestResultReceivedTimestamp = LocalData.initialTestResultReceivedTimestamp() + fun updateTestResult(testResult: TestResult) { + this.testResult.value = testResult - if (initialTestResultReceivedTimestamp == null) { - val currentTime = System.currentTimeMillis() - LocalData.initialTestResultReceivedTimestamp(currentTime) - testResultReceivedDate.value = Date(currentTime) - if (testResult == TestResult.PENDING) { - BackgroundWorkScheduler.startWorkScheduler() - } - } else { - testResultReceivedDate.value = Date(initialTestResultReceivedTimestamp) - } + if (testResult == TestResult.POSITIVE) { + LocalData.isAllowedToSubmitDiagnosisKeys(true) + } + + val initialTestResultReceivedTimestamp = LocalData.initialTestResultReceivedTimestamp() - return when (testResult) { - TestResult.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE - TestResult.POSITIVE -> DeviceUIState.PAIRED_POSITIVE - TestResult.PENDING -> DeviceUIState.PAIRED_NO_RESULT - TestResult.INVALID -> DeviceUIState.PAIRED_ERROR - TestResult.REDEEMED -> DeviceUIState.PAIRED_REDEEMED + if (initialTestResultReceivedTimestamp == null) { + val currentTime = System.currentTimeMillis() + LocalData.initialTestResultReceivedTimestamp(currentTime) + testResultReceivedDate.value = Date(currentTime) + if (testResult == TestResult.PENDING) { + BackgroundWorkScheduler.startWorkScheduler() } - } catch (err: NoRegistrationTokenSetException) { - return DeviceUIState.UNPAIRED + } else { + testResultReceivedDate.value = Date(initialTestResultReceivedTimestamp) + } + } + + private fun deriveUiState(testResult: TestResult?): DeviceUIState { + return when (testResult) { + TestResult.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + TestResult.POSITIVE -> DeviceUIState.PAIRED_POSITIVE + TestResult.PENDING -> DeviceUIState.PAIRED_NO_RESULT + TestResult.REDEEMED -> DeviceUIState.PAIRED_REDEEMED + TestResult.INVALID -> DeviceUIState.PAIRED_ERROR + null -> DeviceUIState.UNPAIRED } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt index 03aecc8b20e2803224d0dcdb03772e76f2a3c42d..34e9cbc9899d8693f0f1d5b4f1b28deedef8e4cc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragment.kt @@ -37,6 +37,8 @@ class SubmissionTestResultFragment : Fragment() { private var _binding: FragmentSubmissionTestResultBinding? = null private val binding: FragmentSubmissionTestResultBinding get() = _binding!! + private var skipInitialTestResultRefresh = false + // Overrides default back behaviour private val backCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) { @@ -59,6 +61,10 @@ class SubmissionTestResultFragment : Fragment() { // registers callback when the os level back is pressed requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) // Inflate the layout for this fragment + + skipInitialTestResultRefresh = + arguments?.getBoolean("skipInitialTestResultRefresh") ?: false + return binding.root } @@ -139,8 +145,10 @@ class SubmissionTestResultFragment : Fragment() { override fun onResume() { super.onResume() binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - submissionViewModel.refreshDeviceUIState() + submissionViewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh) tracingViewModel.refreshIsTracingEnabled() + + skipInitialTestResultRefresh = false } private fun setButtonOnClickListener() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt index 1964d9169bbf3fd3f9448ce09bd2992c5972155a..2fced7642eda4289f66510d0df15fe43d6ed74e0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt @@ -91,9 +91,9 @@ class SubmissionViewModel : ViewModel() { } } - fun refreshDeviceUIState() = + fun refreshDeviceUIState(refreshTestResult: Boolean = true) = executeRequestWithState( - SubmissionRepository::refreshUIState, + { SubmissionRepository.refreshUIState(refreshTestResult) }, _uiStateState, _uiStateError ) 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 13ffd42465f2d8521330216dffac087d8159dbf8..decb93bee6edd861128975c7b75e858266eedfbc 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -215,6 +215,10 @@ android:name="de.rki.coronawarnapp.ui.submission.SubmissionTestResultFragment" android:label="fragment_submission_result" tools:layout="@layout/fragment_submission_test_result"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="false" + app:argType="boolean" /> <action android:id="@+id/action_submissionResultFragment_to_mainFragment" app:destination="@id/mainFragment" @@ -234,7 +238,12 @@ android:id="@+id/action_submissionTanFragment_to_submissionResultFragment" app:destination="@id/submissionResultFragment" app:popUpTo="@id/submissionResultFragment" - app:popUpToInclusive="true" /> + app:popUpToInclusive="true"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> </fragment> <fragment @@ -270,7 +279,12 @@ <action android:id="@+id/action_submissionQRCodeScanFragment_to_submissionResultFragment" app:destination="@id/submissionResultFragment" - app:popUpTo="@id/submissionResultFragment" /> + app:popUpTo="@id/submissionResultFragment"> + <argument + android:name="skipInitialTestResultRefresh" + android:defaultValue="true" + app:argType="boolean" /> + </action> </fragment> <fragment android:id="@+id/submissionDoneFragment" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/PlaybookImplTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/PlaybookImplTest.kt index 60cf0a738269c3df3724c686ea7aba3c42f3c918..508ed2d00de963e84cf299f0a07687d326c469cb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/PlaybookImplTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/PlaybookImplTest.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.http.playbook import de.rki.coronawarnapp.exception.http.InternalServerErrorException import de.rki.coronawarnapp.service.submission.KeyType +import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.newWebRequestBuilder import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.MockResponse @@ -84,18 +85,20 @@ class PlaybookImplTest { server.start() val expectedRegistrationToken = "token" + val expectedTestResult = TestResult.PENDING server.enqueue(MockResponse().setBody("""{"registrationToken":"$expectedRegistrationToken"}""")) - server.enqueue(MockResponse().setResponseCode(500)) + server.enqueue(MockResponse().setBody("""{"testResult":${expectedTestResult.value}}""")) server.enqueue(MockResponse().setResponseCode(500)) - val registrationToken = PlaybookImpl(server.newWebRequestBuilder()) + val (registrationToken, testResult) = PlaybookImpl(server.newWebRequestBuilder()) .initialRegistration("key", KeyType.GUID) assertThat(registrationToken, equalTo(expectedRegistrationToken)) + assertThat(testResult, equalTo(expectedTestResult)) } @Test - fun hasRequestPatternWhenRealRequestFails_initialRegistration(): Unit = runBlocking { + fun hasRequestPatternWhenRealRequestFails_initialRegistrationFirst(): Unit = runBlocking { val server = MockWebServer() server.start() @@ -115,6 +118,26 @@ class PlaybookImplTest { assertRequestPattern(server) } + @Test + fun hasRequestPatternWhenRealRequestFails_initialRegistrationSecond(): Unit = runBlocking { + val server = MockWebServer() + server.start() + + server.enqueue(MockResponse().setBody("""{"registrationToken":"response"}""")) + server.enqueue(MockResponse().setResponseCode(500)) + server.enqueue(MockResponse().setBody("{}")) + + try { + PlaybookImpl(server.newWebRequestBuilder()) + .initialRegistration("9A3B578UMG", KeyType.TELETAN) + fail("exception propagation expected") + } catch (e: InternalServerErrorException) { + } + + // ensure request order is 2x verification and 1x submission + assertRequestPattern(server) + } + @Test fun hasRequestPatternWhenRealRequestFails_testResult(): Unit = runBlocking { val server = MockWebServer() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt index 9d1f958c6895c1dcac2e4984294c9e00e769e83a..fef63707083f8e07ed413d47824cb7032a7b8835 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.http.WebRequestBuilder import de.rki.coronawarnapp.http.playbook.BackgroundNoise import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.transaction.SubmitDiagnosisKeysTransaction import de.rki.coronawarnapp.util.formatter.TestResult import io.mockk.MockKAnnotations @@ -24,6 +25,7 @@ import org.junit.Test class SubmissionServiceTest { private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" private val registrationToken = "asdjnskjfdniuewbheboqudnsojdff" + private val testResult = TestResult.PENDING @MockK private lateinit var webRequestBuilder: WebRequestBuilder @@ -43,6 +45,9 @@ class SubmissionServiceTest { mockkObject(SubmitDiagnosisKeysTransaction) mockkObject(LocalData) + mockkObject(SubmissionRepository) + every { SubmissionRepository.updateTestResult(any()) } just Runs + every { LocalData.teletan() } returns null every { LocalData.testGUID() } returns null every { LocalData.registrationToken() } returns null @@ -66,6 +71,8 @@ class SubmissionServiceTest { coEvery { webRequestBuilder.asyncGetRegistrationToken(any(), KeyType.GUID) } returns registrationToken + coEvery { webRequestBuilder.asyncGetTestResult(registrationToken) } returns testResult.value + every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { @@ -77,6 +84,7 @@ class SubmissionServiceTest { LocalData.devicePairingSuccessfulTimestamp(any()) LocalData.testGUID(null) backgroundNoise.scheduleDummyPattern() + SubmissionRepository.updateTestResult(testResult) } } @@ -91,6 +99,8 @@ class SubmissionServiceTest { coEvery { webRequestBuilder.asyncGetRegistrationToken(any(), KeyType.TELETAN) } returns registrationToken + coEvery { webRequestBuilder.asyncGetTestResult(registrationToken) } returns testResult.value + every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { @@ -102,6 +112,7 @@ class SubmissionServiceTest { LocalData.devicePairingSuccessfulTimestamp(any()) LocalData.teletan(null) backgroundNoise.scheduleDummyPattern() + SubmissionRepository.updateTestResult(testResult) } }