Skip to content
Snippets Groups Projects
Unverified Commit 2b84ce81 authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Fix countries missing in onboarding after in-app reset (EXPOSUREAPP-4203) (#1849)


* Remove the extra flow that would cache the countries. The app config itself is cached already.
When accessing the countrylist they will be mapped from the latest available app config.

* Undo name shortening.

Co-authored-by: default avatarRalf Gehrer <ralfgehrer@users.noreply.github.com>
parent 882ec526
No related branches found
No related tags found
No related merge requests found
Showing
with 50 additions and 60 deletions
package de.rki.coronawarnapp.storage.interoperability package de.rki.coronawarnapp.storage.interoperability
import android.text.TextUtils
import androidx.lifecycle.asLiveData
import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.ui.Country import de.rki.coronawarnapp.ui.Country
import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.flow.map
import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
...@@ -18,26 +12,13 @@ import javax.inject.Singleton ...@@ -18,26 +12,13 @@ import javax.inject.Singleton
@Singleton @Singleton
class InteroperabilityRepository @Inject constructor( class InteroperabilityRepository @Inject constructor(
private val appConfigProvider: AppConfigProvider, private val appConfigProvider: AppConfigProvider
@AppScope private val appScope: CoroutineScope,
private val dispatcherProvider: DefaultDispatcherProvider
) { ) {
private val countryListFlowInternal = MutableStateFlow(listOf<Country>()) val countryList = appConfigProvider.currentConfig
val countryListFlow: Flow<List<Country>> = countryListFlowInternal .map {
@Deprecated("Use countryListFlow")
val countryList = countryListFlow.asLiveData()
init {
getAllCountries()
}
fun getAllCountries() {
// TODO Make this reactive, the AppConfigProvider should refresh itself on network changes.
appScope.launch(context = dispatcherProvider.IO) {
try { try {
val countries = appConfigProvider.getAppConfig() appConfigProvider.getAppConfig()
.supportedCountries .supportedCountries
.mapNotNull { rawCode -> .mapNotNull { rawCode ->
val countryCode = rawCode.toLowerCase(Locale.ROOT) val countryCode = rawCode.toLowerCase(Locale.ROOT)
...@@ -46,17 +27,15 @@ class InteroperabilityRepository @Inject constructor( ...@@ -46,17 +27,15 @@ class InteroperabilityRepository @Inject constructor(
if (mappedCountry == null) Timber.e("Unknown countrycode: %s", rawCode) if (mappedCountry == null) Timber.e("Unknown countrycode: %s", rawCode)
mappedCountry mappedCountry
} }
countryListFlowInternal.value = countries
Timber.d("Country list: ${TextUtils.join(System.lineSeparator(), countries)}")
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e, "Failed to map country list.")
countryListFlowInternal.value = emptyList() emptyList()
} }
} }
} .onEach { Timber.d("Country list: %s", it.joinToString(",")) }
fun clear() { suspend fun refreshCountries() {
countryListFlowInternal.value = emptyList() appConfigProvider.getAppConfig()
} }
fun saveInteroperabilityUsed() { fun saveInteroperabilityUsed() {
......
...@@ -29,7 +29,7 @@ class InteroperabilityConfigurationFragment : ...@@ -29,7 +29,7 @@ class InteroperabilityConfigurationFragment :
private var isNetworkCallbackRegistered = false private var isNetworkCallbackRegistered = false
private val networkCallback = object : ConnectivityHelper.NetworkCallback() { private val networkCallback = object : ConnectivityHelper.NetworkCallback() {
override fun onNetworkAvailable() { override fun onNetworkAvailable() {
vm.getAllCountries() vm.refreshCountries()
} }
override fun onNetworkUnavailable() { override fun onNetworkUnavailable() {
......
package de.rki.coronawarnapp.ui.interoperability package de.rki.coronawarnapp.ui.interoperability
import androidx.lifecycle.asLiveData
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.ui.SingleLiveEvent 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
class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor( class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor(
private val interoperabilityRepository: InteroperabilityRepository private val interoperabilityRepository: InteroperabilityRepository,
) : CWAViewModel() { dispatcherProvider: DispatcherProvider
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
val countryList = interoperabilityRepository.countryList val countryList = interoperabilityRepository.countryList
.asLiveData(context = dispatcherProvider.Default)
val navigateBack = SingleLiveEvent<Boolean>() val navigateBack = SingleLiveEvent<Boolean>()
fun onBackPressed() { fun onBackPressed() {
...@@ -21,8 +25,10 @@ class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor ...@@ -21,8 +25,10 @@ class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor
interoperabilityRepository.saveInteroperabilityUsed() interoperabilityRepository.saveInteroperabilityUsed()
} }
fun getAllCountries() { fun refreshCountries() {
interoperabilityRepository.getAllCountries() launch {
interoperabilityRepository.refreshCountries()
}
} }
@AssistedInject.Factory @AssistedInject.Factory
......
package de.rki.coronawarnapp.ui.onboarding package de.rki.coronawarnapp.ui.onboarding
import androidx.lifecycle.asLiveData
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.ui.SingleLiveEvent 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
class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject constructor( class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject constructor(
private val interoperabilityRepository: InteroperabilityRepository private val interopRepo: InteroperabilityRepository,
) : CWAViewModel() { dispatcherProvider: DispatcherProvider
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
val countryList = interoperabilityRepository.countryList val countryList = interopRepo.countryList.asLiveData(context = dispatcherProvider.Default)
val navigateBack = SingleLiveEvent<Boolean>() val navigateBack = SingleLiveEvent<Boolean>()
fun onBackPressed() { fun onBackPressed() {
...@@ -18,7 +21,7 @@ class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject construct ...@@ -18,7 +21,7 @@ class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject construct
} }
fun saveInteroperabilityUsed() { fun saveInteroperabilityUsed() {
interoperabilityRepository.saveInteroperabilityUsed() interopRepo.saveInteroperabilityUsed()
} }
@AssistedInject.Factory @AssistedInject.Factory
......
package de.rki.coronawarnapp.ui.onboarding package de.rki.coronawarnapp.ui.onboarding
import androidx.lifecycle.asLiveData
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.exception.reporting.report
...@@ -7,14 +8,17 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient ...@@ -7,14 +8,17 @@ import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
import de.rki.coronawarnapp.ui.SingleLiveEvent import de.rki.coronawarnapp.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
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
class OnboardingTracingFragmentViewModel @AssistedInject constructor( class OnboardingTracingFragmentViewModel @AssistedInject constructor(
private val interoperabilityRepository: InteroperabilityRepository private val interoperabilityRepository: InteroperabilityRepository,
) : CWAViewModel() { dispatcherProvider: DispatcherProvider
) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
val countryList = interoperabilityRepository.countryList val countryList = interoperabilityRepository.countryList
.asLiveData(context = dispatcherProvider.Default)
val routeToScreen: SingleLiveEvent<OnboardingNavigationEvents> = SingleLiveEvent() val routeToScreen: SingleLiveEvent<OnboardingNavigationEvents> = SingleLiveEvent()
fun saveInteroperabilityUsed() { fun saveInteroperabilityUsed() {
......
...@@ -16,10 +16,10 @@ import de.rki.coronawarnapp.task.TaskController ...@@ -16,10 +16,10 @@ import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.common.DefaultTaskRequest
import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
import de.rki.coronawarnapp.util.flow.combine
import de.rki.coronawarnapp.util.ui.SingleLiveEvent 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.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
...@@ -47,14 +47,14 @@ class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor( ...@@ -47,14 +47,14 @@ class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor(
} }
} }
val uiState = combineTransform( val uiState = combine(
currentSubmission, currentSubmission,
interoperabilityRepository.countryListFlow interoperabilityRepository.countryList
) { state, countries -> ) { state, countries ->
WarnOthersState( WarnOthersState(
submitTaskState = state, submitTaskState = state,
countryList = countries countryList = countries
).also { emit(it) } )
}.asLiveData(context = dispatcherProvider.Default) }.asLiveData(context = dispatcherProvider.Default)
val submissionError = SingleLiveEvent<Throwable>() val submissionError = SingleLiveEvent<Throwable>()
......
...@@ -72,7 +72,6 @@ class DataReset @Inject constructor( ...@@ -72,7 +72,6 @@ class DataReset @Inject constructor(
SubmissionRepository.reset() SubmissionRepository.reset()
keyCacheRepository.clear() keyCacheRepository.clear()
appConfigProvider.clear() appConfigProvider.clear()
interoperabilityRepository.clear()
exposureDetectionTracker.clear() exposureDetectionTracker.clear()
downloadDiagnosisKeysSettings.clear() downloadDiagnosisKeysSettings.clear()
riskLevelStorage.clear() riskLevelStorage.clear()
......
package de.rki.coronawarnapp.ui.interoperability package de.rki.coronawarnapp.ui.interoperability
import androidx.lifecycle.MutableLiveData
import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
import de.rki.coronawarnapp.ui.Country import de.rki.coronawarnapp.ui.Country
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.Runs import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.verify import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import testhelpers.TestDispatcherProvider
import testhelpers.extensions.InstantExecutorExtension import testhelpers.extensions.InstantExecutorExtension
import testhelpers.extensions.getOrAwaitValue import testhelpers.extensions.getOrAwaitValue
...@@ -25,28 +25,27 @@ class InteroperabilityConfigurationFragmentViewModelTest { ...@@ -25,28 +25,27 @@ class InteroperabilityConfigurationFragmentViewModelTest {
fun setupFreshViewModel() { fun setupFreshViewModel() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { interoperabilityRepository.countryList } returns MutableLiveData( every { interoperabilityRepository.countryList } returns flowOf(Country.values().toList())
Country.values().toList()
)
every { interoperabilityRepository.getAllCountries() } just Runs
} }
private fun createViewModel() = private fun createViewModel() =
InteroperabilityConfigurationFragmentViewModel(interoperabilityRepository) InteroperabilityConfigurationFragmentViewModel(interoperabilityRepository, TestDispatcherProvider)
@Test @Test
fun `viewmodel returns interop repo countryList`() { fun `viewmodel returns interop repo countryList`() {
val vm = createViewModel() val vm = createViewModel()
vm.countryList.getOrAwaitValue() shouldBe Country.values().toList() vm.countryList.getOrAwaitValue() shouldBe Country.values().toList()
verify { interoperabilityRepository.countryList }
} }
@Test @Test
fun testFetchCountryList() { fun `forced countrylist refresh via app config`() {
val vm = createViewModel() val vm = createViewModel()
verify(exactly = 0) { interoperabilityRepository.getAllCountries() } coVerify(exactly = 0) { interoperabilityRepository.refreshCountries() }
vm.getAllCountries() vm.refreshCountries()
verify(exactly = 1) { interoperabilityRepository.getAllCountries() } coVerify(exactly = 1) { interoperabilityRepository.refreshCountries() }
} }
@Test @Test
......
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