Skip to content
Snippets Groups Projects
Commit 38c1a447 authored by Mohamed Metwalli's avatar Mohamed Metwalli
Browse files

Merge branch 'release/1.11.x' into feature/4559-contact-journal-screenshot

parents f1626713 a36cf7ee
No related branches found
No related tags found
No related merge requests found
Showing
with 156 additions and 63 deletions
...@@ -68,7 +68,7 @@ class SubmissionStateProvider @Inject constructor( ...@@ -68,7 +68,7 @@ class SubmissionStateProvider @Inject constructor(
fun isFetching(): Boolean = fun isFetching(): Boolean =
isDeviceRegistered && when (deviceUiState) { isDeviceRegistered && when (deviceUiState) {
is NetworkRequestWrapper.RequestFailed -> deviceUiState.error is CwaServerError is NetworkRequestWrapper.RequestFailed -> false
is NetworkRequestWrapper.RequestStarted -> true is NetworkRequestWrapper.RequestStarted -> true
is NetworkRequestWrapper.RequestIdle -> true is NetworkRequestWrapper.RequestIdle -> true
else -> false else -> false
...@@ -112,11 +112,13 @@ class SubmissionStateProvider @Inject constructor( ...@@ -112,11 +112,13 @@ class SubmissionStateProvider @Inject constructor(
} }
fun isPending(): Boolean = fun isPending(): Boolean =
deviceUiState.withSuccess(false) { when (deviceUiState) {
when (it) { is NetworkRequestWrapper.RequestFailed -> true
DeviceUIState.PAIRED_ERROR, DeviceUIState.PAIRED_NO_RESULT -> true is NetworkRequestWrapper.RequestSuccessful -> {
else -> false deviceUiState.data == DeviceUIState.PAIRED_ERROR ||
deviceUiState.data == DeviceUIState.PAIRED_NO_RESULT
} }
else -> false
} }
} }
} }
package de.rki.coronawarnapp.ui.submission.testresult.pending package de.rki.coronawarnapp.ui.submission.testresult.pending
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import de.rki.coronawarnapp.R import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultPendingBinding import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultPendingBinding
...@@ -12,11 +12,11 @@ import de.rki.coronawarnapp.exception.http.CwaServerError ...@@ -12,11 +12,11 @@ import de.rki.coronawarnapp.exception.http.CwaServerError
import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.http.CwaWebException
import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat
import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.DialogHelper
import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withFailure import de.rki.coronawarnapp.util.NetworkRequestWrapper
import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.di.AutoInject
import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.doNavigate
import de.rki.coronawarnapp.util.ui.observe2 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.popBackStack
import de.rki.coronawarnapp.util.ui.setInvisible import de.rki.coronawarnapp.util.ui.setInvisible
import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.ui.viewBindingLazy
...@@ -33,6 +33,8 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio ...@@ -33,6 +33,8 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
private var skipInitialTestResultRefresh = false private var skipInitialTestResultRefresh = false
private var errorDialog: AlertDialog? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
...@@ -41,19 +43,10 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio ...@@ -41,19 +43,10 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
} }
pendingViewModel.testState.observe2(this) { result -> pendingViewModel.testState.observe2(this) { result ->
result.deviceUiState.withFailure { val hasResult = result.deviceUiState is NetworkRequestWrapper.RequestSuccessful
if (it is CwaWebException) {
DialogHelper.showDialog(buildErrorDialog(it))
}
}
val hasResult = result.deviceUiState.withSuccess(false) { true }
binding.apply { binding.apply {
submissionTestResultSection.setTestResultSection(result.deviceUiState, result.testResultReceivedDate) submissionTestResultSection.setTestResultSection(result.deviceUiState, result.testResultReceivedDate)
submissionTestResultSpinner.setInvisible(hasResult) submissionTestResultSpinner.setInvisible(hasResult)
submissionTestResultContent.setInvisible(!hasResult) submissionTestResultContent.setInvisible(!hasResult)
buttonContainer.setInvisible(!hasResult) buttonContainer.setInvisible(!hasResult)
} }
...@@ -98,8 +91,16 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio ...@@ -98,8 +91,16 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio
super.onResume() super.onResume()
binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
pendingViewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh) pendingViewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh)
skipInitialTestResultRefresh = false 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() { private fun removeTestAfterConfirmation() {
...@@ -118,33 +119,42 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio ...@@ -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() { private fun navigateToMainScreen() {
popBackStack() popBackStack()
} }
private fun buildErrorDialog(exception: CwaWebException): DialogHelper.DialogInstance { private fun buildErrorDialog(exception: CwaWebException) = DialogHelper.DialogInstance(
return when (exception) { requireActivity(),
is CwaClientError, is CwaServerError -> DialogHelper.DialogInstance( R.string.submission_error_dialog_web_generic_error_title,
requireActivity(), getString(
R.string.submission_error_dialog_web_generic_error_title, R.string.submission_error_dialog_web_generic_network_error_body,
getString( exception.statusCode
R.string.submission_error_dialog_web_generic_network_error_body, ),
exception.statusCode R.string.submission_error_dialog_web_generic_error_button_positive,
), null,
R.string.submission_error_dialog_web_generic_error_button_positive, true,
null, ::navigateToMainScreen
true, )
::navigateToMainScreen
) private val genericErrorDialog: DialogHelper.DialogInstance
else -> DialogHelper.DialogInstance( get() = DialogHelper.DialogInstance(
requireActivity(), requireActivity(),
R.string.submission_error_dialog_web_generic_error_title, 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_body,
R.string.submission_error_dialog_web_generic_error_button_positive, R.string.submission_error_dialog_web_generic_error_button_positive,
null, null,
true, true,
::navigateToMainScreen ::navigateToMainScreen
) )
}
}
} }
package de.rki.coronawarnapp.ui.submission.testresult.pending package de.rki.coronawarnapp.ui.submission.testresult.pending
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.exception.http.CwaWebException
import de.rki.coronawarnapp.notification.TestResultNotificationService import de.rki.coronawarnapp.notification.TestResultNotificationService
import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.SubmissionRepository
import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
...@@ -16,7 +16,9 @@ import de.rki.coronawarnapp.util.ui.SingleLiveEvent ...@@ -16,7 +16,9 @@ import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
...@@ -57,7 +59,8 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( ...@@ -57,7 +59,8 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
testResultReceivedDate = resultDate testResultReceivedDate = resultDate
) )
} }
val testState: LiveData<TestResultUIState> = testResultFlow
val testState = testResultFlow
.onEach { testResultUIState -> .onEach { testResultUIState ->
testResultUIState.deviceUiState.withSuccess { deviceState -> testResultUIState.deviceUiState.withSuccess { deviceState ->
when (deviceState) { when (deviceState) {
...@@ -85,9 +88,10 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( ...@@ -85,9 +88,10 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
} }
.asLiveData(context = dispatcherProvider.Default) .asLiveData(context = dispatcherProvider.Default)
fun onTestOpened() { val cwaWebExceptionLiveData = submissionRepository.deviceUIStateFlow
submissionRepository.setViewedTestResult() .filterIsInstance<NetworkRequestWrapper.RequestFailed<DeviceUIState, CwaWebException>>()
} .map { it.error }
.asLiveData()
fun observeTestResultToSchedulePositiveTestResultReminder() = launch { fun observeTestResultToSchedulePositiveTestResultReminder() = launch {
submissionRepository.deviceUIStateFlow submissionRepository.deviceUIStateFlow
...@@ -103,7 +107,6 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( ...@@ -103,7 +107,6 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor(
Timber.d("deregisterTestFromDevice()") Timber.d("deregisterTestFromDevice()")
launch { launch {
submissionRepository.removeTestFromDevice() submissionRepository.removeTestFromDevice()
routeToScreen.postValue(null) routeToScreen.postValue(null)
} }
} }
......
...@@ -3,7 +3,7 @@ package de.rki.coronawarnapp.util ...@@ -3,7 +3,7 @@ package de.rki.coronawarnapp.util
import android.content.Context import android.content.Context
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.PowerManager import android.os.PowerManager
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
...@@ -12,6 +12,7 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest ...@@ -12,6 +12,7 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest
import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.task.submitBlocking
import de.rki.coronawarnapp.util.device.BackgroundModeStatus import de.rki.coronawarnapp.util.device.BackgroundModeStatus
import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.di.AppContext
import de.rki.coronawarnapp.util.di.ProcessLifecycle
import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
...@@ -25,7 +26,8 @@ import javax.inject.Singleton ...@@ -25,7 +26,8 @@ import javax.inject.Singleton
class WatchdogService @Inject constructor( class WatchdogService @Inject constructor(
@AppContext private val context: Context, @AppContext private val context: Context,
private val taskController: TaskController, private val taskController: TaskController,
private val backgroundModeStatus: BackgroundModeStatus private val backgroundModeStatus: BackgroundModeStatus,
@ProcessLifecycle private val processLifecycleOwner: LifecycleOwner
) { ) {
private val powerManager by lazy { private val powerManager by lazy {
...@@ -44,7 +46,7 @@ class WatchdogService @Inject constructor( ...@@ -44,7 +46,7 @@ class WatchdogService @Inject constructor(
} }
Timber.tag(TAG).v("Acquiring wakelocks for watchdog routine.") 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 // A wakelock as the OS does not handle this for us like in the background job execution
val wakeLock = createWakeLock() val wakeLock = createWakeLock()
// A wifi lock to wake up the wifi connection in case the device is dozing // A wifi lock to wake up the wifi connection in case the device is dozing
......
...@@ -2,9 +2,9 @@ package de.rki.coronawarnapp.util.device ...@@ -2,9 +2,9 @@ package de.rki.coronawarnapp.util.device
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import de.rki.coronawarnapp.util.di.ProcessLifecycle
import de.rki.coronawarnapp.CoronaWarnApplication
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
...@@ -15,7 +15,9 @@ import javax.inject.Inject ...@@ -15,7 +15,9 @@ import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class ForegroundState @Inject constructor() { class ForegroundState @Inject constructor(
@ProcessLifecycle val processLifecycleOwner: LifecycleOwner
) {
val isInForeground: Flow<Boolean> by lazy { val isInForeground: Flow<Boolean> by lazy {
MutableStateFlow(false).apply { MutableStateFlow(false).apply {
...@@ -23,19 +25,19 @@ class ForegroundState @Inject constructor() { ...@@ -23,19 +25,19 @@ class ForegroundState @Inject constructor() {
@Suppress("unused") @Suppress("unused")
@OnLifecycleEvent(Lifecycle.Event.ON_START) @OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onAppForegrounded() { fun onAppForegrounded() {
CoronaWarnApplication.isAppInForeground = true
Timber.v("App is in the foreground") Timber.v("App is in the foreground")
tryEmit(true)
} }
@Suppress("unused") @Suppress("unused")
@OnLifecycleEvent(Lifecycle.Event.ON_STOP) @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onAppBackgrounded() { fun onAppBackgrounded() {
CoronaWarnApplication.isAppInForeground = false
Timber.v("App is in the background") Timber.v("App is in the background")
tryEmit(false)
} }
} }
val processLifecycle = ProcessLifecycleOwner.get().lifecycle val processLifecycle = processLifecycleOwner.lifecycle
processLifecycle.addObserver(foregroundStateUpdater) processLifecycle.addObserver(foregroundStateUpdater)
} }
.onStart { Timber.v("isInForeground FLOW start") } .onStart { Timber.v("isInForeground FLOW start") }
......
...@@ -8,6 +8,8 @@ import android.content.Context ...@@ -8,6 +8,8 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import dagger.Module import dagger.Module
...@@ -63,4 +65,9 @@ class AndroidModule { ...@@ -63,4 +65,9 @@ class AndroidModule {
@Provides @Provides
@Singleton @Singleton
fun activityManager(@AppContext context: Context): ActivityManager = context.getSystemService()!! fun activityManager(@AppContext context: Context): ActivityManager = context.getSystemService()!!
@Provides
@Singleton
@ProcessLifecycle
fun procressLifecycleOwner(): LifecycleOwner = ProcessLifecycleOwner.get()
} }
package de.rki.coronawarnapp.util.di
import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class ProcessLifecycle
...@@ -4,15 +4,18 @@ import androidx.fragment.app.Fragment ...@@ -4,15 +4,18 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.observe
fun <T> LiveData<T>.observe2(fragment: Fragment, callback: (T) -> Unit) { 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> { val internalObserver = object : Observer<T> {
override fun onChanged(t: T?) { override fun onChanged(t: T) {
observer.onChanged(t) onValueChanged(t)
removeObserver(this) removeObserver(this)
} }
} }
......
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()
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment