From 57e95dfc1841482662d23e1d7c135d62bde10e03 Mon Sep 17 00:00:00 2001
From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
Date: Fri, 15 Jan 2021 14:36:48 +0100
Subject: [PATCH] Show pending card when offline/ request fails
 (EXPOSUREAPP-4507) (#2074)

* show pending card when offline
* dismiss alert dialog when leaving pending result screen
* handle errors dialog properly when multiple request are triggered through repeated opening of screen or going to background
---
 .../ui/homecards/SubmissionStateProvider.kt   | 12 +--
 .../SubmissionTestResultPendingFragment.kt    | 88 +++++++++++--------
 .../SubmissionTestResultPendingViewModel.kt   | 15 ++--
 .../util/ui/LiveDataExtensions.kt             | 11 ++-
 4 files changed, 72 insertions(+), 54 deletions(-)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt
index cbb701de1..474e84b1b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt
@@ -68,7 +68,7 @@ class SubmissionStateProvider @Inject constructor(
 
         fun isFetching(): Boolean =
             isDeviceRegistered && when (deviceUiState) {
-                is NetworkRequestWrapper.RequestFailed -> deviceUiState.error is CwaServerError
+                is NetworkRequestWrapper.RequestFailed -> false
                 is NetworkRequestWrapper.RequestStarted -> true
                 is NetworkRequestWrapper.RequestIdle -> true
                 else -> false
@@ -112,11 +112,13 @@ class SubmissionStateProvider @Inject constructor(
             }
 
         fun isPending(): Boolean =
-            deviceUiState.withSuccess(false) {
-                when (it) {
-                    DeviceUIState.PAIRED_ERROR, DeviceUIState.PAIRED_NO_RESULT -> true
-                    else -> false
+            when (deviceUiState) {
+                is NetworkRequestWrapper.RequestFailed -> true
+                is NetworkRequestWrapper.RequestSuccessful -> {
+                    deviceUiState.data == DeviceUIState.PAIRED_ERROR ||
+                        deviceUiState.data == DeviceUIState.PAIRED_NO_RESULT
                 }
+                else -> false
             }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt
index 2a2949b43..38721c989 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt
@@ -1,9 +1,9 @@
 package de.rki.coronawarnapp.ui.submission.testresult.pending
 
-import android.app.AlertDialog
 import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
+import androidx.appcompat.app.AlertDialog
 import androidx.fragment.app.Fragment
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultPendingBinding
@@ -12,11 +12,11 @@ import de.rki.coronawarnapp.exception.http.CwaServerError
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat
 import de.rki.coronawarnapp.util.DialogHelper
-import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withFailure
-import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.observeOnce
 import de.rki.coronawarnapp.util.ui.popBackStack
 import de.rki.coronawarnapp.util.ui.setInvisible
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
@@ -33,6 +33,8 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
 
     private var skipInitialTestResultRefresh = false
 
+    private var errorDialog: AlertDialog? = null
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
@@ -41,19 +43,10 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
         }
 
         pendingViewModel.testState.observe2(this) { result ->
-            result.deviceUiState.withFailure {
-                if (it is CwaWebException) {
-                    DialogHelper.showDialog(buildErrorDialog(it))
-                }
-            }
-
-            val hasResult = result.deviceUiState.withSuccess(false) { true }
-
+            val hasResult = result.deviceUiState is NetworkRequestWrapper.RequestSuccessful
             binding.apply {
                 submissionTestResultSection.setTestResultSection(result.deviceUiState, result.testResultReceivedDate)
-
                 submissionTestResultSpinner.setInvisible(hasResult)
-
                 submissionTestResultContent.setInvisible(!hasResult)
                 buttonContainer.setInvisible(!hasResult)
             }
@@ -98,8 +91,16 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
         super.onResume()
         binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
         pendingViewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh)
-
         skipInitialTestResultRefresh = false
+        pendingViewModel.cwaWebExceptionLiveData.observeOnce(this.viewLifecycleOwner) { exception ->
+            handleError(exception)
+        }
+    }
+
+    override fun onPause() {
+        pendingViewModel.cwaWebExceptionLiveData.removeObservers(this.viewLifecycleOwner)
+        errorDialog?.dismiss()
+        super.onPause()
     }
 
     private fun removeTestAfterConfirmation() {
@@ -118,33 +119,42 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
         }
     }
 
+    private fun handleError(exception: CwaWebException) {
+        errorDialog = when (exception) {
+            is CwaClientError, is CwaServerError -> {
+                DialogHelper.showDialog(buildErrorDialog(exception))
+            }
+            else -> {
+                DialogHelper.showDialog(genericErrorDialog)
+            }
+        }
+    }
+
     private fun navigateToMainScreen() {
         popBackStack()
     }
 
-    private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance {
-        return when (exception) {
-            is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance(
-                requireActivity(),
-                R.string.submission_error_dialog_web_generic_error_title,
-                getString(
-                    R.string.submission_error_dialog_web_generic_network_error_body,
-                    exception.statusCode
-                ),
-                R.string.submission_error_dialog_web_generic_error_button_positive,
-                null,
-                true,
-                ::navigateToMainScreen
-            )
-            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,
-                ::navigateToMainScreen
-            )
-        }
-    }
+    private fun buildErrorDialog(exception: CwaWebException) = DialogHelper.DialogInstance(
+        requireActivity(),
+        R.string.submission_error_dialog_web_generic_error_title,
+        getString(
+            R.string.submission_error_dialog_web_generic_network_error_body,
+            exception.statusCode
+        ),
+        R.string.submission_error_dialog_web_generic_error_button_positive,
+        null,
+        true,
+        ::navigateToMainScreen
+    )
+
+    private val genericErrorDialog: DialogHelper.DialogInstance
+        get() = 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,
+            ::navigateToMainScreen
+        )
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt
index 6149c922a..b77a82130 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt
@@ -1,9 +1,9 @@
 package de.rki.coronawarnapp.ui.submission.testresult.pending
 
