From 52152995cf0bd587a9442d2f2475bd632f94f666 Mon Sep 17 00:00:00 2001 From: AlexanderAlferov <64849422+AlexanderAlferov@users.noreply.github.com> Date: Wed, 2 Dec 2020 14:52:34 +0300 Subject: [PATCH] New submission flow: Your consent screen (EXPOSUREAPP-3735) (#1767) * All your strings are belong to us! * Submission your consent screen * Unit test fix * Unit test fix * Unit test fix * Refactor view model, tests and events * Fixed test * Lint fix, added id to bottom divider --- .../viewmodel/SubmissionFragmentModule.kt | 5 + .../SubmissionYourConsentEvents.kt | 6 + .../SubmissionYourConsentFragment.kt | 63 +++++++ .../SubmissionYourConsentModule.kt | 18 ++ .../SubmissionYourConsentViewModel.kt | 43 +++++ .../fragment_submission_your_consent.xml | 159 ++++++++++++++++++ .../SubmissionYourConsentViewModelTest.kt | 109 ++++++++++++ 7 files changed, 403 insertions(+) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentEvents.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt create mode 100644 Corona-Warn-App/src/main/res/layout/fragment_submission_your_consent.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt index cc502e111..9fae7954f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt @@ -24,6 +24,8 @@ import de.rki.coronawarnapp.ui.submission.testresult.SubmissionTestResultFragmen import de.rki.coronawarnapp.ui.submission.testresult.SubmissionTestResultModule import de.rki.coronawarnapp.ui.submission.warnothers.SubmissionResultPositiveOtherWarningFragment import de.rki.coronawarnapp.ui.submission.warnothers.SubmissionResultPositiveOtherWarningModule +import de.rki.coronawarnapp.ui.submission.yourconsent.SubmissionYourConsentFragment +import de.rki.coronawarnapp.ui.submission.yourconsent.SubmissionYourConsentModule @Module internal abstract class SubmissionFragmentModule { @@ -67,6 +69,9 @@ internal abstract class SubmissionFragmentModule { @ContributesAndroidInjector(modules = [SubmissionConsentModule::class]) abstract fun submissionConsentScreen(): SubmissionConsentFragment + @ContributesAndroidInjector(modules = [SubmissionYourConsentModule::class]) + abstract fun submissionYourConsentScreen(): SubmissionYourConsentFragment + @ContributesAndroidInjector(modules = [SubmissionTestResultAvailableModule::class]) abstract fun submissionTestResultAvailableScreen(): SubmissionTestResultAvailableFragment diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentEvents.kt new file mode 100644 index 000000000..44a9fb2f1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentEvents.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.ui.submission.yourconsent + +sealed class SubmissionYourConsentEvents { + object GoBack : SubmissionYourConsentEvents() + object GoLegal : SubmissionYourConsentEvents() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentFragment.kt new file mode 100644 index 000000000..a93399ffe --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentFragment.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.ui.submission.yourconsent + +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.FragmentSubmissionYourConsentBinding +import de.rki.coronawarnapp.ui.main.MainActivity +import de.rki.coronawarnapp.util.di.AutoInject +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 + +class SubmissionYourConsentFragment : Fragment(R.layout.fragment_submission_your_consent), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: SubmissionYourConsentViewModel by cwaViewModels { viewModelFactory } + private val binding: FragmentSubmissionYourConsentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + vm.consent.observe2(this) { + binding.submissionYourConsentSwitch.status = it + binding.submissionYourConsentSwitch.settingsSwitchRowHeaderBody.setText( + if (it) { + R.string.submission_your_consent_switch_status_on + } else { + R.string.submission_your_consent_switch_status_off + } + ) + } + + vm.countryList.observe2(this) { + binding.submissionYourConsentAgreementCountryList.countries = it + } + + vm.clickEvent.observe2(this) { + when (it) { + is SubmissionYourConsentEvents.GoBack -> (activity as MainActivity).goBack() + // TODO: Navigation: is YourConsentEvents.GoLegal -> doNavigate(YourConsentFragmentDirections.actionSubmissionYourConsentFragmentToInformationPrivacyFragment()) + } + } + + binding.apply { + submissionYourConsentTitle.headerButtonBack.buttonIcon.setOnClickListener { vm.goBack() } + submissionYourConsentSwitch.settingsSwitchRowSwitch.setOnCheckedChangeListener { view, _ -> + if (!view.isPressed) return@setOnCheckedChangeListener + vm.switchConsent() + } + submissionYourConsentSwitch.settingsSwitchRow.setOnClickListener { vm.switchConsent() } + // TODO: Navigation: submissionYourConsentLegalDetailsCard.setOnClickListener { vm.goLegal() } + } + } + + override fun onResume() { + super.onResume() + binding.submissionYourConsentContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentModule.kt new file mode 100644 index 000000000..048b8e408 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.submission.yourconsent + +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 SubmissionYourConsentModule { + @Binds + @IntoMap + @CWAViewModelKey(SubmissionYourConsentViewModel::class) + abstract fun yourConsentFragment( + factory: SubmissionYourConsentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt new file mode 100644 index 000000000..1d8817763 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.ui.submission.yourconsent + +import androidx.lifecycle.asLiveData +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.first + +class SubmissionYourConsentViewModel @AssistedInject constructor( + val dispatcherProvider: DispatcherProvider, + interoperabilityRepository: InteroperabilityRepository, + val submissionRepository: SubmissionRepository +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val clickEvent: SingleLiveEvent<SubmissionYourConsentEvents> = SingleLiveEvent() + val consent = submissionRepository.hasGivenConsentToSubmission.asLiveData() + val countryList = interoperabilityRepository.countryListFlow.asLiveData() + + fun goBack() { + clickEvent.postValue(SubmissionYourConsentEvents.GoBack) + } + + fun switchConsent() { + launch { + if (submissionRepository.hasGivenConsentToSubmission.first()) { + submissionRepository.revokeConsentToSubmission() + } else { + submissionRepository.giveConsentToSubmission() + } + } + } + + fun goLegal() { + clickEvent.postValue(SubmissionYourConsentEvents.GoLegal) + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<SubmissionYourConsentViewModel> +} diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_your_consent.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_your_consent.xml new file mode 100644 index 000000000..25b2b9371 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_your_consent.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_your_consent_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/submission_your_consent_title" + android:fillViewport="true" + tools:context=".ui.submission.yourconsent.SubmissionYourConsentFragment"> + + <include + android:id="@+id/submission_your_consent_title" + layout="@layout/include_header" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:icon="@{@drawable/ic_back}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@{@string/submission_your_consent_title}" /> + + <ScrollView + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/match_constraint" + app:layout_constraintBottom_toBottomOf="@id/guideline_bottom" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_title" + app:layout_constraintVertical_bias="1.0"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true"> + + <include + android:id="@+id/submission_your_consent_switch" + layout="@layout/include_settings_switch_row" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + app:enabled="@{true}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:showDivider="@{true}" + app:subtitle="@{@string/submission_your_consent_switch_subtitle}" /> + + <TextView + android:id="@+id/submission_your_consent_about_text" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="@string/submission_your_consent_about_agreement" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_switch" /> + + <LinearLayout + android:id="@+id/submission_your_consent_agreement_card" + style="@style/cardTracing" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_medium" + android:focusable="true" + android:orientation="vertical" + app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" + app:layout_constraintStart_toStartOf="@+id/guideline_card_start" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_about_text"> + + <TextView + android:id="@+id/submission_your_consent_agreement_title" + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:contentDescription="@string/submission_your_consent_agreement_title" + android:text="@string/submission_your_consent_agreement_title"/> + + <TextView + android:id="@+id/submission_your_consent_agreement_share_test_results_text" + style="@style/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:text="@string/submission_your_consent_agreement_share_test_results"/> + + <de.rki.coronawarnapp.ui.view.CountryListView + android:id="@+id/submission_your_consent_agreement_country_list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" /> + + <TextView + android:id="@+id/submission_your_consent_agreement_share_symptoms_text" + style="@style/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:text="@string/submission_your_consent_agreement_share_symptoms"/> + + </LinearLayout> + + <View + android:id="@+id/submission_your_consent_agreement_details_divider_top" + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/card_divider" + android:layout_marginTop="@dimen/spacing_medium" + android:background="@color/colorHairline" + app:layout_constraintEnd_toEndOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_agreement_card" /> + + <TextView + android:id="@+id/submission_your_consent_agreement_details_text" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_small" + android:text="@string/submission_your_consent_agreement_details" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_agreement_details_divider_top" /> + + <View + android:id="@+id/submission_your_consent_agreement_details_divider_bottom" + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/card_divider" + android:layout_marginTop="@dimen/spacing_small" + android:background="@color/colorHairline" + app:layout_constraintEnd_toEndOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_your_consent_agreement_details_text" /> + + <include layout="@layout/merge_guidelines_side" /> + <include layout="@layout/merge_guidelines_card" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </ScrollView> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline_bottom" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/guideline_bottom" /> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt new file mode 100644 index 000000000..56b4b2e06 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt @@ -0,0 +1,109 @@ +package de.rki.coronawarnapp.ui.submission.yourconsent + +import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.ui.Country +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider +import testhelpers.extensions.CoroutinesTestExtension +import testhelpers.extensions.InstantExecutorExtension + +@ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) +class SubmissionYourConsentViewModelTest : BaseTest() { + + @MockK lateinit var submissionRepository: SubmissionRepository + @MockK lateinit var interoperabilityRepository: InteroperabilityRepository + + private val countryList = Country.values().toList() + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + every { interoperabilityRepository.countryListFlow } returns MutableStateFlow(countryList) + every { submissionRepository.hasGivenConsentToSubmission } returns flowOf(true) + every { submissionRepository.giveConsentToSubmission() } just Runs + every { submissionRepository.revokeConsentToSubmission() } just Runs + } + + private fun createViewModel(): SubmissionYourConsentViewModel = SubmissionYourConsentViewModel( + interoperabilityRepository = interoperabilityRepository, + submissionRepository = submissionRepository, + dispatcherProvider = TestDispatcherProvider + ) + + @AfterEach + fun teardown() { + clearAllMocks() + } + + @Test + fun `country list`() { + val viewModel = createViewModel() + + viewModel.countryList.observeForever { } + viewModel.countryList.value shouldBe countryList + } + + @Test + fun `go back`() { + val viewModel = createViewModel() + + viewModel.goBack() + viewModel.clickEvent.value shouldBe SubmissionYourConsentEvents.GoBack + } + + @Test + fun `consent removed`() { + val viewModel = createViewModel() + + coEvery { submissionRepository.hasGivenConsentToSubmission } returns flowOf(true) + viewModel.switchConsent() + verify(exactly = 1) { submissionRepository.revokeConsentToSubmission() } + } + + @Test + fun `consent given`() { + val viewModel = createViewModel() + + coEvery { submissionRepository.hasGivenConsentToSubmission } returns flowOf(false) + viewModel.switchConsent() + verify(exactly = 1) { submissionRepository.giveConsentToSubmission() } + } + + @Test + fun `consent repository changed`() { + val consentMutable = MutableStateFlow(false) + every { submissionRepository.hasGivenConsentToSubmission } returns consentMutable + + val viewModel = createViewModel() + + viewModel.consent.observeForever { } + viewModel.consent.value shouldBe false + + consentMutable.value = true + viewModel.consent.value shouldBe true + } + + @Test + fun `go to legal page`() { + val viewModel = createViewModel() + + viewModel.goLegal() + viewModel.clickEvent.value shouldBe SubmissionYourConsentEvents.GoLegal + } +} -- GitLab