diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragmentTest.kt new file mode 100644 index 00000000000..afcc0661a26 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragmentTest.kt @@ -0,0 +1,102 @@ +package de.rki.coronawarnapp.ui.onboarding + +import androidx.lifecycle.asLiveData +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.Module +import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.datadonation.analytics.Analytics +import de.rki.coronawarnapp.datadonation.analytics.common.Districts +import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.spyk +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseUITest +import testhelpers.Screenshot +import testhelpers.SystemUIDemoModeRule +import testhelpers.TestDispatcherProvider +import testhelpers.captureScreenshot +import testhelpers.launchFragment2 +import tools.fastlane.screengrab.locale.LocaleTestRule + +@RunWith(AndroidJUnit4::class) +class OnboardingAnalyticsFragmentTest : BaseUITest() { + + @MockK lateinit var settings: AnalyticsSettings + @MockK lateinit var districts: Districts + @MockK lateinit var analytics: Analytics + + private lateinit var viewModel: OnboardingAnalyticsViewModel + + @Rule + @JvmField + val localeTestRule = LocaleTestRule() + + @get:Rule + val systemUIDemoModeRule = SystemUIDemoModeRule() + + @Before + fun setup() { + MockKAnnotations.init(this, relaxed = true) + + coEvery { districts.loadDistricts() } returns listOf(Districts.District( + districtId = 11011004, + districtName = "SK Berlin Charlottenburg-Wilmersdorf" + )) + + viewModel = onboardingAnalyticsViewModelSpy() + with(viewModel) { + every { ageGroup } returns flowOf(PpaData.PPAAgeGroup.AGE_GROUP_0_TO_29).asLiveData() + every { federalState } returns flowOf(PpaData.PPAFederalState.FEDERAL_STATE_BE).asLiveData() + every { district } returns flow { emit(districts.loadDistricts().first()) }.asLiveData() + } + + setupMockViewModel( + object : OnboardingAnalyticsViewModel.Factory { + override fun create(): OnboardingAnalyticsViewModel = viewModel + } + ) + } + + private fun onboardingAnalyticsViewModelSpy() = spyk( + OnboardingAnalyticsViewModel( + settings = settings, + districts = districts, + dispatcherProvider = TestDispatcherProvider(), + analytics = analytics + ) + ) + + @After + fun teardown() { + clearAllViewModels() + unmockkAll() + } + + @Test + fun launch_fragment() { + launchFragment2() + } + + @Screenshot + @Test + fun capture_screenshot() { + captureScreenshot() + } +} + +@Module +abstract class OnboardingAnalyticsFragmentTestModule { + @ContributesAndroidInjector + abstract fun onboardingAnalyticsFragment(): OnboardingAnalyticsFragment +} diff --git a/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt index 76df52f0469..c740c6b53e2 100644 --- a/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt +++ b/Corona-Warn-App/src/androidTest/java/testhelpers/FragmentTestModuleRegistrar.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.ui.contactdiary.ContactDiaryOnboardingFragmentTestMo import de.rki.coronawarnapp.ui.contactdiary.ContactDiaryOverviewFragmentTestModule import de.rki.coronawarnapp.ui.contactdiary.ContactDiaryPersonListFragmentTestModule import de.rki.coronawarnapp.ui.main.home.HomeFragmentTestModule +import de.rki.coronawarnapp.ui.onboarding.OnboardingAnalyticsFragmentTestModule import de.rki.coronawarnapp.ui.onboarding.OnboardingDeltaInteroperabilityFragmentTestModule import de.rki.coronawarnapp.ui.onboarding.OnboardingFragmentTestModule import de.rki.coronawarnapp.ui.onboarding.OnboardingNotificationsTestModule @@ -41,6 +42,7 @@ import de.rki.coronawarnapp.ui.tracing.TracingDetailsFragmentTestTestModule OnboardingPrivacyTestModule::class, OnboardingTestFragmentModule::class, OnboardingTracingFragmentTestModule::class, + OnboardingAnalyticsFragmentTestModule::class, // Submission SubmissionDispatcherTestModule::class, SubmissionTanTestModule::class, diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt new file mode 100644 index 00000000000..edfdc72fc92 --- /dev/null +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.datadonation.analytics.storage + +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import javax.inject.Inject + +class DefaultLastAnalyticsSubmissionLogger @Inject constructor() : LastAnalyticsSubmissionLogger { + override suspend fun storeAnalyticsData(analyticsProto: PpaData.PPADataAndroid) { + // Do not store past analytics submissions in Production + } + + override suspend fun getLastAnalyticsData(): LastAnalyticsSubmission? = null +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt new file mode 100644 index 00000000000..3b062450b50 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/datadonation/analytics/storage/DefaultLastAnalyticsSubmissionLogger.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.datadonation.analytics.storage + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.adapter.InstantAdapter +import de.rki.coronawarnapp.util.serialization.fromJson +import de.rki.coronawarnapp.util.serialization.toJson +import kotlinx.coroutines.withContext +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant +import org.json.JSONObject +import timber.log.Timber +import java.io.File +import javax.inject.Inject + +class DefaultLastAnalyticsSubmissionLogger @Inject constructor( + @AppContext private val context: Context, + private val dispatcherProvider: DispatcherProvider, + @BaseGson private val baseGson: Gson, + private val timeStamper: TimeStamper +) : LastAnalyticsSubmissionLogger { + private val analyticsDir = File(context.cacheDir, "analytics_storage") + private val analyticsFile = File(analyticsDir, "last_analytics.bin") + + private val gson by lazy { + baseGson.newBuilder() + .registerTypeAdapter(Instant::class.java, InstantAdapter()) + .registerTypeAdapter(PpaData.PPADataAndroid::class.java, PPADataAndroidAdapter()) + .create() + } + + override suspend fun storeAnalyticsData(analyticsProto: PpaData.PPADataAndroid) = + withContext(dispatcherProvider.IO) { + if (!analyticsDir.exists()) { + analyticsDir.mkdirs() + } + + val dataObject = LastAnalyticsSubmission( + timestamp = timeStamper.nowUTC, + ppaDataAndroid = analyticsProto + ) + + try { + gson.toJson(dataObject, analyticsFile) + } catch (e: Exception) { + Timber.e(e, "Failed to store analytics data.") + } + } + + override suspend fun getLastAnalyticsData(): LastAnalyticsSubmission? = withContext(dispatcherProvider.IO) { + try { + gson.fromJson(analyticsFile)?.also { + requireNotNull(it.ppaDataAndroid) + requireNotNull(it.timestamp) + } + } catch (e: Exception) { + Timber.e(e, "Couldn't load analytics data.") + null + } + } + + companion object { + class PPADataAndroidAdapter : TypeAdapter() { + override fun write(out: JsonWriter, value: PpaData.PPADataAndroid?) { + if (value == null) out.nullValue() + else value.toByteArray()?.toByteString()?.base64().let { out.value(it) } + } + + override fun read(reader: JsonReader): PpaData.PPADataAndroid? = when (reader.peek()) { + JSONObject.NULL -> reader.nextNull().let { null } + else -> { + val raw = reader.nextString().decodeBase64()?.toByteArray() + if (raw == null) { + throw JsonParseException("Can't decode base64 ByteArray") + } else { + PpaData.PPADataAndroid.parseFrom(raw) + } + } + } + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragment.kt index 54ccba0807a..fa6cd1d6b83 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragment.kt @@ -42,9 +42,16 @@ class DataDonationTestFragment : Fragment(R.layout.fragment_test_datadonation), } } + vm.currentAnalyticsData.observe2(this) { + binding.analyticsBody.text = it.toString() + } + binding.apply { safetynetCreateReport.setOnClickListener { vm.createSafetyNetReport() } safetynetCopyJws.setOnClickListener { vm.copyJWS() } + analyticsCollect.setOnClickListener { vm.collectAnalyticsData() } + analyticsCopy.setOnClickListener { vm.copyAnalytics() } + analyticsSubmit.setOnClickListener { vm.submitAnalytics() } } vm.copyJWSEvent.observe2(this) { jws -> @@ -56,6 +63,19 @@ class DataDonationTestFragment : Fragment(R.layout.fragment_test_datadonation), startActivity(intent) } + vm.copyAnalyticsEvent.observe2(this) { analytics -> + val intent = ShareCompat.IntentBuilder.from(requireActivity()).apply { + setType("text/plain") + setSubject("Analytics") + setText(analytics) + }.createChooserIntent() + startActivity(intent) + } + + vm.infoEvents.observe2(this) { + Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show() + } + vm.currentValidation.observe2(this) { items -> if (items?.first == null) { binding.safetynetRequirementsBody.text = "No validation yet." @@ -71,15 +91,19 @@ class DataDonationTestFragment : Fragment(R.layout.fragment_test_datadonation), } } } + binding.apply { safetynetRequirementsCasually.setOnClickListener { vm.validateSafetyNetCasually() } safetynetRequirementsStrict.setOnClickListener { vm.validateSafetyNetStrict() } } - vm.errorEvents.observe2(this) { - Toast.makeText(requireContext(), it.toString(), Toast.LENGTH_LONG).show() + vm.lastAnalyticsData.observe2(this) { + binding.analyticsLastSubmitBody.text = + it?.toString() ?: "No analytics were successfully submitted until now" } + vm.checkLastAnalytics() + binding.oneTimePasswordBody.text = vm.otp vm.surveyConfig.observe2(this) { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragmentViewModel.kt index c0603cc8211..6e71ce8d584 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/datadonation/ui/DataDonationTestFragmentViewModel.kt @@ -6,6 +6,9 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.SafetyNetRequirementsContainer +import de.rki.coronawarnapp.datadonation.analytics.Analytics +import de.rki.coronawarnapp.datadonation.analytics.storage.LastAnalyticsSubmission +import de.rki.coronawarnapp.datadonation.analytics.storage.LastAnalyticsSubmissionLogger import de.rki.coronawarnapp.datadonation.safetynet.CWASafetyNet import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetClientWrapper @@ -14,6 +17,7 @@ import de.rki.coronawarnapp.datadonation.safetynet.errorMsgRes import de.rki.coronawarnapp.datadonation.storage.OTPRepository import de.rki.coronawarnapp.datadonation.survey.SurveyException import de.rki.coronawarnapp.datadonation.survey.errorMsgRes +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -27,21 +31,30 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val safetyNetClientWrapper: SafetyNetClientWrapper, private val secureRandom: SecureRandom, + private val analytics: Analytics, + private val lastAnalyticsSubmissionLogger: LastAnalyticsSubmissionLogger, private val cwaSafetyNet: CWASafetyNet, otpRepository: OTPRepository, - appConfigProvider: AppConfigProvider + private val appConfigProvider: AppConfigProvider ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + val infoEvents = SingleLiveEvent() + private val currentReportInternal = MutableStateFlow(null) val currentReport = currentReportInternal.asLiveData(context = dispatcherProvider.Default) private val currentValidationInternal = MutableStateFlow?>(null) val currentValidation = currentValidationInternal.asLiveData(context = dispatcherProvider.Default) - - val errorEvents = SingleLiveEvent() val copyJWSEvent = SingleLiveEvent() + private val currentAnalyticsDataInternal = MutableStateFlow(null) + val currentAnalyticsData = currentAnalyticsDataInternal.asLiveData(context = dispatcherProvider.Default) + val copyAnalyticsEvent = SingleLiveEvent() + + private val lastAnalyticsDataInternal = MutableStateFlow(null) + val lastAnalyticsData = lastAnalyticsDataInternal.asLiveData(context = dispatcherProvider.Default) + val otp: String = otpRepository.otpAuthorizationResult?.toString() ?: "No OTP generated and authorized yet" val surveyConfig = appConfigProvider.currentConfig @@ -66,7 +79,7 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor( currentReportInternal.value = report } catch (e: Exception) { Timber.e(e, "attest() failed.") - errorEvents.postValue(e) + infoEvents.postValue(e.toString()) } } } @@ -110,6 +123,39 @@ class DataDonationTestFragmentViewModel @AssistedInject constructor( } } + fun collectAnalyticsData() = launch { + try { + val ppaDataAndroid = PpaData.PPADataAndroid.newBuilder() + analytics.collectContributions(ppaDataBuilder = ppaDataAndroid) + currentAnalyticsDataInternal.value = ppaDataAndroid.build() + } catch (e: Exception) { + Timber.e(e, "collectContributions() failed.") + infoEvents.postValue(e.toString()) + } + } + + fun submitAnalytics() = launch { + infoEvents.postValue("Starting Analytics Submission") + val analyticsConfig = appConfigProvider.getAppConfig().analytics + analytics.submitAnalyticsData(analyticsConfig) + infoEvents.postValue("Analytics Submission Done") + checkLastAnalytics() + } + + fun copyAnalytics() = launch { + val value = currentAnalyticsData.value?.toString() ?: "" + copyAnalyticsEvent.postValue(value) + } + + fun checkLastAnalytics() = launch { + try { + lastAnalyticsDataInternal.value = lastAnalyticsSubmissionLogger.getLastAnalyticsData() + } catch (e: Exception) { + Timber.e(e, "checkLastAnalytics() failed.") + infoEvents.postValue(e.toString()) + } + } + fun selectSafetyNetExceptionType(type: SafetyNetException.Type) { currentSafetyNetExceptionTypeInternal.value = type } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index 7d09cc6c432..45d99a414bd 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment +import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment @@ -30,6 +31,7 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { SettingsCrashReportFragment.MENU_ITEM, MiscInfoFragment.MENU_ITEM, ContactDiaryTestFragment.MENU_ITEM, + PlaygroundFragment.MENU_ITEM, DataDonationTestFragment.MENU_ITEM ).let { MutableLiveData(it) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt new file mode 100644 index 00000000000..82abd140ca3 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundFragment.kt @@ -0,0 +1,60 @@ +package de.rki.coronawarnapp.test.playground.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestPlaygroundBinding +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragment +import de.rki.coronawarnapp.datadonation.analytics.ui.input.AnalyticsUserInputFragmentArgs +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +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 + +@SuppressLint("SetTextI18n") +class PlaygroundFragment : Fragment(R.layout.fragment_test_playground), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + + private val vm: PlaygroundViewModel by cwaViewModels { viewModelFactory } + private val binding: FragmentTestPlaygroundBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + dataDonationUserinfoAgegroup.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.AGE_GROUP).toBundle() + ) + } + dataDonationUserinfoFederalstate.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.FEDERAL_STATE).toBundle() + ) + } + dataDonationUserinfoDistrict.setOnClickListener { + findNavController().navigate( + R.id.analyticsUserInputFragment, + AnalyticsUserInputFragmentArgs(AnalyticsUserInputFragment.InputType.DISTRICT).toBundle() + ) + } + } + } + + companion object { + val TAG: String = PlaygroundFragment::class.simpleName!! + val MENU_ITEM = TestMenuItem( + title = "Playground", + description = "Random options for not integrated features", + targetId = R.id.playgroundFragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt new file mode 100644 index 00000000000..85f6e94a93d --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundModule.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.test.playground.ui + +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 PlaygroundModule { + @Binds + @IntoMap + @CWAViewModelKey(PlaygroundViewModel::class) + abstract fun playground(factory: PlaygroundViewModel.Factory): CWAViewModelFactory +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt new file mode 100644 index 00000000000..d411ea539ab --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/playground/ui/PlaygroundViewModel.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.test.playground.ui + +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class PlaygroundViewModel @AssistedInject constructor() : CWAViewModel() { + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt index 1533e351341..c0511503a46 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -16,6 +16,8 @@ import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule +import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment +import de.rki.coronawarnapp.test.playground.ui.PlaygroundModule import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment @@ -53,6 +55,9 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [ContactDiaryTestFragmentModule::class]) abstract fun contactDiaryTest(): ContactDiaryTestFragment + @ContributesAndroidInjector(modules = [PlaygroundModule::class]) + abstract fun playground(): PlaygroundFragment + @ContributesAndroidInjector(modules = [DataDonationTestFragmentModule::class]) abstract fun dataDonation(): DataDonationTestFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_datadonation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_datadonation.xml index 1af2af29b52..39be37917e3 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_datadonation.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_datadonation.xml @@ -257,6 +257,98 @@ app:layout_constraintTop_toBottomOf="@id/survey_exception_simulation_radio_group" /> + + + + + + + + + + + + + + +