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 index eaf88daf0bfa813ccc1a453083c58ef0feae1004..634908d8bedf93147d08614b1d69c075480182aa 100644 --- 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 @@ -13,6 +13,7 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.spyk +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import org.junit.After @@ -71,6 +72,7 @@ class OnboardingAnalyticsFragmentTest : BaseUITest() { private fun onboardingAnalyticsViewModelSpy() = spyk( OnboardingAnalyticsViewModel( + appScope = GlobalScope, settings = settings, districts = districts, dispatcherProvider = TestDispatcherProvider(), diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/AnalyticsSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/AnalyticsSettings.kt index 9c240c16e7269c78546b020de46897865fa7d329..d8cd04f462c68679ded5ef54d7b502db2c69b2d3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/AnalyticsSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/AnalyticsSettings.kt @@ -90,6 +90,11 @@ class AnalyticsSettings @Inject constructor( defaultValue = false ) + val lastOnboardingVersionCode = prefs.createFlowPreference( + key = PKEY_ONBOARDED_VERSION_CODE, + defaultValue = 0L + ) + fun clear() = prefs.clearAndNotify() companion object { @@ -99,5 +104,6 @@ class AnalyticsSettings @Inject constructor( private const val PKEY_USERINFO_DISTRICT = "userinfo.district" private const val PKEY_LAST_SUBMITTED_TIMESTAMP = "analytics.submission.timestamp" private const val PKEY_ANALYTICS_ENABLED = "analytics.enabled" + private const val PKEY_ONBOARDED_VERSION_CODE = "analytics.onboarding.versionCode" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoFragment.kt index 55741441fd6f7001ce7351a8818b5f91a2236a1b..5e090ad1587a0af310a71330d55f70c8cdc643d9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.View import android.view.ViewGroup import android.view.accessibility.AccessibilityEvent +import androidx.activity.OnBackPressedCallback import androidx.core.view.isGone import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs @@ -52,16 +53,21 @@ class NewReleaseInfoFragment : Fragment(R.layout.new_release_info_screen_fragmen recyclerView.adapter = ItemAdapter(getItems()) } + // Override android back button to bypass the infinite loop + val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = vm.onNextButtonClick() + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) + vm.routeToScreen.observe2(this) { - if (it is NewReleaseInfoNavigationEvents.CloseScreen) { - if (args.comesFromInfoScreen) { + when (it) { + is NewReleaseInfoNavigationEvents.CloseScreen -> popBackStack() - } else { + is NewReleaseInfoNavigationEvents.NavigateToOnboardingDeltaAnalyticsFragment -> doNavigate( NewReleaseInfoFragmentDirections .actionNewReleaseInfoFragmentToOnboardingDeltaAnalyticsFragment() ) - } } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoNavigationEvents.kt index e485871c4a3ab9b3e9a8ffec4acc29b42bdf5877..d9fe60928f22b5b3fd49f3df5cf690a7c89dcd25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoNavigationEvents.kt @@ -2,4 +2,5 @@ package de.rki.coronawarnapp.release sealed class NewReleaseInfoNavigationEvents { object CloseScreen : NewReleaseInfoNavigationEvents() + object NavigateToOnboardingDeltaAnalyticsFragment : NewReleaseInfoNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModel.kt index 476bbd9bdf9b5c47276ac58fb1a8746d2d7c512e..6a872ed31b89446f265d33b3e10c6cdb4f75198d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModel.kt @@ -4,6 +4,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.SingleLiveEvent @@ -15,7 +16,8 @@ import timber.log.Timber class NewReleaseInfoViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, - private val settings: CWASettings + private val appSettings: CWASettings, + private val analyticsSettings: AnalyticsSettings ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen: SingleLiveEvent<NewReleaseInfoNavigationEvents> = SingleLiveEvent() @@ -23,8 +25,12 @@ class NewReleaseInfoViewModel @AssistedInject constructor( val title = R.string.release_info_version_title.toResolvingString(BuildConfig.VERSION_NAME) fun onNextButtonClick() { - settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } - routeToScreen.postValue(NewReleaseInfoNavigationEvents.CloseScreen) + appSettings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } + if (analyticsSettings.lastOnboardingVersionCode.value == 0L) { + routeToScreen.postValue(NewReleaseInfoNavigationEvents.NavigateToOnboardingDeltaAnalyticsFragment) + } else { + routeToScreen.postValue(NewReleaseInfoNavigationEvents.CloseScreen) + } } fun getItems( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragment.kt index 8727896d40b0c3a0b5dc66dd67e45e14d2edcdee..cd2d2231d7a44bb90c1a2ec4a51690bcf5aecb73 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsFragment.kt @@ -26,8 +26,8 @@ class OnboardingAnalyticsFragment : Fragment(R.layout.fragment_onboarding_ppa), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.apply { - onboardingButtonNext.setOnClickListener { viewModel.onNextButtonClick() } - onboardingButtonDisable.setOnClickListener { viewModel.onDisableClick() } + onboardingButtonNext.setOnClickListener { viewModel.onProceed(true) } + onboardingButtonDisable.setOnClickListener { viewModel.onProceed(false) } onboardingButtonBack.buttonIcon.setOnClickListener { requireActivity().onBackPressed() } federalStateRow.setOnClickListener { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModel.kt index ea44c1ea5a76bd05879084de383d0439d6b91041..600c2405f51c51168d9355634cc2f233bdc43d4a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModel.kt @@ -7,18 +7,23 @@ import dagger.assisted.AssistedInject 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.environment.BuildConfigWrap import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch class OnboardingAnalyticsViewModel @AssistedInject constructor( - settings: AnalyticsSettings, - dispatcherProvider: DispatcherProvider, + private val settings: AnalyticsSettings, + private val dispatcherProvider: DispatcherProvider, private val analytics: Analytics, - val districts: Districts + val districts: Districts, + @AppScope private val appScope: CoroutineScope ) : CWAViewModel() { val completedOnboardingEvent = SingleLiveEvent<Unit>() @@ -32,19 +37,11 @@ class OnboardingAnalyticsViewModel @AssistedInject constructor( districtsList.singleOrNull { it.districtId == id } }.asLiveData(dispatcherProvider.IO) - fun onNextButtonClick() { - launch { - analytics.setAnalyticsEnabled(enabled = true) + fun onProceed(enable: Boolean) { + appScope.launch(context = dispatcherProvider.IO) { + analytics.setAnalyticsEnabled(enabled = enable) } - - completedOnboardingEvent.postValue(Unit) - } - - fun onDisableClick() { - launch { - analytics.setAnalyticsEnabled(enabled = false) - } - + settings.lastOnboardingVersionCode.update { BuildConfigWrap.VERSION_CODE } completedOnboardingEvent.postValue(Unit) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaAnalyticsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaAnalyticsFragment.kt index e6e9ba278c958b0c58fd8e89619de7a0f1d12751..8efd0a961f244f665593e68517fbe20438e21050 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaAnalyticsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingDeltaAnalyticsFragment.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.ui.onboarding import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent +import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentOnboardingDeltaPpaBinding @@ -26,9 +27,15 @@ class OnboardingDeltaAnalyticsFragment : Fragment(R.layout.fragment_onboarding_d override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = viewModel.onProceed(false) + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) + binding.apply { - onboardingButtonNext.setOnClickListener { viewModel.onNextButtonClick() } - onboardingButtonDisable.setOnClickListener { viewModel.onDisableClick() } + onboardingButtonNext.setOnClickListener { viewModel.onProceed(true) } + onboardingButtonDisable.setOnClickListener { viewModel.onProceed(false) } onboardingButtonBack.buttonIcon.setOnClickListener { requireActivity().onBackPressed() } federalStateRow.setOnClickListener { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt index f9ae8c5c3879e6d6417e0ee3c51ecd8fa41c555f..04d9af9abf9d1e52c2f65fa2a6388a23d5a48704 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.release +import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.main.CWASettings +import de.rki.coronawarnapp.util.preferences.FlowPreference import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -10,29 +12,49 @@ import io.mockk.just 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.InstantExecutorExtension +import testhelpers.preferences.mockFlowPreference @ExtendWith(InstantExecutorExtension::class) -class NewReleaseInfoViewModelTest { +class NewReleaseInfoViewModelTest : BaseTest() { - @MockK lateinit var settings: CWASettings + @MockK lateinit var appSettings: CWASettings + @MockK lateinit var analyticsSettings: AnalyticsSettings + private lateinit var lastOnboardingVersionCode: FlowPreference<Long> lateinit var viewModel: NewReleaseInfoViewModel @BeforeEach fun setUp() { MockKAnnotations.init(this) - every { settings.lastChangelogVersion.update(any()) } just Runs + lastOnboardingVersionCode = mockFlowPreference(0L) + every { analyticsSettings.lastOnboardingVersionCode } returns lastOnboardingVersionCode + + every { appSettings.lastChangelogVersion.update(any()) } just Runs viewModel = NewReleaseInfoViewModel( TestDispatcherProvider(), - settings + appSettings, + analyticsSettings ) } @Test - fun testOnNextButtonClick() { + fun `if analytics onboarding has not yet been done, navigate to it`() { + lastOnboardingVersionCode.value shouldBe 0L + + viewModel.onNextButtonClick() + viewModel.routeToScreen.value shouldBe NewReleaseInfoNavigationEvents.NavigateToOnboardingDeltaAnalyticsFragment + } + + @Test + fun `if analytics onboarding is done, just close the release screen`() { + lastOnboardingVersionCode.update { 1130000L } + viewModel.onNextButtonClick() viewModel.routeToScreen.value shouldBe NewReleaseInfoNavigationEvents.CloseScreen + + lastOnboardingVersionCode.value shouldBe 1130000L } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c07b541dc8e7355c3eb832482cc7099623cf2d1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/onboarding/OnboardingAnalyticsViewModelTest.kt @@ -0,0 +1,84 @@ +package de.rki.coronawarnapp.ui.onboarding + +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.environment.BuildConfigWrap +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData.PPAAgeGroup +import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData.PPAFederalState +import de.rki.coronawarnapp.util.preferences.FlowPreference +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockkObject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider +import testhelpers.extensions.InstantExecutorExtension +import testhelpers.preferences.mockFlowPreference + +@ExtendWith(InstantExecutorExtension::class) +class OnboardingAnalyticsViewModelTest : BaseTest() { + + @MockK lateinit var settings: AnalyticsSettings + @MockK lateinit var analytics: Analytics + @MockK lateinit var districts: Districts + private lateinit var lastOnboardingVersionCode: FlowPreference<Long> + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + lastOnboardingVersionCode = mockFlowPreference(0L) + + every { settings.lastOnboardingVersionCode } returns lastOnboardingVersionCode + every { settings.userInfoAgeGroup } returns mockFlowPreference(PPAAgeGroup.AGE_GROUP_UNSPECIFIED) + every { settings.userInfoDistrict } returns mockFlowPreference(0) + every { settings.userInfoFederalState } returns mockFlowPreference(PPAFederalState.FEDERAL_STATE_UNSPECIFIED) + + coEvery { analytics.setAnalyticsEnabled(any()) } just Runs + + mockkObject(BuildConfigWrap) + every { BuildConfigWrap.VERSION_CODE } returns 1234567890L + } + + private fun createInstance(scope: CoroutineScope) = OnboardingAnalyticsViewModel( + appScope = scope, + dispatcherProvider = TestDispatcherProvider(), + analytics = analytics, + districts = districts, + settings = settings + ) + + @Test + fun `accepting ppa updates versioncode and state `() { + lastOnboardingVersionCode.value shouldBe 0L + + runBlockingTest { + createInstance(scope = this).onProceed(true) + } + + coVerify { analytics.setAnalyticsEnabled(true) } + lastOnboardingVersionCode.value shouldBe 1234567890L + } + + @Test + fun `declining ppa updates versioncode and state`() { + lastOnboardingVersionCode.value shouldBe 0L + + runBlockingTest { + createInstance(scope = this).onProceed(false) + } + + coVerify { analytics.setAnalyticsEnabled(false) } + lastOnboardingVersionCode.value shouldBe 1234567890L + } +}