diff --git a/.github/ISSUE_TEMPLATE/01_bugs.md b/.github/ISSUE_TEMPLATE/01_bugs.md index 87643902da22e4b1deb8dc73179d2cd687f5c662..e1c0c762e321b188de8bf3113deb6d746fe05667 100644 --- a/.github/ISSUE_TEMPLATE/01_bugs.md +++ b/.github/ISSUE_TEMPLATE/01_bugs.md @@ -17,13 +17,15 @@ Also, be sure to check our documentation first: https://github.com/corona-warn-a * [ ] Bug is specific for Android only, for general issues / questions that apply to iOS and Android please raise them in the [documentation repository](https://github.com/corona-warn-app/cwa-documentation) * [ ] Bug is not already reported in another issue -## Describe the bug +## Technical details -<!-- Describe your issue, but please be descriptive! Thanks again 🙌 â¤ï¸ --> +- Device name: +- Android version: +- App version: -## Expected behaviour +## Describe the bug -<!-- A clear and concise description of what you expected to happen. --> +<!-- Describe your issue, but please be descriptive! Thanks again 🙌 â¤ï¸ --> ## Steps to reproduce the issue @@ -36,10 +38,9 @@ Also, be sure to check our documentation first: https://github.com/corona-warn-a 4. See error --> -## Technical details +## Expected behaviour -- Mobile device: -- Android version: +<!-- A clear and concise description of what you expected to happen. --> ## Possible Fix diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index 23b0a591b2ae07771449a46d1eb4e5cc4e68ce37..a1d99bd4515ca2e7e674a1aef7aa513547badde6 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -53,7 +53,7 @@ android:enabled="true"/> <activity - android:name=".ui.LauncherActivity" + android:name=".ui.launcher.LauncherActivity" android:screenOrientation="portrait" android:theme="@style/AppTheme.Launcher"> <intent-filter> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt index f7bde81b1a52cfd96f878fa0dc43d310df3f7afd..9f67e506ed173a831038cb98c546008e0ef90789 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt @@ -15,8 +15,10 @@ data class ConfigDataContainer( ) : ConfigData, ConfigMapping by mappedConfig { override val updatedAt: Instant = serverTime.plus(localOffset) - override fun isValid(nowUTC: Instant): Boolean { + override fun isValid(nowUTC: Instant): Boolean = if (cacheValidity == Duration.ZERO) { + false + } else { val expiresAt = updatedAt.plus(cacheValidity) - return nowUTC.isBefore(expiresAt) + nowUTC.isBefore(expiresAt) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt index 4f0db33f244672f31344b0217142d6ef1305ac21..498b6ed9a828a6b80de9eb4dec50dea601741ae7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt @@ -49,10 +49,10 @@ class AppConfigStorage @Inject constructor( return@withLock try { InternalConfigData( rawData = legacyConfigFile.readBytes(), - serverTime = timeStamper.nowUTC, + serverTime = Instant.ofEpochMilli(legacyConfigFile.lastModified()), localOffset = Duration.ZERO, etag = "legacy.migration", - cacheValidity = Duration.standardMinutes(5) + cacheValidity = Duration.standardSeconds(0) ) } catch (e: Exception) { Timber.e(e, "Legacy config exits but couldn't be read.") @@ -81,6 +81,12 @@ class AppConfigStorage @Inject constructor( Timber.v("Overwriting %d from %s", configFile.length(), configFile.lastModified()) } + if (legacyConfigFile.exists()) { + if (legacyConfigFile.delete()) { + Timber.i("Legacy config file deleted, superseeded.") + } + } + if (value == null) { if (configFile.delete()) Timber.d("Config file was deleted (value=null).") return @@ -88,12 +94,6 @@ class AppConfigStorage @Inject constructor( try { gson.toJson(value, configFile) - - if (legacyConfigFile.exists()) { - if (legacyConfigFile.delete()) { - Timber.i("Legacy config file deleted, superseeded.") - } - } } catch (e: Exception) { // We'll not rethrow as we could still keep working just with the remote config, // but we will notify the user. diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt index b0c972544a74ef5442d0fa6da7173d55f9e3ff82..a0f7936a9711fb0a640ff6927b29ebad35845cc7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt @@ -1,16 +1,10 @@ package de.rki.coronawarnapp.storage.interoperability -import android.text.TextUtils -import androidx.lifecycle.asLiveData import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.Country -import de.rki.coronawarnapp.util.coroutine.AppScope -import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -18,26 +12,13 @@ import javax.inject.Singleton @Singleton class InteroperabilityRepository @Inject constructor( - private val appConfigProvider: AppConfigProvider, - @AppScope private val appScope: CoroutineScope, - private val dispatcherProvider: DefaultDispatcherProvider + private val appConfigProvider: AppConfigProvider ) { - private val countryListFlowInternal = MutableStateFlow(listOf<Country>()) - val countryListFlow: Flow<List<Country>> = countryListFlowInternal - - @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) { + val countryList = appConfigProvider.currentConfig + .map { try { - val countries = appConfigProvider.getAppConfig() + appConfigProvider.getAppConfig() .supportedCountries .mapNotNull { rawCode -> val countryCode = rawCode.toLowerCase(Locale.ROOT) @@ -46,17 +27,15 @@ class InteroperabilityRepository @Inject constructor( if (mappedCountry == null) Timber.e("Unknown countrycode: %s", rawCode) mappedCountry } - countryListFlowInternal.value = countries - Timber.d("Country list: ${TextUtils.join(System.lineSeparator(), countries)}") } catch (e: Exception) { - Timber.e(e) - countryListFlowInternal.value = emptyList() + Timber.e(e, "Failed to map country list.") + emptyList() } } - } + .onEach { Timber.d("Country list: %s", it.joinToString(",")) } - fun clear() { - countryListFlowInternal.value = emptyList() + suspend fun refreshCountries() { + appConfigProvider.getAppConfig() } fun saveInteroperabilityUsed() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt index 011e464a4139ce61ae3d27277f58feee268fd9ef..be99bd0647f1cfd07e087f595989465870bc5099 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/ActivityBinder.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.ui import dagger.Module import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.ui.launcher.LauncherActivity +import de.rki.coronawarnapp.ui.launcher.LauncherActivityModule import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.main.MainActivityModule import de.rki.coronawarnapp.ui.main.MainActivityTestModule @@ -13,7 +15,7 @@ abstract class ActivityBinder { @ContributesAndroidInjector(modules = [MainActivityModule::class, MainActivityTestModule::class]) abstract fun mainActivity(): MainActivity - @ContributesAndroidInjector + @ContributesAndroidInjector(modules = [LauncherActivityModule::class]) abstract fun launcherActivity(): LauncherActivity @ContributesAndroidInjector(modules = [OnboardingActivityModule::class]) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt deleted file mode 100644 index 1f55312ecea5c8dd259a6445f235aca801693e86..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/LauncherActivity.kt +++ /dev/null @@ -1,53 +0,0 @@ -package de.rki.coronawarnapp.ui - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.ui.main.MainActivity -import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity -import de.rki.coronawarnapp.update.UpdateChecker -import de.rki.coronawarnapp.util.di.AppInjector -import kotlinx.coroutines.launch - -class LauncherActivity : AppCompatActivity() { - companion object { - private val TAG: String? = LauncherActivity::class.simpleName - } - - private lateinit var updateChecker: UpdateChecker - - override fun onCreate(savedInstanceState: Bundle?) { - AppInjector.setup(this) - super.onCreate(savedInstanceState) - } - - override fun onResume() { - super.onResume() - - updateChecker = UpdateChecker(this) - lifecycleScope.launch { - updateChecker.checkForUpdate() - } - } - - fun navigateToActivities() { - if (LocalData.isOnboarded()) { - startMainActivity() - } else { - startOnboardingActivity() - } - } - - private fun startOnboardingActivity() { - OnboardingActivity.start(this) - this.overridePendingTransition(0, 0) - finish() - } - - private fun startMainActivity() { - MainActivity.start(this) - this.overridePendingTransition(0, 0) - finish() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt index be8e13c919927fcf9ea8da8c30100b8b8fadfd82..52beb8ba1fc8bb080a451945f501e52c355dcdae 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragment.kt @@ -29,7 +29,7 @@ class InteroperabilityConfigurationFragment : private var isNetworkCallbackRegistered = false private val networkCallback = object : ConnectivityHelper.NetworkCallback() { override fun onNetworkAvailable() { - vm.getAllCountries() + vm.refreshCountries() } override fun onNetworkUnavailable() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt index 4d73e11f99f4b63f41033b114b6a7081812167ae..95d96003cd7d339b74719e4a99c84317c4dde355 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModel.kt @@ -1,16 +1,20 @@ package de.rki.coronawarnapp.ui.interoperability +import androidx.lifecycle.asLiveData import com.squareup.inject.assisted.AssistedInject 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.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor( - private val interoperabilityRepository: InteroperabilityRepository -) : CWAViewModel() { + private val interoperabilityRepository: InteroperabilityRepository, + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val countryList = interoperabilityRepository.countryList + .asLiveData(context = dispatcherProvider.Default) val navigateBack = SingleLiveEvent<Boolean>() fun onBackPressed() { @@ -21,8 +25,10 @@ class InteroperabilityConfigurationFragmentViewModel @AssistedInject constructor interoperabilityRepository.saveInteroperabilityUsed() } - fun getAllCountries() { - interoperabilityRepository.getAllCountries() + fun refreshCountries() { + launch { + interoperabilityRepository.refreshCountries() + } } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt new file mode 100644 index 0000000000000000000000000000000000000000..af70ecc409fafd944ebeae4ef6e3d0e1c82bbdd4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivity.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.ui.launcher + +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.ui.main.MainActivity +import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity +import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +class LauncherActivity : AppCompatActivity() { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: LauncherActivityViewModel by cwaViewModels( + ownerProducer = { viewModelStore }, + factoryProducer = { viewModelFactory } + ) + + override fun onCreate(savedInstanceState: Bundle?) { + AppInjector.setup(this) + super.onCreate(savedInstanceState) + + vm.events.observe2(this) { + when (it) { + LauncherEvent.GoToOnboarding -> { + OnboardingActivity.start(this) + this.overridePendingTransition(0, 0) + finish() + } + LauncherEvent.GoToMainActivity -> { + MainActivity.start(this) + this.overridePendingTransition(0, 0) + finish() + } + is LauncherEvent.ShowUpdateDialog -> { + showUpdateNeededDialog(it.updateIntent) + } + } + } + } + + private fun showUpdateNeededDialog(intent: Intent) { + AlertDialog.Builder(this) + .setTitle(R.string.update_dialog_title) + .setMessage(R.string.update_dialog_message) + .setCancelable(false) + .setPositiveButton(R.string.update_dialog_button) { _, _ -> + ContextCompat.startActivity(this, intent, null) + } + .show() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca5fbbc15195e480e5e4b075512a15ace806bb16 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityModule.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.ui.launcher + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class LauncherActivityModule { + + @Binds + @IntoMap + @CWAViewModelKey(LauncherActivityViewModel::class) + abstract fun launcherActivity( + factory: LauncherActivityViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..d344f94c4a8960dc5da662697f398b0646178e26 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.ui.launcher + +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.update.UpdateChecker +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class LauncherActivityViewModel @AssistedInject constructor( + private val updateChecker: UpdateChecker, + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val events = SingleLiveEvent<LauncherEvent>() + + init { + launch { + val updateResult = updateChecker.checkForUpdate() + when { + updateResult.isUpdateNeeded -> LauncherEvent.ShowUpdateDialog(updateResult.updateIntent?.invoke()!!) + LocalData.isOnboarded() -> LauncherEvent.GoToMainActivity + else -> LauncherEvent.GoToOnboarding + }.let { events.postValue(it) } + } + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<LauncherActivityViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherEvent.kt new file mode 100644 index 0000000000000000000000000000000000000000..71470a17155c7f03ff8b85f672aa6cff9437de4d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherEvent.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.ui.launcher + +import android.content.Intent + +sealed class LauncherEvent { + object GoToOnboarding : LauncherEvent() + object GoToMainActivity : LauncherEvent() + data class ShowUpdateDialog( + val updateIntent: Intent + ) : LauncherEvent() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaInteroperabilityFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaInteroperabilityFragmentViewModel.kt index d817af9e90e91a55f57f2c9fb5cdadfea1847041..10ff693cd92a8bfcd921353a5c0aaf6a09a0196f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaInteroperabilityFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaInteroperabilityFragmentViewModel.kt @@ -1,16 +1,19 @@ package de.rki.coronawarnapp.ui.onboarding +import androidx.lifecycle.asLiveData import com.squareup.inject.assisted.AssistedInject 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.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject constructor( - private val interoperabilityRepository: InteroperabilityRepository -) : CWAViewModel() { + private val interopRepo: InteroperabilityRepository, + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - val countryList = interoperabilityRepository.countryList + val countryList = interopRepo.countryList.asLiveData(context = dispatcherProvider.Default) val navigateBack = SingleLiveEvent<Boolean>() fun onBackPressed() { @@ -18,7 +21,7 @@ class OnboardingDeltaInteroperabilityFragmentViewModel @AssistedInject construct } fun saveInteroperabilityUsed() { - interoperabilityRepository.saveInteroperabilityUsed() + interopRepo.saveInteroperabilityUsed() } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt index ad4d8e2808078116ea7f8be6538055d29e176fb4..078e462f9def169a578006b396056627fe9be5a0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt @@ -11,16 +11,19 @@ import de.rki.coronawarnapp.nearby.TracingPermissionHelper import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository 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.SimpleCWAViewModelFactory import timber.log.Timber class OnboardingTracingFragmentViewModel @AssistedInject constructor( private val interoperabilityRepository: InteroperabilityRepository, - private val tracingPermissionHelper: TracingPermissionHelper -) : CWAViewModel() { + private val tracingPermissionHelper: TracingPermissionHelper, + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - val countryList = interoperabilityRepository.countryListFlow.asLiveData() + val countryList = interoperabilityRepository.countryList + .asLiveData(context = dispatcherProvider.Default) val routeToScreen: SingleLiveEvent<OnboardingNavigationEvents> = SingleLiveEvent() val permissionRequestEvent = SingleLiveEvent<(Activity) -> Unit>() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..a6153679a5251c26f3a08649433fe610c5ef5463 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt @@ -0,0 +1,111 @@ +package de.rki.coronawarnapp.ui.submission.warnothers + +import androidx.lifecycle.asLiveData +import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.notification.TestResultNotificationService +import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.submission.SubmissionTask +import de.rki.coronawarnapp.submission.Symptoms +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +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.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import java.util.UUID + +class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor( + @Assisted private val symptoms: Symptoms, + dispatcherProvider: DispatcherProvider, + private val enfClient: ENFClient, + private val taskController: TaskController, + interoperabilityRepository: InteroperabilityRepository, + private val testResultNotificationService: TestResultNotificationService +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + private var currentSubmissionRequestId: UUID? = null + + private val currentSubmission = taskController.tasks + .map { it.find { taskInfo -> taskInfo.taskState.request.id == currentSubmissionRequestId }?.taskState } + .onEach { + it?.let { + when { + it.isFailed -> submissionError.postValue(it.error) + it.isSuccessful -> routeToScreen.postValue(SubmissionNavigationEvents.NavigateToSubmissionDone) + } + } + } + + val uiState = combine( + currentSubmission, + interoperabilityRepository.countryList + ) { state, countries -> + WarnOthersState( + submitTaskState = state, + countryList = countries + ) + }.asLiveData(context = dispatcherProvider.Default) + + val submissionError = SingleLiveEvent<Throwable>() + val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + + val requestKeySharing = SingleLiveEvent<Unit>() + val showEnableTracingEvent = SingleLiveEvent<Unit>() + + fun onBackPressed() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTestResult) + } + + fun onWarnOthersPressed() { + launch { + if (enfClient.isTracingEnabled.first()) { + requestKeySharing.postValue(Unit) + } else { + showEnableTracingEvent.postValue(Unit) + } + } + } + + fun onKeysShared(keys: List<TemporaryExposureKey>) { + if (keys.isNotEmpty()) { + submitDiagnosisKeys(keys) + } else { + submitWithNoDiagnosisKeys() + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToSubmissionDone) + } + testResultNotificationService.cancelPositiveTestResultNotification() + } + + private fun submitDiagnosisKeys(keys: List<TemporaryExposureKey>) { + Timber.d("submitDiagnosisKeys(keys=%s, symptoms=%s)", keys, symptoms) + val registrationToken = + LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + val taskRequest = DefaultTaskRequest( + SubmissionTask::class, + SubmissionTask.Arguments(registrationToken, keys, symptoms) + ) + currentSubmissionRequestId = taskRequest.id + taskController.submit(taskRequest) + } + + private fun submitWithNoDiagnosisKeys() { + Timber.d("submitWithNoDiagnosisKeys()") + SubmissionService.submissionSuccessful() + } + + @AssistedInject.Factory + interface Factory : CWAViewModelFactory<SubmissionResultPositiveOtherWarningViewModel> { + fun create(symptoms: Symptoms): SubmissionResultPositiveOtherWarningViewModel + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt index 695e297a1d7d12d223abc79a9303a4a69890d62a..dd664afaed6e8b74e869a4114b6b25bd6b27f761 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt @@ -2,85 +2,73 @@ package de.rki.coronawarnapp.update import android.content.Intent import android.net.Uri -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat.startActivity +import dagger.Reusable import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException -import de.rki.coronawarnapp.ui.LauncherActivity -import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.environment.BuildConfigWrap import timber.log.Timber +import javax.inject.Inject -class UpdateChecker(private val activity: LauncherActivity) { +@Reusable +class UpdateChecker @Inject constructor( + private val appConfigProvider: AppConfigProvider +) { - companion object { - val TAG: String? = UpdateChecker::class.simpleName - - const val STORE_PREFIX = "https://play.google.com/store/apps/details?id=" - const val COM_ANDROID_VENDING = "com.android.vending" - } - - suspend fun checkForUpdate() { - // check if an update is needed based on server config - val updateNeededFromServer: Boolean = try { - checkIfUpdatesNeededFromServer() - } catch (exception: ApplicationConfigurationCorruptException) { - Timber.e( - "ApplicationConfigurationCorruptException caught:%s", - exception.localizedMessage - ) - true - } catch (exception: Exception) { - Timber.e("Exception caught:%s", exception.localizedMessage) - false - } - - if (updateNeededFromServer) { - showUpdateNeededDialog() + suspend fun checkForUpdate(): Result = try { + if (isUpdateNeeded()) { + Result(isUpdateNeeded = true, updateIntent = createUpdateAction()) } else { - activity.navigateToActivities() + Result(isUpdateNeeded = false) } - } - - /** - * Show dialog there an update is needed and links to the play store - */ - private fun showUpdateNeededDialog() { - AlertDialog.Builder(activity) - .setTitle(activity.getString(R.string.update_dialog_title)) - .setMessage(activity.getString(R.string.update_dialog_message)) - .setCancelable(false) - .setPositiveButton(activity.getString(R.string.update_dialog_button)) { _, _ -> + } catch (exception: ApplicationConfigurationCorruptException) { + Timber.e( + "ApplicationConfigurationCorruptException caught:%s", + exception.localizedMessage + ) - val uriStringInPlayStore = STORE_PREFIX + BuildConfig.APPLICATION_ID - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse( - uriStringInPlayStore - ) - setPackage(COM_ANDROID_VENDING) - } - startActivity(activity, intent, null) - } - .create().show() + Result(isUpdateNeeded = true, updateIntent = createUpdateAction()) + } catch (exception: Exception) { + Timber.tag(TAG).e("Exception caught:%s", exception.localizedMessage) + Result(isUpdateNeeded = false) } - private suspend fun checkIfUpdatesNeededFromServer(): Boolean { - val cwaAppConfig: CWAConfig = AppInjector.component.appConfigProvider.getAppConfig() + private suspend fun isUpdateNeeded(): Boolean { + val cwaAppConfig: CWAConfig = appConfigProvider.getAppConfig() val minVersionFromServer = cwaAppConfig.minVersionCode - Timber.d( - "minVersionFromServer:%s", - minVersionFromServer - ) - Timber.d("Current app version:%s", BuildConfig.VERSION_CODE) + val currentVersion = BuildConfigWrap.VERSION_CODE + + Timber.tag(TAG).d("minVersionFromServer:%s", minVersionFromServer) + Timber.tag(TAG).d("Current app version:%s", currentVersion) val needsImmediateUpdate = VersionComparator.isVersionOlder( - BuildConfig.VERSION_CODE.toLong(), + currentVersion, minVersionFromServer ) - Timber.e("needs update:$needsImmediateUpdate") + Timber.tag(TAG).e("needs update:$needsImmediateUpdate") return needsImmediateUpdate } + + private fun createUpdateAction(): () -> Intent = { + val uriStringInPlayStore = STORE_PREFIX + BuildConfig.APPLICATION_ID + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(uriStringInPlayStore) + setPackage(COM_ANDROID_VENDING) + } + } + + data class Result( + val isUpdateNeeded: Boolean, + val updateIntent: (() -> Intent)? = null + ) + + companion object { + private const val TAG: String = "UpdateChecker" + + private const val STORE_PREFIX = "https://play.google.com/store/apps/details?id=" + private const val COM_ANDROID_VENDING = "com.android.vending" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index e5438192ad55b436bd559dc31e998c7e162facd2..03896cec4a8964a186cf8ac45da2a0349052c307 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -73,7 +73,6 @@ class DataReset @Inject constructor( submissionRepository.reset() keyCacheRepository.clear() appConfigProvider.clear() - interoperabilityRepository.clear() exposureDetectionTracker.clear() downloadDiagnosisKeysSettings.clear() riskLevelStorage.clear() diff --git a/Corona-Warn-App/src/main/res/layout/include_16_years.xml b/Corona-Warn-App/src/main/res/layout/include_16_years.xml index 2d6fbcdb4fe40deffad6cabf3142e14ff70066ff..cb9865c2471dcbfa2a23240a385c0daa33a62c2c 100644 --- a/Corona-Warn-App/src/main/res/layout/include_16_years.xml +++ b/Corona-Warn-App/src/main/res/layout/include_16_years.xml @@ -2,48 +2,28 @@ <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> - <data> - - <variable - name="headline" - type="String" /> - - <variable - name="body" - type="String" /> - - </data> - <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/sixteen_years" - style="@style/SixteenInclude" + style="@style/card" android:layout_width="match_parent" android:layout_height="wrap_content" - android:focusable="true"> + android:layout_margin="@dimen/spacing_small" + android:backgroundTint="@color/colorCardBackgroundHighlightGray" + android:focusable="true" + android:padding="@dimen/card_padding" + android:textColor="@color/colorStableLight"> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/sixteen_years_header" - android:layout_width="@dimen/match_constraint" + <TextView + android:id="@+id/sixteen_years_headline" + style="@style/headline6Sixteen" + android:layout_width="0dp" android:layout_height="wrap_content" + android:accessibilityHeading="true" + android:contentDescription="@string/sixteen_title_text" + android:text="@string/sixteen_title_text" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <TextView - android:id="@+id/sixteen_years_headline" - style="@style/headline6Sixteen" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:contentDescription="@{headline}" - android:text="@{headline}" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - - </androidx.constraintlayout.widget.ConstraintLayout> + app:layout_constraintTop_toTopOf="parent" /> <TextView android:id="@+id/sixteen_years_body" @@ -51,10 +31,11 @@ android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" - android:text="@{body}" + android:text="@string/sixteen_description_text" + app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/sixteen_years_header" /> + app:layout_constraintTop_toBottomOf="@+id/sixteen_years_headline" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/include_interoperability.xml b/Corona-Warn-App/src/main/res/layout/include_interoperability.xml index afd91f1ecea348d005c50995bb0b649c47d6973b..158adfad9e6dd935dbb9372aa94dfa34feb452ee 100644 --- a/Corona-Warn-App/src/main/res/layout/include_interoperability.xml +++ b/Corona-Warn-App/src/main/res/layout/include_interoperability.xml @@ -226,6 +226,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" android:padding="@dimen/card_padding" + tools:visibility="visible" android:visibility="@{FormatterHelper.formatVisibility(showFooter)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/include_onboarding.xml b/Corona-Warn-App/src/main/res/layout/include_onboarding.xml index c3d9ad1d20136c335457220bd162d01e2b2d1bcf..2236318df353218c3203ccaa60970758e4474d39 100644 --- a/Corona-Warn-App/src/main/res/layout/include_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/include_onboarding.xml @@ -200,11 +200,10 @@ layout="@layout/include_16_years" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_medium" + android:layout_marginTop="@dimen/spacing_small" android:focusable="true" + tools:visibility="visible" android:visibility="@{FormatterHelper.formatVisibilityText(locationHeadlineCard)}" - app:body="@{@string/sixteen_description_text}" - app:headline="@{@string/sixteen_title_text}" app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" app:layout_constraintStart_toStartOf="@+id/guideline_card_start" app:layout_constraintTop_toBottomOf="@+id/onboarding_location_card" /> @@ -214,7 +213,7 @@ layout="@layout/include_tracing_status_card" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_medium" + android:layout_marginTop="@dimen/spacing_small" android:focusable="true" android:visibility="@{FormatterHelper.formatVisibilityText(headlineCard)}" app:body="@{bodyCard}" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml b/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml index 0dc277c240812c260c786037fe7ae97ac6bc2eb9..b5a79867553ab763abb0170601c8dba691fb58c8 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml @@ -82,10 +82,8 @@ layout="@layout/include_16_years" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" android:focusable="true" - app:body="@{@string/sixteen_description_text}" - app:headline="@{@string/sixteen_title_text}" app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" app:layout_constraintStart_toStartOf="@+id/guideline_card_start" app:layout_constraintTop_toBottomOf="@+id/countryList" /> @@ -95,7 +93,7 @@ layout="@layout/include_privacy_card" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_small" app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" app:layout_constraintStart_toStartOf="@+id/guideline_card_start" app:layout_constraintTop_toBottomOf="@+id/submission_positive_location_card_16_years" /> 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 bcaf5c5974c2030e1648febe5a25b692b6bdcf29..d9fd3b077a133f96abd4a76d9bbc146502ce0ee2 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -288,7 +288,7 @@ <activity android:id="@+id/launcherActivity" - android:name="de.rki.coronawarnapp.ui.LauncherActivity" + android:name="de.rki.coronawarnapp.ui.launcher.LauncherActivity" android:label="LauncherActivity"> <deepLink android:id="@+id/deepLink" diff --git a/Corona-Warn-App/src/main/res/values/colors.xml b/Corona-Warn-App/src/main/res/values/colors.xml index 6c335bd728d4d9a5951b71a168d8a99544bc8c1d..972080081cf72dbe8e585585084623c74dbf26f8 100644 --- a/Corona-Warn-App/src/main/res/values/colors.xml +++ b/Corona-Warn-App/src/main/res/values/colors.xml @@ -14,6 +14,11 @@ <color name="colorSurface2Pressed">#D7D7D7</color> <color name="colorHairline">#3317191A</color> + <color name="cwaGrayHighlight">#5D6F80</color> + + <!--Cards--> + <color name="colorCardBackgroundHighlightGray">@color/cwaGrayHighlight</color> + <!-- Text --> <color name="colorTextPrimary1">#17191A</color> <color name="colorTextPrimary1Stable">#17191A</color> @@ -24,7 +29,7 @@ <color name="colorTextSixteen">#17191A</color> <color name="colorTextSemanticRed">#C00F2D</color> <color name="colorTextSemanticGreen">#2E854B</color> - <color name="colorTextSemanticNeutral">#5D6E80</color> + <color name="colorTextSemanticNeutral">@color/cwaGrayHighlight</color> <color name="colorTextTint">#007FAD</color> <!-- Semantic --> @@ -32,7 +37,7 @@ <color name="colorSemanticHighRiskPressed">#AE102B</color> <color name="colorSemanticLowRisk">#2E854B</color> <color name="colorSemanticLowRiskPressed">#2B7A46</color> - <color name="colorSemanticNeutralRisk">#5D6E80</color> + <color name="colorSemanticNeutralRisk">@color/cwaGrayHighlight</color> <color name="colorSemanticNeutralRiskPressed">#556675</color> <color name="colorSemanticUnknownRisk">#FFFFFF</color> <color name="colorSemanticUnknownRiskPressed">#E7E8E8</color> @@ -56,10 +61,10 @@ <color name="colorStableHairlineDark">#3317191A</color> <!-- Calendar --> - <color name="colorCalendarSelectedDayBackground">#5D6F80</color> + <color name="colorCalendarSelectedDayBackground">@color/cwaGrayHighlight</color> <color name="colorCalendarTodayBorder">#007FAD</color> <color name="colorCalendarTodayText">#007FAD</color> <color name="colorCalendarMonthText">#DE000000</color> - <color name="colorCalendarLayoutFocusOn">#FF5D6F80</color> + <color name="colorCalendarLayoutFocusOn">@color/cwaGrayHighlight</color> <color name="colorCalendarLayoutFocusOff">#F5F5F5</color> </resources> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index 08685ba09018032bc25a3b2252a2e1bfa62b5554..fb0b8a3a4444824fba211ec5ae23999a938de26b 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -133,12 +133,6 @@ <item name="android:backgroundTint">@color/colorSurface2</item> </style> - <style name="SixteenInclude"> - <item name="android:padding">@dimen/card_padding</item> - <item name="android:background">@drawable/card</item> - <item name="android:textColor">@color/colorStableLight</item> - </style> - <style name="selectionButton" parent="@style/Widget.AppCompat.Button.Borderless"> <item name="android:padding">@dimen/card_padding</item> <item name="android:gravity">left</item> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..dc6752cb715a3c5288c4144832f0f4847ed134af --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainerTest.kt @@ -0,0 +1,69 @@ +package de.rki.coronawarnapp.appconfig.internal + +import de.rki.coronawarnapp.appconfig.ConfigData +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ConfigDataContainerTest : BaseTest() { + + @Test + fun `cache validity is evaluated`() { + val now = Instant.EPOCH + val config = ConfigDataContainer( + serverTime = now, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.LAST_RETRIEVED, + identifier = "localetag", + cacheValidity = Duration.standardSeconds(300) + ) + config.isValid(now) shouldBe true + config.isValid(now.plus(Duration.standardSeconds(300))) shouldBe true + config.isValid(now.minus(Duration.standardSeconds(300))) shouldBe true + + val nowWithOffset = now.plus(config.localOffset) + config.isValid(nowWithOffset.plus(Duration.standardSeconds(299))) shouldBe true + config.isValid(nowWithOffset.minus(Duration.standardSeconds(299))) shouldBe true + + config.isValid(nowWithOffset) shouldBe true + config.isValid(nowWithOffset.minus(Duration.standardSeconds(300))) shouldBe true + config.isValid(nowWithOffset.plus(Duration.standardSeconds(300))) shouldBe false + } + + @Test + fun `cache validity can be set to 0`() { + val config = ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.LAST_RETRIEVED, + identifier = "localetag", + cacheValidity = Duration.standardSeconds(0) + ) + config.isValid(Instant.EPOCH) shouldBe false + config.isValid(Instant.EPOCH.plus(Duration.standardHours(1))) shouldBe false + config.isValid(Instant.EPOCH.plus(Duration.standardHours(24))) shouldBe false + config.isValid(Instant.EPOCH.plus(Duration.standardDays(14))) shouldBe false + + config.isValid(Instant.EPOCH.minus(Duration.standardHours(1))) shouldBe false + config.isValid(Instant.EPOCH.minus(Duration.standardHours(24))) shouldBe false + config.isValid(Instant.EPOCH.minus(Duration.standardDays(14))) shouldBe false + } + + @Test + fun `updated at is based on servertime and offset`() { + val config = ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.LAST_RETRIEVED, + identifier = "localetag", + cacheValidity = Duration.standardSeconds(0) + ) + config.updatedAt shouldBe Instant.EPOCH.plus(Duration.standardHours(1)) + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt index 13b19949d1f28e9aee3ea878d73df6cb34e387dd..8696718e5bf785905a7726ad6367a8becebe99f7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt @@ -130,6 +130,26 @@ class AppConfigStorageTest : BaseIOTest() { configPath.exists() shouldBe false } + @Test + fun `nulling deletes legacy config`() = runBlockingTest { + val storage = createStorage() + configPath.exists() shouldBe false + + storage.getStoredConfig() shouldBe null + storage.setStoredConfig(null) + configPath.exists() shouldBe false + + legacyConfigPath.exists() shouldBe false + legacyConfigPath.parentFile!!.mkdirs() + legacyConfigPath.writeBytes(APPCONFIG_RAW) + legacyConfigPath.exists() shouldBe true + + storage.setStoredConfig(null) + storage.getStoredConfig() shouldBe null + configPath.exists() shouldBe false + legacyConfigPath.exists() shouldBe false + } + @Test fun `if no fallback exists, but we have a legacy config, use that`() = runBlockingTest { configPath.exists() shouldBe false @@ -142,10 +162,10 @@ class AppConfigStorageTest : BaseIOTest() { storage.getStoredConfig() shouldBe InternalConfigData( rawData = APPCONFIG_RAW, - serverTime = Instant.ofEpochMilli(1234), + serverTime = Instant.ofEpochMilli(legacyConfigPath.lastModified()), localOffset = Duration.ZERO, etag = "I am an ETag :)!", - cacheValidity = Duration.standardMinutes(5) + cacheValidity = Duration.standardSeconds(0) ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt index d5e13842a13a8de0cdacbd91bffca9cae2b65fce..87c6c0f008a10f8fc3c5ff0a2b4cb8b4f5c876ec 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/interoperability/InteroperabilityConfigurationFragmentViewModelTest.kt @@ -1,18 +1,18 @@ package de.rki.coronawarnapp.ui.interoperability -import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.ui.Country import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.Runs +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just import io.mockk.verify +import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.TestDispatcherProvider import testhelpers.extensions.InstantExecutorExtension import testhelpers.extensions.getOrAwaitValue @@ -25,28 +25,27 @@ class InteroperabilityConfigurationFragmentViewModelTest { fun setupFreshViewModel() { MockKAnnotations.init(this) - every { interoperabilityRepository.countryList } returns MutableLiveData( - Country.values().toList() - ) - every { interoperabilityRepository.getAllCountries() } just Runs + every { interoperabilityRepository.countryList } returns flowOf(Country.values().toList()) } private fun createViewModel() = - InteroperabilityConfigurationFragmentViewModel(interoperabilityRepository) + InteroperabilityConfigurationFragmentViewModel(interoperabilityRepository, TestDispatcherProvider) @Test fun `viewmodel returns interop repo countryList`() { val vm = createViewModel() vm.countryList.getOrAwaitValue() shouldBe Country.values().toList() + + verify { interoperabilityRepository.countryList } } @Test - fun testFetchCountryList() { + fun `forced countrylist refresh via app config`() { val vm = createViewModel() - verify(exactly = 0) { interoperabilityRepository.getAllCountries() } - vm.getAllCountries() - verify(exactly = 1) { interoperabilityRepository.getAllCountries() } + coVerify(exactly = 0) { interoperabilityRepository.refreshCountries() } + vm.refreshCountries() + coVerify(exactly = 1) { interoperabilityRepository.refreshCountries() } } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..343f7b9609c1ee4baf50c2b74dbf63bf1ac2b47d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.ui.launcher + +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.update.UpdateChecker +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.instanceOf +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import io.mockk.mockkObject +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.TestDispatcherProvider +import testhelpers.extensions.InstantExecutorExtension + +@ExtendWith(InstantExecutorExtension::class) +class LauncherActivityViewModelTest { + + @MockK lateinit var updateChecker: UpdateChecker + + @BeforeEach + fun setupFreshViewModel() { + MockKAnnotations.init(this) + + mockkObject(LocalData) + every { LocalData.isOnboarded() } returns false + + coEvery { updateChecker.checkForUpdate() } returns UpdateChecker.Result(isUpdateNeeded = false) + } + + private fun createViewModel() = LauncherActivityViewModel( + updateChecker = updateChecker, + dispatcherProvider = TestDispatcherProvider + ) + + @Test + fun `update is available`() = runBlockingTest { + coEvery { updateChecker.checkForUpdate() } returns UpdateChecker.Result( + isUpdateNeeded = true, + updateIntent = { mockk() } + ) + + val vm = createViewModel() + + vm.events.value shouldBe instanceOf(LauncherEvent.ShowUpdateDialog::class) + } + + @Test + fun `fresh install no update needed`() { + val vm = createViewModel() + + vm.events.value shouldBe LauncherEvent.GoToOnboarding + } + + @Test + fun `onboarding finished`() { + every { LocalData.isOnboarded() } returns true + + val vm = createViewModel() + + vm.events.value shouldBe LauncherEvent.GoToMainActivity + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/UpdateCheckerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/UpdateCheckerTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5a783eb0516ee09a9aa95233b751035c8b8f149c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/UpdateCheckerTest.kt @@ -0,0 +1,100 @@ +package de.rki.coronawarnapp.update + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException +import de.rki.coronawarnapp.environment.BuildConfigWrap +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockkObject +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 + +class UpdateCheckerTest : BaseTest() { + + @MockK private lateinit var configData: ConfigData + @MockK private lateinit var appConfigProvider: AppConfigProvider + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + mockkObject(BuildConfigWrap) + + + coEvery { appConfigProvider.getAppConfig() } returns configData + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + fun createInstance() = UpdateChecker( + appConfigProvider = appConfigProvider + ) + + @Test + fun `update is required`() = runBlockingTest { + every { configData.minVersionCode } returns 10 + every { BuildConfigWrap.VERSION_CODE } returns 9 + + createInstance().checkForUpdate().apply { + isUpdateNeeded shouldBe true + updateIntent shouldNotBe null + } + + coVerifySequence { + appConfigProvider.getAppConfig() + BuildConfigWrap.VERSION_CODE + } + } + + @Test + fun `update is NOT required`() = runBlockingTest { + every { configData.minVersionCode } returns 10 + every { BuildConfigWrap.VERSION_CODE } returns 11 + + createInstance().checkForUpdate().apply { + isUpdateNeeded shouldBe false + updateIntent shouldBe null + } + + coVerifySequence { + appConfigProvider.getAppConfig() + BuildConfigWrap.VERSION_CODE + } + } + + @Test + fun `general error defaults to no update required`() = runBlockingTest { + every { configData.minVersionCode } throws Exception() + + createInstance().checkForUpdate().apply { + isUpdateNeeded shouldBe false + updateIntent shouldBe null + } + } + + @Test + fun `config parsing error means update is required`() = runBlockingTest { + every { configData.minVersionCode } throws ApplicationConfigurationCorruptException() + + createInstance().checkForUpdate().apply { + isUpdateNeeded shouldBe true + updateIntent shouldNotBe null + } + + coVerifySequence { + appConfigProvider.getAppConfig() + } + } +}