From b4f32951f12c392c40ad72e3bf5cb05b24a13666 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Wed, 7 Oct 2020 10:13:29 +0200 Subject: [PATCH] Introduce VM injection for home screen and enable screen testing (EXPOSUREAPP-2946, EXPOSUREAPP-2948) (#1320) * Rename "MainFragment" to "HomeFragment" Add VM injection. Move initial logic into VM. TODO: Move more code, write more tests. * Additional refactoring, moving logic out of the Fragment. Moving code into their own encapsulated routines/components. Use composition to reuse TracingViewModel, SettingsViewModel and SubmissionViewModel * Introduce @AppContext annotation to prevent anyone mistaking it for something else. * Add VM to RiskDetailsFragment * Simplify nested viewmodels, here: TracingViewModel.kt * Add skeletons instrumentation test skeleton for single fragments with injection and mocking. * Address PR comments. * Introduce sealed events class, + some refactoring. * Make the linter happy. --- Corona-Warn-App/build.gradle | 12 +- .../ui/main/home/HomeFragmentTest.kt | 46 ++++ .../java/testhelpers/BaseUITest.kt | 16 ++ .../FragmentTestModuleRegistrar.kt | 11 + .../java/testhelpers/TestAppComponent.kt | 24 ++ .../java/testhelpers/TestApplication.kt | 72 ++++++ .../testhelpers/TestApplicationUIRunner.kt | 12 + .../viewmodels/MockViewModelModule.kt | 38 +++ .../api/ui/TestForApiFragmentViewModel.kt | 3 +- ...iskLevelCalculationFragmentCWAViewModel.kt | 3 +- .../appconfig/AppConfigModule.kt | 3 +- .../appconfig/AppConfigStorage.kt | 3 +- .../diagnosiskeys/DiagnosisKeysModule.kt | 3 +- .../diagnosiskeys/storage/KeyCacheDatabase.kt | 3 +- .../storage/KeyCacheRepository.kt | 3 +- .../storage/legacy/LegacyKeyCacheMigration.kt | 3 +- .../environment/EnvironmentSetup.kt | 3 +- .../nearby/ENFClientLocalData.kt | 3 +- .../de/rki/coronawarnapp/nearby/ENFModule.kt | 3 +- .../coronawarnapp/storage/DeviceStorage.kt | 3 +- .../storage/SettingsRepository.kt | 3 +- .../submission/SubmissionModule.kt | 3 +- .../ui/main/MainActivityModule.kt | 10 +- .../rki/coronawarnapp/ui/main/MainFragment.kt | 234 ------------------ .../ui/main/home/HomeFragment.kt | 142 +++++++++++ .../ui/main/home/HomeFragmentEvents.kt | 11 + .../ui/main/home/HomeFragmentModule.kt | 22 ++ .../ui/main/home/HomeFragmentViewModel.kt | 72 ++++++ .../coronawarnapp/ui/main/home/HomeMenu.kt | 46 ++++ .../ui/main/home/TracingExplanationDialog.kt | 37 +++ .../{ => overview}/MainOverviewFragment.kt | 3 +- .../ui/main/{ => share}/MainShareFragment.kt | 3 +- .../ui/riskdetails/RiskDetailsFragment.kt | 54 ++-- .../riskdetails/RiskDetailsFragmentModule.kt | 22 ++ .../RiskDetailsFragmentViewModel.kt | 35 +++ .../ui/settings/SettingsTracingFragment.kt | 9 +- .../ui/viewmodel/SettingsViewModel.kt | 5 +- .../ui/viewmodel/SubmissionViewModel.kt | 5 +- .../ui/viewmodel/TracingViewModel.kt | 13 +- .../de/rki/coronawarnapp/util/DialogHelper.kt | 40 +-- .../rki/coronawarnapp/util/WatchdogService.kt | 5 +- .../util/device/DefaultPowerManagement.kt | 3 +- .../coronawarnapp/util/di/AndroidModule.kt | 1 + .../rki/coronawarnapp/util/di/AppContext.kt | 8 + .../security/EncryptedPreferencesFactory.kt | 3 +- .../util/security/EncryptionErrorResetTool.kt | 3 +- .../util/ui/FragmentExtensions.kt | 8 + .../coronawarnapp/util/ui/SmartLiveData.kt | 14 +- .../util/viewmodel/CWAViewModel.kt | 27 +- .../verification/VerificationModule.kt | 3 +- .../{fragment_main.xml => fragment_home.xml} | 2 +- .../src/main/res/navigation/nav_graph.xml | 8 +- .../ui/main/home/MainFragmentViewModelTest.kt | 2 + .../java/testhelpers/BaseTest.kt | 0 .../java/testhelpers/logging/JUnitTree.kt | 0 55 files changed, 775 insertions(+), 348 deletions(-) create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/BaseUITest.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/TestApplication.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/TestApplicationUIRunner.kt create mode 100644 Corona-Warn-App/src/androidTest/java/testhelpers/viewmodels/MockViewModelModule.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeMenu.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/TracingExplanationDialog.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/{ => overview}/MainOverviewFragment.kt (92%) rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/{ => share}/MainShareFragment.kt (94%) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppContext.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/FragmentExtensions.kt rename Corona-Warn-App/src/main/res/layout/{fragment_main.xml => fragment_home.xml} (99%) create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/main/home/MainFragmentViewModelTest.kt rename Corona-Warn-App/src/{test => testShared}/java/testhelpers/BaseTest.kt (100%) rename Corona-Warn-App/src/{test => testShared}/java/testhelpers/logging/JUnitTree.kt (100%) diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 78fd7e894..58504f84e 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -43,7 +43,7 @@ android { versionCode 43 versionName "1.5.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "testhelpers.TestApplicationUIRunner" resConfigs "de", "en", "tr", "bg", "pl", "ro" @@ -166,6 +166,12 @@ android { srcDirs = ['src/device'] } } + test { + java.srcDirs += "$projectDir/src/testShared/java" + } + androidTest { + java.srcDirs += "$projectDir/src/testShared/java" + } } } @@ -237,6 +243,7 @@ dependencies { implementation 'com.google.dagger:dagger-android-support:2.28.1' kapt 'com.google.dagger:dagger-compiler:2.28.1' kapt 'com.google.dagger:dagger-android-processor:2.28.1' + kaptAndroidTest 'com.google.dagger:dagger-compiler:2.28.1' compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2' kapt 'com.squareup.inject:assisted-inject-processor-dagger2:0.5.2' @@ -274,6 +281,7 @@ dependencies { androidTestImplementation "io.kotest:kotest-property-jvm:4.2.0" // Testing - Instrumentation + androidTestImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' @@ -282,7 +290,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.work:work-testing:2.3.4' androidTestImplementation "io.mockk:mockk-android:1.10.0" - debugImplementation 'androidx.fragment:fragment-testing:1.2.4' + debugImplementation 'androidx.fragment:fragment-testing:1.2.5' // Play Services implementation 'com.google.android.play:core:1.7.3' diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt new file mode 100644 index 000000000..baca08057 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.ui.main.home + +import androidx.fragment.app.testing.launchFragment +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Module +import dagger.android.ContributesAndroidInjector +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest + +@RunWith(AndroidJUnit4::class) +class HomeFragmentTest : BaseUITest() { + + @MockK lateinit var viewModel: HomeFragmentViewModel + + @Before + fun setup() { + MockKAnnotations.init(this, relaxed = true) + + setupMockViewModel(object : HomeFragmentViewModel.Factory { + override fun create(): HomeFragmentViewModel = viewModel + }) + } + + @After + fun teardown() { + clearAllViewModels() + } + + @Test + fun launch_fragment() { + launchFragment<HomeFragment>() + + // ... + } +} + +@Module +abstract class HomeFragmentTestModule { + @ContributesAndroidInjector + abstract fun homeScreen(): HomeFragment +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/BaseUITest.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/BaseUITest.kt new file mode 100644 index 000000000..8f487ee1d --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/BaseUITest.kt @@ -0,0 +1,16 @@ +package testhelpers + +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import testhelpers.viewmodels.MockViewModelModule + +abstract class BaseUITest : BaseTest() { + + inline fun <reified T : CWAViewModel> setupMockViewModel(factory: SimpleCWAViewModelFactory<T>) { + MockViewModelModule.CREATORS[T::class.java] = factory + } + + fun clearAllViewModels() { + MockViewModelModule.CREATORS.clear() + } +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt new file mode 100644 index 000000000..310356920 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt @@ -0,0 +1,11 @@ +package testhelpers + +import dagger.Module +import de.rki.coronawarnapp.ui.main.home.HomeFragmentTestModule + +@Module( + includes = [ + HomeFragmentTestModule::class + ] +) +class FragmentTestModuleRegistrar diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt new file mode 100644 index 000000000..53c3c747c --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestAppComponent.kt @@ -0,0 +1,24 @@ +package testhelpers + +import dagger.BindsInstance +import dagger.Component +import dagger.android.AndroidInjector +import dagger.android.support.AndroidSupportInjectionModule +import testhelpers.viewmodels.MockViewModelModule +import javax.inject.Singleton + +@Component( + modules = [ + AndroidSupportInjectionModule::class, + MockViewModelModule::class, + FragmentTestModuleRegistrar::class + ] +) +@Singleton +interface TestAppComponent : AndroidInjector<TestApplication> { + + @Component.Factory + interface Factory { + fun create(@BindsInstance app: TestApplication): TestAppComponent + } +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplication.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplication.kt new file mode 100644 index 000000000..dc215f014 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplication.kt @@ -0,0 +1,72 @@ +package testhelpers + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import dagger.android.support.AndroidSupportInjection +import de.rki.coronawarnapp.util.di.AutoInject +import timber.log.Timber +import javax.inject.Inject + +class TestApplication : Application(), HasAndroidInjector { + @Inject lateinit var androidInjector: DispatchingAndroidInjector<Any> + override fun androidInjector(): AndroidInjector<Any> = androidInjector + + private lateinit var component: TestAppComponent + + override fun onCreate() { + super.onCreate() + component = DaggerTestAppComponent.factory().create(this) + component.inject(this) + setupActivityHook() + } + + private fun setupActivityHook() { + registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks { + override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) { + setupFragmentHook(activity) + } + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit + + override fun onActivityStarted(activity: Activity) = Unit + + override fun onActivityResumed(activity: Activity) = Unit + + override fun onActivityPaused(activity: Activity) = Unit + + override fun onActivityStopped(activity: Activity) = Unit + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) = Unit + + override fun onActivityDestroyed(activity: Activity) = Unit + }) + } + + private fun setupFragmentHook(activity: Activity) { + if (activity is FragmentActivity) { + activity.supportFragmentManager + .registerFragmentLifecycleCallbacks(object : + FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentPreAttached( + fm: FragmentManager, + f: Fragment, + context: Context + ) { + if (f is AutoInject) { + Timber.d("Injecting %s", f) + AndroidSupportInjection.inject(f) + } + super.onFragmentPreAttached(fm, f, context) + } + }, true) + } + } +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplicationUIRunner.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplicationUIRunner.kt new file mode 100644 index 000000000..1576aab58 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/TestApplicationUIRunner.kt @@ -0,0 +1,12 @@ +package testhelpers + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +class TestApplicationUIRunner : AndroidJUnitRunner() { + + override fun newApplication(cl: ClassLoader, className: String, context: Context): Application { + return super.newApplication(cl, TestApplication::class.java.name, context) + } +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/viewmodels/MockViewModelModule.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/viewmodels/MockViewModelModule.kt new file mode 100644 index 000000000..1a4527b79 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/viewmodels/MockViewModelModule.kt @@ -0,0 +1,38 @@ +package testhelpers.viewmodels + +import android.os.Bundle +import androidx.lifecycle.SavedStateHandle +import androidx.savedstate.SavedStateRegistryOwner +import dagger.Module +import dagger.Provides +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider + +@Module +class MockViewModelModule { + + @Provides + fun viewmodelFactoryProvider(): CWAViewModelFactoryProvider.Factory { + val factory = object : CWAViewModelFactoryProvider.Factory { + override fun create( + savedStateOwner: SavedStateRegistryOwner, + defaultSavedState: Bundle?, + assistAction: ((CWAViewModelFactory<out CWAViewModel>, SavedStateHandle) -> CWAViewModel)? + ): CWAViewModelFactoryProvider { + return CWAViewModelFactoryProvider( + CREATORS, + savedStateOwner, + defaultSavedState, + assistAction + ) + } + } + return factory + } + + companion object { + val CREATORS: MutableMap<Class<out CWAViewModel>, CWAViewModelFactory<out CWAViewModel>> = + mutableMapOf() + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt index 5593009bb..47f89e72e 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt @@ -15,6 +15,7 @@ import de.rki.coronawarnapp.test.api.ui.EnvironmentState.Companion.toEnvironment import de.rki.coronawarnapp.test.api.ui.LoggerState.Companion.toLoggerState import de.rki.coronawarnapp.transaction.RiskLevelTransaction import de.rki.coronawarnapp.util.CWADebug +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.ui.smartLiveData import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -24,7 +25,7 @@ import kotlinx.coroutines.launch import java.io.File class TestForApiFragmentViewModel @AssistedInject constructor( - private val context: Context, + @AppContext private val context: Context, private val envSetup: EnvironmentSetup ) : CWAViewModel() { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index 7ba08ef6d..cd0b93eeb 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -23,6 +23,7 @@ import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.transaction.RiskLevelTransaction import de.rki.coronawarnapp.util.KeyFileHelper +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -41,7 +42,7 @@ import kotlin.coroutines.suspendCoroutine class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( @Assisted private val handle: SavedStateHandle, @Assisted private val exampleArg: String?, - private val context: Context, // App context + @AppContext private val context: Context, // App context private val enfClient: ENFClient, private val keyCacheRepository: KeyCacheRepository ) : CWAViewModel() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index aa295a352..d742d7370 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.Provides import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl +import de.rki.coronawarnapp.util.di.AppContext import okhttp3.Cache import okhttp3.OkHttpClient import org.joda.time.Duration @@ -20,7 +21,7 @@ class AppConfigModule { @Singleton @Provides fun provideAppConfigApi( - context: Context, + @AppContext context: Context, @DownloadCDNHttpClient client: OkHttpClient, @DownloadCDNServerUrl url: String, gsonConverterFactory: GsonConverterFactory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt index 44edccd9c..f81d9bee0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig import android.content.Context +import de.rki.coronawarnapp.util.di.AppContext import timber.log.Timber import java.io.File import javax.inject.Inject @@ -8,7 +9,7 @@ import javax.inject.Singleton @Singleton class AppConfigStorage @Inject constructor( - context: Context + @AppContext context: Context ) { private val configDir = File(context.filesDir, "appconfig_storage") private val configFile = File(configDir, "appconfig") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DiagnosisKeysModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DiagnosisKeysModule.kt index 213c479c3..c106f0a7e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DiagnosisKeysModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DiagnosisKeysModule.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.legacy.KeyCacheLegacyDao import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl import de.rki.coronawarnapp.storage.AppDatabase +import de.rki.coronawarnapp.util.di.AppContext import okhttp3.OkHttpClient import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory @@ -31,7 +32,7 @@ class DiagnosisKeysModule { @Singleton @Provides - fun legacyKeyCacheDao(context: Context): KeyCacheLegacyDao { + fun legacyKeyCacheDao(@AppContext context: Context): KeyCacheLegacyDao { return AppDatabase.getInstance(context).dateDao() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabase.kt index e4be6ba47..ec23391b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabase.kt @@ -12,6 +12,7 @@ import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.room.Update import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject @Database( @@ -45,7 +46,7 @@ abstract class KeyCacheDatabase : RoomDatabase() { suspend fun updateDownloadState(update: CachedKeyInfo.DownloadUpdate) } - class Factory @Inject constructor(private val context: Context) { + class Factory @Inject constructor(@AppContext private val context: Context) { /** * The fallback behavior is to reset the app as we only store exposure summaries * and cached references that are non-critical to app operation. diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheRepository.kt index 4336c7ac8..c6f42e84e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheRepository.kt @@ -23,6 +23,7 @@ import android.content.Context import android.database.sqlite.SQLiteConstraintException import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.joda.time.LocalDate @@ -35,7 +36,7 @@ import javax.inject.Singleton @Singleton class KeyCacheRepository @Inject constructor( - private val context: Context, + @AppContext private val context: Context, private val databaseFactory: KeyCacheDatabase.Factory, private val timeStamper: TimeStamper ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/legacy/LegacyKeyCacheMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/legacy/LegacyKeyCacheMigration.kt index ca2cf9586..b7b4113d7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/legacy/LegacyKeyCacheMigration.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/storage/legacy/LegacyKeyCacheMigration.kt @@ -5,6 +5,7 @@ import dagger.Lazy import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.util.HashExtensions.hashToMD5 import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.joda.time.Duration @@ -14,7 +15,7 @@ import java.io.File import javax.inject.Inject class LegacyKeyCacheMigration @Inject constructor( - private val context: Context, + @AppContext private val context: Context, private val legacyDao: Lazy<KeyCacheLegacyDao>, private val timeStamper: TimeStamper ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt index ea54dfb6d..f8937d9b9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt @@ -12,13 +12,14 @@ import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.VERIFICATION import de.rki.coronawarnapp.environment.EnvironmentSetup.EnvKey.VERIFICATION_KEYS import de.rki.coronawarnapp.environment.EnvironmentSetup.Type.Companion.toEnvironmentType import de.rki.coronawarnapp.util.CWADebug +import de.rki.coronawarnapp.util.di.AppContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class EnvironmentSetup @Inject constructor( - private val context: Context + @AppContext private val context: Context ) { enum class EnvKey(val rawKey: String) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClientLocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClientLocalData.kt index 26564ab4a..91d977c96 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClientLocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClientLocalData.kt @@ -2,13 +2,14 @@ package de.rki.coronawarnapp.nearby import android.content.Context import androidx.core.content.edit +import de.rki.coronawarnapp.util.di.AppContext import org.joda.time.Instant import javax.inject.Inject import javax.inject.Singleton @Singleton class ENFClientLocalData @Inject constructor( - private val context: Context + @AppContext private val context: Context ) { private val prefs by lazy { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt index 4b7094c8f..af07ccda5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt @@ -7,6 +7,7 @@ import dagger.Module import dagger.Provides import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider +import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Singleton @Module @@ -14,7 +15,7 @@ class ENFModule { @Singleton @Provides - fun exposureNotificationClient(context: Context): ExposureNotificationClient = + fun exposureNotificationClient(@AppContext context: Context): ExposureNotificationClient = Nearby.getExposureNotificationClient(context) @Singleton diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/DeviceStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/DeviceStorage.kt index 412d5900f..5fd8d4b51 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/DeviceStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/DeviceStorage.kt @@ -8,6 +8,7 @@ import android.os.storage.StorageManager import android.text.format.Formatter import dagger.Reusable import de.rki.coronawarnapp.util.ApiLevel +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.storage.StatsFsProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -19,7 +20,7 @@ import javax.inject.Inject @Reusable class DeviceStorage @Inject constructor( - private val context: Context, + @AppContext private val context: Context, private val apiLevel: ApiLevel, private val statsFsProvider: StatsFsProvider ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt index 54c2f4150..37b9e7c67 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt @@ -5,6 +5,7 @@ import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.util.BackgroundPrioritization import de.rki.coronawarnapp.util.ConnectivityHelper +import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject import javax.inject.Singleton @@ -18,7 +19,7 @@ import javax.inject.Singleton */ @Singleton class SettingsRepository @Inject constructor( - private val context: Context, + @AppContext private val context: Context, private val backgroundPrioritization: BackgroundPrioritization ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionModule.kt index 437d565d6..cb5fd7406 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionModule.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.http.HttpClientDefault import de.rki.coronawarnapp.http.RestrictedConnectionSpecs import de.rki.coronawarnapp.submission.server.SubmissionApiV1 import de.rki.coronawarnapp.submission.server.SubmissionHttpClient +import de.rki.coronawarnapp.util.di.AppContext import okhttp3.Cache import okhttp3.ConnectionSpec import okhttp3.OkHttpClient @@ -33,7 +34,7 @@ class SubmissionModule { @Singleton @Provides fun provideSubmissionApi( - context: Context, + @AppContext context: Context, @SubmissionHttpClient client: OkHttpClient, @SubmissionCDNServerUrl url: String, protoConverterFactory: ProtoConverterFactory, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt index 689223bc1..4db9d98e1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt @@ -4,9 +4,17 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule +import de.rki.coronawarnapp.ui.main.home.HomeFragmentModule import de.rki.coronawarnapp.ui.onboarding.OnboardingDeltaInteroperabilityModule +import de.rki.coronawarnapp.ui.riskdetails.RiskDetailsFragmentModule -@Module(includes = [OnboardingDeltaInteroperabilityModule::class]) +@Module( + includes = [ + OnboardingDeltaInteroperabilityModule::class, + HomeFragmentModule::class, + RiskDetailsFragmentModule::class + ] +) abstract class MainActivityModule { // activity specific injection module for future dependencies diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt deleted file mode 100644 index db82cafc9..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainFragment.kt +++ /dev/null @@ -1,234 +0,0 @@ -package de.rki.coronawarnapp.ui.main - -import android.os.Bundle -import android.view.View -import android.view.accessibility.AccessibilityEvent -import android.widget.PopupMenu -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentMainBinding -import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.timer.TimerHelper -import de.rki.coronawarnapp.ui.doNavigate -import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel -import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel -import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel -import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.DialogHelper -import de.rki.coronawarnapp.util.ExternalActionHelper -import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.errors.RecoveryByResetDialogFactory -import de.rki.coronawarnapp.util.ui.viewBindingLazy -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * After the user has finished the onboarding this fragment will be the heart of the application. - * Three ViewModels are needed that this fragment shows all relevant information to the user. - * Also the Menu is set here. - * - * @see tracingViewModel - * @see settingsViewModel - * @see submissionViewModel - * @see PopupMenu - */ -class MainFragment : Fragment(R.layout.fragment_main) { - - private val tracingViewModel: TracingViewModel by activityViewModels() - private val settingsViewModel: SettingsViewModel by activityViewModels() - private val submissionViewModel: SubmissionViewModel by activityViewModels() - private val binding: FragmentMainBinding by viewBindingLazy() - - private val errorResetTool by lazy { - AppInjector.component.errorResetTool - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - binding.submissionViewModel = submissionViewModel - - setButtonOnClickListener() - setContentDescription() - checkShouldInteroperabilityBeOpened() - showOneTimeTracingExplanationDialog() - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - - if (errorResetTool.isResetNoticeToBeShown) { - RecoveryByResetDialogFactory(this).showDialog( - detailsLink = R.string.errors_generic_text_catastrophic_error_encryption_failure, - onDismiss = { - errorResetTool.isResetNoticeToBeShown = false - } - ) - } - } - - override fun onResume() { - super.onResume() - // refresh required data - tracingViewModel.refreshRiskLevel() - tracingViewModel.refreshExposureSummary() - tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() - tracingViewModel.refreshIsTracingEnabled() - tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() - TimerHelper.checkManualKeyRetrievalTimer() - submissionViewModel.refreshDeviceUIState() - tracingViewModel.refreshLastSuccessfullyCalculatedScore() - binding.mainScrollview.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - } - - private fun setContentDescription() { - binding.mainHeaderShare.buttonIcon.contentDescription = getString(R.string.button_share) - binding.mainHeaderOptionsMenu.buttonIcon.contentDescription = - getString(R.string.button_menu) - binding.mainAbout.mainCard.contentDescription = getString(R.string.hint_external_webpage) - } - - private fun setButtonOnClickListener() { - binding.mainTestUnregistered.submissionStatusCardUnregistered.setOnClickListener { - toSubmissionIntro() - } - binding.mainTestUnregistered.submissionStatusCardUnregisteredButton.setOnClickListener { - toSubmissionIntro() - } - binding.mainTestDone.submissionStatusCardDone.setOnClickListener { - findNavController().doNavigate( - MainFragmentDirections.actionMainFragmentToSubmissionDoneFragment() - ) - } - binding.mainTestResult.submissionStatusCardContent.setOnClickListener { - toSubmissionResult() - } - binding.mainTestResult.submissionStatusCardContentButton.setOnClickListener { - toSubmissionResult() - } - binding.mainTestPositive.submissionStatusCardPositive.setOnClickListener { - toSubmissionResult() - } - binding.mainTestPositive.submissionStatusCardPositiveButton.setOnClickListener { - toSubmissionResult() - } - binding.mainTracing.setOnClickListener { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToSettingsTracingFragment()) - } - binding.mainRisk.riskCard.setOnClickListener { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToRiskDetailsFragment()) - } - binding.mainRisk.riskCardButtonUpdate.setOnClickListener { - tracingViewModel.refreshDiagnosisKeys() - settingsViewModel.updateManualKeyRetrievalEnabled(false) - } - binding.mainRisk.riskCardButtonEnableTracing.setOnClickListener { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToSettingsTracingFragment()) - } - binding.mainAbout.mainCard.setOnClickListener { - ExternalActionHelper.openUrl(this, requireContext().getString(R.string.main_about_link)) - } - binding.mainHeaderShare.buttonIcon.setOnClickListener { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToMainSharingFragment()) - } - binding.mainHeaderOptionsMenu.buttonIcon.setOnClickListener { - showPopup(it) - } - } - - private fun toSubmissionResult() { - findNavController().doNavigate( - MainFragmentDirections.actionMainFragmentToSubmissionResultFragment() - ) - } - - private fun toSubmissionIntro() { - findNavController().doNavigate( - MainFragmentDirections.actionMainFragmentToSubmissionIntroFragment() - ) - } - - private fun showPopup(view: View) = PopupMenu(requireContext(), view).apply { - inflate(R.menu.menu_main) - menu.findItem(R.id.menu_test).isVisible = CWADebug.isDeviceForTestersBuild - setOnMenuItemClickListener { - return@setOnMenuItemClickListener when (it.itemId) { - R.id.menu_help -> { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToMainOverviewFragment()) - true - } - R.id.menu_information -> { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToInformationFragment()) - true - } - R.id.menu_settings -> { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToSettingsFragment()) - true - } - R.id.menu_test -> { - findNavController().doNavigate(MainFragmentDirections.actionMainFragmentToTestNavGraph()) - true - } - else -> super.onOptionsItemSelected(it) - } - } - }.show() - - private fun checkShouldInteroperabilityBeOpened() { - if (!LocalData.isInteroperabilityShownAtLeastOnce) { - navigateToInteroperabilityFeature() - } - } - - private fun navigateToInteroperabilityFeature() { - findNavController().doNavigate( - MainFragmentDirections.actionMainFragmentToOnboardingDeltaInteroperabilityFragment() - ) - } - - private fun showOneTimeTracingExplanationDialog() { - - // check if the dialog explaining the tracing time was already shown - if (!LocalData.tracingExplanationDialogWasShown()) { - - val activity = this.requireActivity() - - lifecycleScope.launch { - - // get all text strings and the current active tracing time - val infoPeriodLogged = - getString(R.string.risk_details_information_body_period_logged) - val infoPeriodLoggedAssessment = - getString( - R.string.risk_details_information_body_period_logged_assessment, - (TimeVariables.getActiveTracingDaysInRetentionPeriod()).toString() - ) - val infoFAQ = getString(R.string.risk_details_explanation_dialog_faq_body) - - withContext(Dispatchers.Main) { - - // display the dialog - DialogHelper.showDialog( - DialogHelper.DialogInstance( - activity, - getString(R.string.risk_details_explanation_dialog_title), - "$infoPeriodLogged\n\n$infoPeriodLoggedAssessment\n\n$infoFAQ", - getString(R.string.errors_generic_button_positive), - null, - null, - { - LocalData.tracingExplanationDialogWasShown(true) - }, - {} - )) - } - } - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt new file mode 100644 index 000000000..da0071854 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -0,0 +1,142 @@ +package de.rki.coronawarnapp.ui.main.home + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentHomeBinding +import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.errors.RecoveryByResetDialogFactory +import de.rki.coronawarnapp.util.ui.doNavigate +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +/** + * After the user has finished the onboarding this fragment will be the heart of the application. + * Three ViewModels are needed that this fragment shows all relevant information to the user. + * Also the Menu is set here. + */ +class HomeFragment : Fragment(R.layout.fragment_home), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: HomeFragmentViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentHomeBinding by viewBindingLazy() + + @Inject lateinit var homeMenu: HomeMenu + @Inject lateinit var tracingExplanationDialog: TracingExplanationDialog + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.tracingViewModel = vm.tracingViewModel + binding.settingsViewModel = vm.settingsViewModel + binding.submissionViewModel = vm.submissionViewModel + + setupToolbar() + + setupTestResultCard() + + binding.mainTracing.setOnClickListener { + doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment()) + } + + setupRiskCard() + + binding.mainAbout.mainCard.apply { + setOnClickListener { + ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.main_about_link)) + } + contentDescription = getString(R.string.hint_external_webpage) + } + + vm.events.observe2(this) { + when (it) { + HomeFragmentEvents.ShowInteropDeltaOnboarding -> { + doNavigate( + HomeFragmentDirections.actionMainFragmentToOnboardingDeltaInteroperabilityFragment() + ) + } + is HomeFragmentEvents.ShowTracingExplanation -> { + tracingExplanationDialog.show(it.activeTracingDaysInRetentionPeriod) { + vm.tracingExplanationWasShown() + } + } + HomeFragmentEvents.ShowErrorResetDialog -> { + RecoveryByResetDialogFactory(this).showDialog( + detailsLink = R.string.errors_generic_text_catastrophic_error_encryption_failure, + onDismiss = { vm.errorResetDialogDismissed() } + ) + } + } + } + } + + override fun onResume() { + super.onResume() + vm.refreshRequiredData() + binding.mainScrollview.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } + + private fun setupRiskCard() { + binding.mainRisk.apply { + riskCard.setOnClickListener { + doNavigate(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) + } + riskCardButtonUpdate.setOnClickListener { + vm.tracingViewModel.refreshDiagnosisKeys() + vm.settingsViewModel.updateManualKeyRetrievalEnabled(false) + } + riskCardButtonEnableTracing.setOnClickListener { + doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment()) + } + } + } + + private fun setupTestResultCard() { + binding.apply { + val toSubmissionResult = { + doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionResultFragment()) + } + mainTestUnregistered.apply { + val toSubmissionIntro = { + doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionIntroFragment()) + } + submissionStatusCardUnregistered.setOnClickListener { toSubmissionIntro() } + submissionStatusCardUnregisteredButton.setOnClickListener { toSubmissionIntro() } + } + + mainTestDone.submissionStatusCardDone.setOnClickListener { + doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionDoneFragment()) + } + mainTestResult.apply { + submissionStatusCardContent.setOnClickListener { toSubmissionResult() } + submissionStatusCardContentButton.setOnClickListener { toSubmissionResult() } + } + + mainTestPositive.apply { + submissionStatusCardPositive.setOnClickListener { toSubmissionResult() } + submissionStatusCardPositiveButton.setOnClickListener { toSubmissionResult() } + } + } + } + + private fun setupToolbar() { + binding.mainHeaderShare.buttonIcon.apply { + contentDescription = getString(R.string.button_share) + setOnClickListener { + doNavigate(HomeFragmentDirections.actionMainFragmentToMainSharingFragment()) + } + } + + binding.mainHeaderOptionsMenu.buttonIcon.apply { + contentDescription = getString(R.string.button_menu) + setOnClickListener { homeMenu.showMenuFor(it) } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt new file mode 100644 index 000000000..cb0ea79e3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentEvents.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.ui.main.home + +sealed class HomeFragmentEvents { + object ShowInteropDeltaOnboarding : HomeFragmentEvents() + + data class ShowTracingExplanation( + val activeTracingDaysInRetentionPeriod: Long + ) : HomeFragmentEvents() + + object ShowErrorResetDialog : HomeFragmentEvents() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentModule.kt new file mode 100644 index 000000000..0032c0aa4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentModule.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui.main.home + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 HomeFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(HomeFragmentViewModel::class) + abstract fun homeFragment( + factory: HomeFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun homeScreen(): HomeFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt new file mode 100644 index 000000000..fb2bdc4d2 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -0,0 +1,72 @@ +package de.rki.coronawarnapp.ui.main.home + +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.timer.TimerHelper +import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowErrorResetDialog +import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowInteropDeltaOnboarding +import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowTracingExplanation +import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel +import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel +import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class HomeFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val errorResetTool: EncryptionErrorResetTool, + val tracingViewModel: TracingViewModel, + val settingsViewModel: SettingsViewModel, + val submissionViewModel: SubmissionViewModel +) : CWAViewModel( + dispatcherProvider = dispatcherProvider, + childViewModels = listOf(tracingViewModel, settingsViewModel, submissionViewModel) +) { + + val events = SingleLiveEvent<HomeFragmentEvents>() + + init { + if (!LocalData.isInteroperabilityShownAtLeastOnce) { + events.postValue(ShowInteropDeltaOnboarding) + } else { + launch { + if (!LocalData.tracingExplanationDialogWasShown()) { + events.postValue( + ShowTracingExplanation(TimeVariables.getActiveTracingDaysInRetentionPeriod()) + ) + } + } + launch { + if (errorResetTool.isResetNoticeToBeShown) { + events.postValue(ShowErrorResetDialog) + } + } + } + } + + fun errorResetDialogDismissed() { + errorResetTool.isResetNoticeToBeShown = false + } + + fun refreshRequiredData() { + tracingViewModel.refreshRiskLevel() + tracingViewModel.refreshExposureSummary() + tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() + tracingViewModel.refreshIsTracingEnabled() + tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() + TimerHelper.checkManualKeyRetrievalTimer() + submissionViewModel.refreshDeviceUIState() + tracingViewModel.refreshLastSuccessfullyCalculatedScore() + } + + fun tracingExplanationWasShown() { + LocalData.tracingExplanationDialogWasShown(true) + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<HomeFragmentViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeMenu.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeMenu.kt new file mode 100644 index 000000000..a20df5ef4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeMenu.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.ui.main.home + +import android.content.Context +import android.view.View +import android.widget.PopupMenu +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.ui.doNavigate +import de.rki.coronawarnapp.util.CWADebug +import javax.inject.Inject + +class HomeMenu @Inject constructor( + private val homeFragment: HomeFragment +) { + private val context: Context = homeFragment.requireContext() + + private val navController: NavController + get() = homeFragment.findNavController() + + fun showMenuFor(view: View) = PopupMenu(context, view).apply { + inflate(R.menu.menu_main) + menu.findItem(R.id.menu_test).isVisible = CWADebug.isDeviceForTestersBuild + setOnMenuItemClickListener { + return@setOnMenuItemClickListener when (it.itemId) { + R.id.menu_help -> { + navController.doNavigate(HomeFragmentDirections.actionMainFragmentToMainOverviewFragment()) + true + } + R.id.menu_information -> { + navController.doNavigate(HomeFragmentDirections.actionMainFragmentToInformationFragment()) + true + } + R.id.menu_settings -> { + navController.doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsFragment()) + true + } + R.id.menu_test -> { + navController.doNavigate(HomeFragmentDirections.actionMainFragmentToTestNavGraph()) + true + } + else -> homeFragment.onOptionsItemSelected(it) + } + } + }.show() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/TracingExplanationDialog.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/TracingExplanationDialog.kt new file mode 100644 index 000000000..a9bdd4dac --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/TracingExplanationDialog.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.ui.main.home + +import android.content.Context +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.DialogHelper +import javax.inject.Inject + +class TracingExplanationDialog @Inject constructor( + private val homeFragment: HomeFragment +) { + private val context: Context + get() = homeFragment.requireContext() + + fun show(activeTracingDaysInRetentionPeriod: Long, onPositive: () -> Unit) { + // get all text strings and the current active tracing time + val infoPeriodLogged = + context.getString(R.string.risk_details_information_body_period_logged) + val infoPeriodLoggedAssessment = + context.getString( + R.string.risk_details_information_body_period_logged_assessment, + activeTracingDaysInRetentionPeriod.toString() + ) + val infoFAQ = context.getString(R.string.risk_details_explanation_dialog_faq_body) + + val data = DialogHelper.DialogInstance( + context = context, + title = context.getString(R.string.risk_details_explanation_dialog_title), + message = "$infoPeriodLogged\n\n$infoPeriodLoggedAssessment\n\n$infoFAQ", + positiveButton = context.getString(R.string.errors_generic_button_positive), + negativeButton = null, + cancelable = null, + positiveButtonFunction = onPositive, + negativeButtonFunction = {} + ) + DialogHelper.showDialog(data) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/overview/MainOverviewFragment.kt similarity index 92% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/overview/MainOverviewFragment.kt index c99a5dba3..0a4dce381 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainOverviewFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/overview/MainOverviewFragment.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.ui.main +package de.rki.coronawarnapp.ui.main.overview import android.os.Bundle import android.view.View @@ -6,6 +6,7 @@ import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentMainOverviewBinding +import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.ui.viewBindingLazy /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/share/MainShareFragment.kt similarity index 94% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/share/MainShareFragment.kt index a82e0c320..6616b55c8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainShareFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/share/MainShareFragment.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.ui.main +package de.rki.coronawarnapp.ui.main.share import android.os.Bundle import android.view.View @@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentMainShareBinding +import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.ExternalActionHelper import de.rki.coronawarnapp.util.ui.viewBindingLazy diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt index b3846cbb9..0d53843a0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragment.kt @@ -4,17 +4,19 @@ import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentRiskDetailsBinding -import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel -import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.convertToHyperlink +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject /** * This is the detail view of the risk card if additional information for the user. @@ -22,51 +24,37 @@ import de.rki.coronawarnapp.util.convertToHyperlink * @see TracingViewModel * @see SettingsViewModel */ -class RiskDetailsFragment : Fragment(R.layout.fragment_risk_details) { +class RiskDetailsFragment : Fragment(R.layout.fragment_risk_details), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: RiskDetailsFragmentViewModel by cwaViewModels { viewModelFactory } - private val tracingViewModel: TracingViewModel by activityViewModels() - private val settingsViewModel: SettingsViewModel by activityViewModels() private val binding: FragmentRiskDetailsBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.tracingViewModel = tracingViewModel - binding.settingsViewModel = settingsViewModel - setButtonOnClickListeners() - setUpWebLinks() - } + binding.tracingViewModel = vm.tracingViewModel + binding.settingsViewModel = vm.settingsViewModel - override fun onResume() { - super.onResume() - // refresh required data - tracingViewModel.refreshRiskLevel() - tracingViewModel.refreshExposureSummary() - tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() - TimerHelper.checkManualKeyRetrievalTimer() - tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() - binding.riskDetailsContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - } - - /** - * Make the links clickable and convert to hyperlink - */ - private fun setUpWebLinks() { - binding.riskDetailsInformationLowriskBodyUrl - .convertToHyperlink(getString(R.string.risk_details_explanation_faq_body_with_link)) - } - - private fun setButtonOnClickListeners() { binding.riskDetailsHeaderButtonBack.setOnClickListener { (activity as MainActivity).goBack() } binding.riskDetailsButtonUpdate.setOnClickListener { - tracingViewModel.refreshDiagnosisKeys() - settingsViewModel.updateManualKeyRetrievalEnabled(false) + vm.updateRiskDetails() } binding.riskDetailsButtonEnableTracing.setOnClickListener { findNavController().doNavigate( RiskDetailsFragmentDirections.actionRiskDetailsFragmentToSettingsTracingFragment() ) } + + binding.riskDetailsInformationLowriskBodyUrl + .convertToHyperlink(getString(R.string.risk_details_explanation_faq_body_with_link)) + } + + override fun onResume() { + super.onResume() + vm.refreshData() + binding.riskDetailsContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentModule.kt new file mode 100644 index 000000000..1cf96b5c4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentModule.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui.riskdetails + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 RiskDetailsFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(RiskDetailsFragmentViewModel::class) + abstract fun homeFragment( + factory: RiskDetailsFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun riskDetails(): RiskDetailsFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentViewModel.kt new file mode 100644 index 000000000..cfe5ccd3f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/riskdetails/RiskDetailsFragmentViewModel.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.ui.riskdetails + +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.timer.TimerHelper +import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel +import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class RiskDetailsFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + val tracingViewModel: TracingViewModel, + val settingsViewModel: SettingsViewModel +) : CWAViewModel( + dispatcherProvider = dispatcherProvider, + childViewModels = listOf(tracingViewModel, settingsViewModel) +) { + + fun refreshData() { + tracingViewModel.refreshRiskLevel() + tracingViewModel.refreshExposureSummary() + tracingViewModel.refreshLastTimeDiagnosisKeysFetchedDate() + TimerHelper.checkManualKeyRetrievalTimer() + tracingViewModel.refreshActiveTracingDaysInRetentionPeriod() + } + + fun updateRiskDetails() { + tracingViewModel.refreshDiagnosisKeys() + settingsViewModel.updateManualKeyRetrievalEnabled(false) + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<RiskDetailsFragmentViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt index 7f496e7d2..e25321632 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsTracingFragment.kt @@ -22,7 +22,6 @@ import de.rki.coronawarnapp.ui.viewmodel.TracingViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper import de.rki.coronawarnapp.util.formatter.formatTracingSwitchEnabled -import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch @@ -53,12 +52,6 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), binding.tracingViewModel = tracingViewModel binding.settingsViewModel = settingsViewModel - tracingViewModel.navigateToInteroperability.observe2(this) { - if (it) { - navigateToInteroperability() - } - } - setButtonOnClickListener() } @@ -130,7 +123,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), ExternalActionHelper.toMainSettings(requireContext()) } interoperability.setOnClickListener { - tracingViewModel.onInteroperabilitySettingPressed() + navigateToInteroperability() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt index fa6bde7ff..2f9880069 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt @@ -1,16 +1,17 @@ package de.rki.coronawarnapp.ui.viewmodel import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel import de.rki.coronawarnapp.storage.SettingsRepository import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import javax.inject.Inject /** * ViewModel for everything settings related. * * @see SettingsRepository */ -class SettingsViewModel : ViewModel() { +class SettingsViewModel @Inject constructor() : CWAViewModel() { private val settingsRepository by lazy { AppInjector.component.settingsRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt index 5f2c6cbda..0d985fe35 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.ui.viewmodel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.exception.ExceptionCategory @@ -23,12 +22,14 @@ import de.rki.coronawarnapp.ui.submission.SymptomIntroductionEvent import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.Event import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import kotlinx.coroutines.launch import org.joda.time.LocalDate import timber.log.Timber import java.util.Date +import javax.inject.Inject -class SubmissionViewModel : ViewModel() { +class SubmissionViewModel @Inject constructor() : CWAViewModel() { private val _scanStatus = MutableLiveData(Event(ScanStatus.STARTED)) private val _registrationState = MutableLiveData(Event(ApiRequestState.IDLE)) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt index 06e3b5123..8e347e234 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/TracingViewModel.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.ui.viewmodel import android.view.View import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL @@ -19,13 +18,14 @@ import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.transaction.RiskLevelTransaction import de.rki.coronawarnapp.ui.riskdetails.DefaultRiskDetailPresenter import de.rki.coronawarnapp.util.ConnectivityHelper -import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant import timber.log.Timber import java.util.Date +import javax.inject.Inject /** * Provides all the relevant data for tracing relevant topics and settings. @@ -36,7 +36,7 @@ import java.util.Date * @see TracingRepository * @see RiskLevelRepository */ -class TracingViewModel : ViewModel() { +class TracingViewModel @Inject constructor() : CWAViewModel() { companion object { val TAG: String? = TracingViewModel::class.simpleName @@ -63,9 +63,6 @@ class TracingViewModel : ViewModel() { val additionalInformationVisibility = MediatorLiveData<Int>() val informationBodyNoticeVisibility = MediatorLiveData<Int>() - // event for interoperability navigation - val navigateToInteroperability = SingleLiveEvent<Boolean>() - init { additionalInformationVisibility.addSource(riskLevel) { additionalInformationVisibility.value = @@ -244,8 +241,4 @@ class TracingViewModel : ViewModel() { fun refreshLastSuccessfullyCalculatedScore() { RiskLevelRepository.refreshLastSuccessfullyCalculatedScore() } - - fun onInteroperabilitySettingPressed() { - navigateToInteroperability.postValue(true) - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt index fb3171270..e7a295a16 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DialogHelper.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.util -import android.app.Activity +import android.content.Context import android.text.SpannableString import android.text.method.LinkMovementMethod import android.text.util.Linkify @@ -12,7 +12,7 @@ import java.util.regex.Pattern object DialogHelper { data class DialogInstance( - val activity: Activity, + val context: Context, val title: String, val message: String?, val positiveButton: String, @@ -22,7 +22,7 @@ object DialogHelper { val negativeButtonFunction: () -> Unit? = {} ) { constructor( - activity: Activity, + context: Context, title: Int, message: Int, positiveButton: Int, @@ -31,18 +31,18 @@ object DialogHelper { positiveButtonFunction: () -> Unit? = {}, negativeButtonFunction: () -> Unit? = {} ) : this( - activity, - activity.resources.getString(title), - activity.resources.getString(message), - activity.resources.getString(positiveButton), - negativeButton?.let { activity.resources.getString(it) }, + context, + context.resources.getString(title), + context.resources.getString(message), + context.resources.getString(positiveButton), + negativeButton?.let { context.resources.getString(it) }, cancelable, positiveButtonFunction, negativeButtonFunction ) constructor( - activity: Activity, + context: Context, title: Int, message: String, positiveButton: Int, @@ -51,11 +51,11 @@ object DialogHelper { positiveButtonFunction: () -> Unit? = {}, negativeButtonFunction: () -> Unit? = {} ) : this( - activity, - activity.resources.getString(title), + context, + context.resources.getString(title), message, - activity.resources.getString(positiveButton), - negativeButton?.let { activity.resources.getString(it) }, + context.resources.getString(positiveButton), + negativeButton?.let { context.resources.getString(it) }, cancelable, positiveButtonFunction, negativeButtonFunction @@ -65,8 +65,8 @@ object DialogHelper { fun showDialog( dialogInstance: DialogInstance ): AlertDialog { - val message = getMessage(dialogInstance.activity, dialogInstance.message) - val alertDialog: AlertDialog = dialogInstance.activity.let { + val message = getMessage(dialogInstance.context, dialogInstance.message) + val alertDialog: AlertDialog = dialogInstance.context.let { val builder = AlertDialog.Builder(it) builder.apply { setTitle(dialogInstance.title) @@ -91,22 +91,22 @@ object DialogHelper { return alertDialog } - private fun getMessage(activity: Activity, message: String?): TextView { + private fun getMessage(context: Context, message: String?): TextView { // create spannable and add links, removed stack trace links into nowhere val spannable = SpannableString(message) val httpPattern: Pattern = Pattern.compile("[a-z]+://[^ \\n]*") Linkify.addLinks(spannable, httpPattern, "") // get padding for all sides - val paddingStartEnd = activity.resources.getDimension(R.dimen.spacing_normal).toInt() - val paddingLeftRight = activity.resources.getDimension(R.dimen.spacing_small).toInt() + val paddingStartEnd = context.resources.getDimension(R.dimen.spacing_normal).toInt() + val paddingLeftRight = context.resources.getDimension(R.dimen.spacing_small).toInt() // create a textview with clickable links from the spannable - val textView = TextView(activity) + val textView = TextView(context) textView.text = spannable textView.linksClickable = true textView.movementMethod = LinkMovementMethod.getInstance() textView.setPadding(paddingStartEnd, paddingLeftRight, paddingStartEnd, paddingLeftRight) textView.setTextAppearance(R.style.body1) - textView.setLinkTextColor(activity.getColorStateList(R.color.button_primary)) + textView.setLinkTextColor(context.getColorStateList(R.color.button_primary)) return textView } } 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 d2e5aa97c..93ad141a6 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.worker.BackgroundWorkHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch @@ -16,7 +17,9 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class WatchdogService @Inject constructor(private val context: Context) { +class WatchdogService @Inject constructor( + @AppContext private val context: Context +) { private val powerManager by lazy { context.getSystemService(Context.POWER_SERVICE) as PowerManager diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/DefaultPowerManagement.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/DefaultPowerManagement.kt index 12475b333..9cdd2143a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/DefaultPowerManagement.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/device/DefaultPowerManagement.kt @@ -5,10 +5,11 @@ import android.content.Intent import android.net.Uri import android.os.PowerManager import android.provider.Settings +import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject class DefaultPowerManagement @Inject constructor( - private val context: Context + @AppContext private val context: Context ) : PowerManagement { private val powerManager by lazy { 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 15b4f6a39..b5e1e3a24 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 @@ -16,5 +16,6 @@ class AndroidModule { @Provides @Singleton + @AppContext fun context(app: Application): Context = app.applicationContext } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppContext.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppContext.kt new file mode 100644 index 000000000..62a9e570e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AppContext.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.util.di + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class AppContext diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt index 34bd1d355..a352abdb1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKeys import de.rki.coronawarnapp.util.RetryMechanism +import de.rki.coronawarnapp.util.di.AppContext import timber.log.Timber import java.security.KeyException import javax.inject.Inject @@ -12,7 +13,7 @@ import javax.inject.Singleton @Singleton class EncryptedPreferencesFactory @Inject constructor( - private val context: Context + @AppContext private val context: Context ) { private val masterKeyAlias by lazy { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt index de29ea8f6..329cff723 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit import de.rki.coronawarnapp.storage.DATABASE_NAME +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.errors.causes import org.joda.time.Instant import timber.log.Timber @@ -22,7 +23,7 @@ import javax.inject.Singleton */ @Singleton class EncryptionErrorResetTool @Inject constructor( - private val context: Context + @AppContext private val context: Context ) { // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ContextImpl.java;drc=3b8e8d76315f6718a982d5e6a019b3aa4f634bcd;l=626 private val encryptedPreferencesFile by lazy { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/FragmentExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/FragmentExtensions.kt new file mode 100644 index 000000000..50e4175de --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/FragmentExtensions.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.util.ui + +import androidx.fragment.app.Fragment +import androidx.navigation.NavDirections +import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.ui.doNavigate + +fun Fragment.doNavigate(direction: NavDirections) = findNavController().doNavigate(direction) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SmartLiveData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SmartLiveData.kt index cdd903d65..d598cdf5b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SmartLiveData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/SmartLiveData.kt @@ -11,12 +11,16 @@ import kotlin.reflect.KProperty fun <T : Any> ViewModel.smartLiveData( dispatcher: CoroutineDispatcher = Dispatchers.Default, + liveDataFactory: (ViewModel, CoroutineDispatcher) -> SmartLiveData<T> = { vm, disp -> + SmartLiveData(vm, disp) + }, initAction: suspend () -> T -) = SmartLiveDataProperty(dispatcher, initAction) +) = SmartLiveDataProperty(dispatcher, initAction, liveDataFactory) -class SmartLiveDataProperty<T : Any>( +class SmartLiveDataProperty<T : Any, LV : SmartLiveData<T>>( private val dispatcher: CoroutineDispatcher = Dispatchers.Default, - private val initialValueProvider: suspend () -> T + private val initialValueProvider: suspend () -> T, + private val liveDataFactory: (ViewModel, CoroutineDispatcher) -> LV ) : ReadOnlyProperty<ViewModel, SmartLiveData<T>> { private var liveData: SmartLiveData<T>? = null @@ -29,7 +33,7 @@ class SmartLiveDataProperty<T : Any>( return@getValue it } - return SmartLiveData<T>(thisRef, dispatcher).also { + return liveDataFactory(thisRef, dispatcher).also { liveData = it thisRef.viewModelScope.launch(context = dispatcher) { it.postValue(initialValueProvider()) @@ -38,7 +42,7 @@ class SmartLiveDataProperty<T : Any>( } } -class SmartLiveData<T : Any>( +open class SmartLiveData<T : Any>( private val viewModel: ViewModel, private val dispatcher: CoroutineDispatcher ) : MutableLiveData<T>() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt index deaf4368c..9a61f091a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt @@ -3,20 +3,37 @@ package de.rki.coronawarnapp.util.viewmodel import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers +import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber +import kotlin.coroutines.CoroutineContext -abstract class CWAViewModel : ViewModel() { +abstract class CWAViewModel constructor( + private val dispatcherProvider: DispatcherProvider = DefaultDispatcherProvider(), + private val childViewModels: List<CWAViewModel> = emptyList() +) : ViewModel() { + + private val tag: String = this::class.simpleName!! init { - Timber.v("Initialized") + Timber.tag(tag).v("Initialized") } + fun launch( + context: CoroutineContext = dispatcherProvider.Main, + block: suspend CoroutineScope.() -> Unit + ): Job = viewModelScope.launch(context = context, block = block) + @CallSuper override fun onCleared() { - viewModelScope.launch(context = Dispatchers.Default) { } - Timber.v("onCleared()") + Timber.tag(tag).v("onCleared()") + childViewModels.forEach { + Timber.tag(tag).v("Clearing child VM: %s", it) + it.onCleared() + } super.onCleared() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt index 8bd41f974..85d2bea56 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt @@ -7,6 +7,7 @@ import dagger.Reusable import de.rki.coronawarnapp.environment.verification.VerificationCDNServerUrl import de.rki.coronawarnapp.http.HttpClientDefault import de.rki.coronawarnapp.http.RestrictedConnectionSpecs +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.verification.server.VerificationApiV1 import de.rki.coronawarnapp.verification.server.VerificationHttpClient import okhttp3.Cache @@ -32,7 +33,7 @@ class VerificationModule { @Singleton @Provides fun provideVerificationApi( - context: Context, + @AppContext context: Context, @VerificationHttpClient client: OkHttpClient, @VerificationCDNServerUrl url: String, gsonConverterFactory: GsonConverterFactory diff --git a/Corona-Warn-App/src/main/res/layout/fragment_main.xml b/Corona-Warn-App/src/main/res/layout/fragment_home.xml similarity index 99% rename from Corona-Warn-App/src/main/res/layout/fragment_main.xml rename to Corona-Warn-App/src/main/res/layout/fragment_home.xml index 67452ab83..d0330cb93 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_main.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_home.xml @@ -30,7 +30,7 @@ android:layout_height="match_parent" android:contentDescription="@string/main_title" android:fillViewport="true" - tools:context="de.rki.coronawarnapp.ui.main.MainFragment"> + tools:context="de.rki.coronawarnapp.ui.main.home.HomeFragment"> <!-- todo apply merge tags through xml when applicable (eod) --> <androidx.constraintlayout.widget.ConstraintLayout 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 ac0bff96c..0ad00127e 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -10,9 +10,9 @@ <!-- Main --> <fragment android:id="@+id/mainFragment" - android:name="de.rki.coronawarnapp.ui.main.MainFragment" + android:name="de.rki.coronawarnapp.ui.main.home.HomeFragment" android:label="MainFragment" - tools:layout="@layout/fragment_main"> + tools:layout="@layout/fragment_home"> <action android:id="@+id/action_mainFragment_to_settingsTracingFragment" app:destination="@id/settingsTracingFragment" /> @@ -50,13 +50,13 @@ <fragment android:id="@+id/mainSharingFragment" - android:name="de.rki.coronawarnapp.ui.main.MainShareFragment" + android:name="de.rki.coronawarnapp.ui.main.share.MainShareFragment" android:label="@layout/fragment_main_share" tools:layout="@layout/fragment_main_share" /> <fragment android:id="@+id/mainOverviewFragment" - android:name="de.rki.coronawarnapp.ui.main.MainOverviewFragment" + android:name="de.rki.coronawarnapp.ui.main.overview.MainOverviewFragment" android:label="@layout/fragment_main_overview" tools:layout="@layout/fragment_main_overview" /> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/main/home/MainFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/main/home/MainFragmentViewModelTest.kt new file mode 100644 index 000000000..4d2f97fd4 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/main/home/MainFragmentViewModelTest.kt @@ -0,0 +1,2 @@ +package de.rki.coronawarnapp.ui.main.home + diff --git a/Corona-Warn-App/src/test/java/testhelpers/BaseTest.kt b/Corona-Warn-App/src/testShared/java/testhelpers/BaseTest.kt similarity index 100% rename from Corona-Warn-App/src/test/java/testhelpers/BaseTest.kt rename to Corona-Warn-App/src/testShared/java/testhelpers/BaseTest.kt diff --git a/Corona-Warn-App/src/test/java/testhelpers/logging/JUnitTree.kt b/Corona-Warn-App/src/testShared/java/testhelpers/logging/JUnitTree.kt similarity index 100% rename from Corona-Warn-App/src/test/java/testhelpers/logging/JUnitTree.kt rename to Corona-Warn-App/src/testShared/java/testhelpers/logging/JUnitTree.kt -- GitLab