-import androidx.lifecycle.LiveData
 import androidx.lifecycle.asLiveData
 import androidx.navigation.NavDirections
 import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.notification.TestResultNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
@@ -16,7 +16,9 @@ import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterIsInstance
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
@@ -57,7 +59,8 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
             testResultReceivedDate = resultDate
         )
     }
-    val testState: LiveData<TestResultUIState> = testResultFlow
+
+    val testState = testResultFlow
         .onEach { testResultUIState ->
             testResultUIState.deviceUiState.withSuccess { deviceState ->
                 when (deviceState) {
@@ -85,9 +88,10 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
         }
         .asLiveData(context = dispatcherProvider.Default)
 
-    fun onTestOpened() {
-        submissionRepository.setViewedTestResult()
-    }
+    val cwaWebExceptionLiveData = submissionRepository.deviceUIStateFlow
+        .filterIsInstance<NetworkRequestWrapper.RequestFailed<DeviceUIState, CwaWebException>>()
+        .map { it.error }
+        .asLiveData()
 
     fun observeTestResultToSchedulePositiveTestResultReminder() = launch {
         submissionRepository.deviceUIStateFlow
@@ -103,7 +107,6 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
         Timber.d("deregisterTestFromDevice()")
         launch {
             submissionRepository.removeTestFromDevice()
-
             routeToScreen.postValue(null)
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt
index f31e15bfc..e1d652cd0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LiveDataExtensions.kt
@@ -4,15 +4,18 @@ import androidx.fragment.app.Fragment
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.Observer
+import androidx.lifecycle.observe
 
 fun <T> LiveData<T>.observe2(fragment: Fragment, callback: (T) -> Unit) {
-    observe(fragment.viewLifecycleOwner, { callback.invoke(it) })
+    observe(fragment.viewLifecycleOwner) {
+        callback.invoke(it)
+    }
 }
 
-fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner? = null, observer: Observer<T>) {
+fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner? = null, onValueChanged: (t: T) -> Unit) {
     val internalObserver = object : Observer<T> {
-        override fun onChanged(t: T?) {
-            observer.onChanged(t)
+        override fun onChanged(t: T) {
+            onValueChanged(t)
             removeObserver(this)
         }
     }
-- 
GitLab