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 cbb701de15b06a0af099a671f6b967659fc2f5fd..474e84b1bf79bf3bda4d15f3bf3224644c40e8c7 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 2a2949b43996845f734ae09a5fc95c9b1facc289..38721c989499cdeda4c57cd2380797df07d6a146 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 6149c922a10258b789653dfe864f22178685d770..b77a82130c1a636d56fb819b735a7a8113b5f8df 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/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt index 562cfe70cf01b4bcf8fffa47de66ff1b0dd39bcd..3365de8462647f20759cd2856b946090ccac19e2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.util import android.content.Context import android.net.wifi.WifiManager import android.os.PowerManager -import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.storage.LocalData @@ -12,6 +12,7 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.device.BackgroundModeStatus import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.di.ProcessLifecycle import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -25,7 +26,8 @@ import javax.inject.Singleton class WatchdogService @Inject constructor( @AppContext private val context: Context, private val taskController: TaskController, - private val backgroundModeStatus: BackgroundModeStatus + private val backgroundModeStatus: BackgroundModeStatus, + @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner ) { private val powerManager by lazy { @@ -44,7 +46,7 @@ class WatchdogService @Inject constructor( } Timber.tag(TAG).v("Acquiring wakelocks for watchdog routine.") - ProcessLifecycleOwner.get().lifecycleScope.launch { + processLifecycleOwner.lifecycleScope.launch { // A wakelock as the OS does not handle this for us like in the background job execution val wakeLock = createWakeLock() // A wifi lock to wake up the wifi connection in case the device is dozing diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/ForegroundState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/ForegroundState.kt index 2980ccbc29947ac6b79fb6a44b7996e515099c1c..1babb17239d19f618d42c9a1253efc1a24616137 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/ForegroundState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/ForegroundState.kt @@ -2,9 +2,9 @@ package de.rki.coronawarnapp.util.device import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.ProcessLifecycleOwner -import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.util.di.ProcessLifecycle import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.onCompletion @@ -15,7 +15,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class ForegroundState @Inject constructor() { +class ForegroundState @Inject constructor( + @ProcessLifecycle val processLifecycleOwner: LifecycleOwner +) { val isInForeground: Flow<Boolean> by lazy { MutableStateFlow(false).apply { @@ -23,19 +25,19 @@ class ForegroundState @Inject constructor() { @Suppress("unused") @OnLifecycleEvent(Lifecycle.Event.ON_START) fun onAppForegrounded() { - CoronaWarnApplication.isAppInForeground = true Timber.v("App is in the foreground") + tryEmit(true) } @Suppress("unused") @OnLifecycleEvent(Lifecycle.Event.ON_STOP) fun onAppBackgrounded() { - CoronaWarnApplication.isAppInForeground = false Timber.v("App is in the background") + tryEmit(false) } } - val processLifecycle = ProcessLifecycleOwner.get().lifecycle + val processLifecycle = processLifecycleOwner.lifecycle processLifecycle.addObserver(foregroundStateUpdater) } .onStart { Timber.v("isInForeground FLOW start") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt index 245e67fe97c08c7a37764e64f12f44e9bdbd4122..3d3cda2e2a3d9f3367d6a69c28b02a7823440477 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt @@ -8,6 +8,8 @@ import android.content.Context import android.content.SharedPreferences import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner import androidx.navigation.NavDeepLinkBuilder import androidx.work.WorkManager import dagger.Module @@ -63,4 +65,9 @@ class AndroidModule { @Provides @Singleton fun activityManager(@AppContext context: Context): ActivityManager = context.getSystemService()!! + + @Provides + @Singleton + @ProcessLifecycle + fun procressLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ProcessLifecycle.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ProcessLifecycle.kt new file mode 100644 index 0000000000000000000000000000000000000000..ada39e1d77afd61f9990e55bcf4acdb402eb1de5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ProcessLifecycle.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.util.di + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class ProcessLifecycle 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 f31e15bfcf090fb98d8aa99266a90c7463ebb57d..e1d652cd060853b2ced95dcce8c5be835e7b7347 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) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/ForegroundStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/ForegroundStateTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..57ef31fc9325943dba13c67b8875303be582671a --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/device/ForegroundStateTest.kt @@ -0,0 +1,56 @@ +package de.rki.coronawarnapp.util.device + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.coroutines.test + +class ForegroundStateTest : BaseTest() { + + @MockK lateinit var lifecycleOwner: LifecycleOwner + lateinit var lifecycle: LifecycleRegistry + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + lifecycle = LifecycleRegistry(lifecycleOwner) + every { lifecycleOwner.lifecycle } returns lifecycle + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + fun createInstance() = ForegroundState( + processLifecycleOwner = lifecycleOwner + ) + + @Test + fun `test emissions`() = runBlockingTest { + val instance = createInstance() + + val testCollector = instance.isInForeground.test(startOnScope = this) + + testCollector.latestValue shouldBe false + + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) + testCollector.latestValue shouldBe true + + lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + testCollector.latestValue shouldBe false + + testCollector.cancel() + advanceUntilIdle() + } +}