From 4b817ba404061243bebb4514ccbe9b940db600b9 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 20 Nov 2021 18:43:33 +0300 Subject: [PATCH] Add disclaimer fragment --- .../data/disclaimer/DisclaimerStorage.kt | 7 ++ .../data/disclaimer/DisclaimerStorageImpl.kt | 30 ++++++++ .../ui/disclaimer/DisclaimerFragment.kt | 76 +++++++++++++++++++ .../ui/disclaimer/DisclaimerModule.kt | 15 ++++ .../ui/disclaimer/DisclaimerViewModel.kt | 73 ++++++++++++++++++ .../main/res/layout/fragment_disclaimer.xml | 42 ++++++++++ app/src/main/res/navigation/nav_graph.xml | 19 ++++- app/src/main/res/values/strings.xml | 4 + .../disclaimer/DisclaimerStorageImplTest.kt | 26 +++++++ .../ui/disclaimer/DisclaimerViewModelTest.kt | 42 ++++++++++ 10 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerModule.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt create mode 100644 app/src/main/res/layout/fragment_disclaimer.xml create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImplTest.kt create mode 100644 app/src/test/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModelTest.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt new file mode 100644 index 0000000..7ace2de --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorage.kt @@ -0,0 +1,7 @@ +package gq.kirmanak.mealient.data.disclaimer + +interface DisclaimerStorage { + suspend fun isDisclaimerAccepted(): Boolean + + fun acceptDisclaimer() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt new file mode 100644 index 0000000..1952f2f --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.data.disclaimer + +import android.content.SharedPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +private const val IS_DISCLAIMER_ACCEPTED_KEY = "IS_DISCLAIMER_ACCEPTED" + +class DisclaimerStorageImpl @Inject constructor( + private val sharedPreferences: SharedPreferences +) : DisclaimerStorage { + + override suspend fun isDisclaimerAccepted(): Boolean { + Timber.v("isDisclaimerAccepted() called") + val isAccepted = withContext(Dispatchers.IO) { + sharedPreferences.getBoolean(IS_DISCLAIMER_ACCEPTED_KEY, false) + } + Timber.v("isDisclaimerAccepted() returned: $isAccepted") + return isAccepted + } + + override fun acceptDisclaimer() { + Timber.v("acceptDisclaimer() called") + sharedPreferences.edit() + .putBoolean(IS_DISCLAIMER_ACCEPTED_KEY, true) + .apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt new file mode 100644 index 0000000..d0a030f --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -0,0 +1,76 @@ +package gq.kirmanak.mealient.ui.disclaimer + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import gq.kirmanak.mealient.R +import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding +import timber.log.Timber + +@AndroidEntryPoint +class DisclaimerFragment : Fragment() { + private var _binding: FragmentDisclaimerBinding? = null + private val binding: FragmentDisclaimerBinding + get() = checkNotNull(_binding) { "Binding requested when fragment is off screen" } + private val viewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") + listenToAcceptStatus() + } + + private fun listenToAcceptStatus() { + Timber.v("listenToAcceptStatus() called") + viewModel.isAccepted.observe(this) { + Timber.d("listenToAcceptStatus: new status = $it") + if (it) navigateToAuth() + } + viewModel.checkIsAccepted() + } + + private fun navigateToAuth() { + Timber.v("navigateToAuth() called") + findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToAuthenticationFragment()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + Timber.v("onCreateView() called with: inflater = $inflater, container = $container, savedInstanceState = $savedInstanceState") + _binding = FragmentDisclaimerBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + binding.okay.setOnClickListener { + Timber.v("onViewCreated: okay clicked") + viewModel.acceptDisclaimer() + } + viewModel.okayCountDown.observe(viewLifecycleOwner) { + Timber.d("onViewCreated: new count $it") + binding.okay.text = if (it > 0) { + getString(R.string.fragment_disclaimer_button_okay_timer, it) + } else { + getString(R.string.fragment_disclaimer_button_okay) + } + binding.okay.isClickable = it == 0 + } + viewModel.startCountDown() + } + + override fun onDestroyView() { + super.onDestroyView() + Timber.v("onDestroyView() called") + _binding = null + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerModule.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerModule.kt new file mode 100644 index 0000000..93d0a64 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerModule.kt @@ -0,0 +1,15 @@ +package gq.kirmanak.mealient.ui.disclaimer + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorageImpl + +@Module +@InstallIn(SingletonComponent::class) +interface DisclaimerModule { + @Binds + fun provideDisclaimerStorage(disclaimerStorageImpl: DisclaimerStorageImpl): DisclaimerStorage +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt new file mode 100644 index 0000000..ea056d1 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt @@ -0,0 +1,73 @@ +package gq.kirmanak.mealient.ui.disclaimer + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.launch +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@HiltViewModel +class DisclaimerViewModel @Inject constructor( + private val disclaimerStorage: DisclaimerStorage +) : ViewModel() { + private val _isAccepted = MutableLiveData(false) + val isAccepted: LiveData = _isAccepted + + private val _okayCountDown = MutableLiveData(FULL_COUNT_DOWN_SEC) + val okayCountDown: LiveData = _okayCountDown + + fun checkIsAccepted() { + Timber.v("checkIsAccepted() called") + viewModelScope.launch { + _isAccepted.value = disclaimerStorage.isDisclaimerAccepted() + } + } + + fun acceptDisclaimer() { + Timber.v("acceptDisclaimer() called") + disclaimerStorage.acceptDisclaimer() + _isAccepted.value = true + } + + fun startCountDown() { + Timber.v("startCountDown() called") + tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS) + .take(FULL_COUNT_DOWN_SEC - COUNT_DOWN_TICK_PERIOD_SEC + 1) + .onEach { _okayCountDown.value = FULL_COUNT_DOWN_SEC - it } + .launchIn(viewModelScope) + } + + /** + * Sends an event every [period] of [timeUnit]. For example, if period = 3 and timeUnit = SECOND + * then this will send an event every 3 seconds. Additionally to the event, it sends counter + * of how many ticks there were. It doesn't depend on period or timeUnit parameters, it just + * counts how many events it sent starting from 1. + */ + @VisibleForTesting + fun tickerFlow(period: Long, timeUnit: TimeUnit) = flow { + Timber.v("tickerFlow() called with: period = $period, timeUnit = $timeUnit") + val periodMillis = timeUnit.toMillis(period) + var counter = 0 + while (true) { + delay(periodMillis) + counter++ + emit(counter) + } + } + + companion object { + private const val FULL_COUNT_DOWN_SEC = 5 + private const val COUNT_DOWN_TICK_PERIOD_SEC = 1 + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml new file mode 100644 index 0000000..3af6a60 --- /dev/null +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -0,0 +1,42 @@ + + + + + + + +