Add disclaimer fragment

This commit is contained in:
Kirill Kamakin
2021-11-20 18:43:33 +03:00
parent 071ce453e2
commit 4b817ba404
10 changed files with 331 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
package gq.kirmanak.mealient.data.disclaimer
interface DisclaimerStorage {
suspend fun isDisclaimerAccepted(): Boolean
fun acceptDisclaimer()
}

View File

@@ -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()
}
}

View File

@@ -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<DisclaimerViewModel>()
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
}
}

View File

@@ -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
}

View File

@@ -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<Boolean> = _isAccepted
private val _okayCountDown = MutableLiveData(FULL_COUNT_DOWN_SEC)
val okayCountDown: LiveData<Int> = _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
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="ui.disclaimer.DisclaimerFragment">
<TextView
android:id="@+id/header"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:gravity="center"
android:text="@string/fragment_disclaimer_header"
android:textAppearance="?textAppearanceHeadline3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/main_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:text="@string/fragment_disclaimer_main_text"
android:textAppearance="?textAppearanceHeadline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/header" />
<Button
android:id="@+id/okay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:clickable="false"
android:text="@string/fragment_disclaimer_button_okay_timer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -3,12 +3,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/authenticationFragment">
app:startDestination="@id/disclaimerFragment">
<fragment
android:id="@+id/authenticationFragment"
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
android:label="AuthenticationFragment">
android:label="AuthenticationFragment"
tools:layout="@layout/fragment_authentication">
<action
android:id="@+id/action_authenticationFragment_to_recipesFragment"
app:destination="@id/recipesFragment"
@@ -32,7 +33,8 @@
<fragment
android:id="@+id/recipeInfoFragment"
android:name="gq.kirmanak.mealient.ui.recipes.info.RecipeInfoFragment"
android:label="RecipeInfoFragment">
android:label="RecipeInfoFragment"
tools:layout="@layout/fragment_recipe_info">
<action
android:id="@+id/action_recipeInfoFragment_to_authenticationFragment"
app:destination="@id/authenticationFragment"
@@ -45,4 +47,15 @@
android:name="recipe_id"
app:argType="long" />
</fragment>
<fragment
android:id="@+id/disclaimerFragment"
android:name="gq.kirmanak.mealient.ui.disclaimer.DisclaimerFragment"
android:label="DisclaimerFragment"
tools:layout="@layout/fragment_disclaimer">
<action
android:id="@+id/action_disclaimerFragment_to_authenticationFragment"
app:destination="@id/authenticationFragment"
app:popUpTo="@id/nav_graph"
app:popUpToInclusive="true" />
</fragment>
</navigation>

View File

@@ -11,4 +11,8 @@
<string name="content_description_fragment_recipe_info_image">@string/content_description_view_holder_recipe_image</string>
<string name="fragment_recipe_info_ingredients_header">Ingredients</string>
<string name="fragment_recipe_info_instructions_header">Instructions</string>
<string name="fragment_disclaimer_button_okay_timer">OKAY (%d seconds)</string>
<string name="fragment_disclaimer_main_text">This project is NOT associated with the core Mealie developers. If you face any issues when using the application or have a feature request DO NOT send them to the Mealie developers. Instead, report a new issue in the Mealient repository. THAT IS NOT AN OFFICIAL CLIENT.</string>
<string name="fragment_disclaimer_header">DISCLAIMER</string>
<string name="fragment_disclaimer_button_okay">OKAY</string>
</resources>

View File

@@ -0,0 +1,26 @@
package gq.kirmanak.mealient.data.disclaimer
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.test.HiltRobolectricTest
import kotlinx.coroutines.runBlocking
import org.junit.Test
import javax.inject.Inject
@HiltAndroidTest
class DisclaimerStorageImplTest : HiltRobolectricTest() {
@Inject
lateinit var subject: DisclaimerStorageImpl
@Test
fun `when isDisclaimerAccepted initially then false`(): Unit = runBlocking {
assertThat(subject.isDisclaimerAccepted()).isFalse()
}
@Test
fun `when isDisclaimerAccepted after accept then true`(): Unit = runBlocking {
subject.acceptDisclaimer()
assertThat(subject.isDisclaimerAccepted()).isTrue()
}
}

View File

@@ -0,0 +1,42 @@
package gq.kirmanak.mealient.ui.disclaimer
import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import gq.kirmanak.mealient.data.test.HiltRobolectricTest
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Test
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ExperimentalCoroutinesApi
@HiltAndroidTest
class DisclaimerViewModelTest : HiltRobolectricTest() {
@Inject
lateinit var storage: DisclaimerStorage
lateinit var subject: DisclaimerViewModel
@Before
fun setUp() {
subject = DisclaimerViewModel(storage)
}
@Test
fun `when tickerFlow 3 seconds then sends count every 3 seconds`() = runBlockingTest() {
subject.tickerFlow(3, TimeUnit.SECONDS).take(10).collect {
assertThat(it * 3000).isEqualTo(currentTime)
}
}
@Test
fun `when tickerFlow 500 ms then sends count every 500 ms`() = runBlockingTest() {
subject.tickerFlow(500, TimeUnit.MILLISECONDS).take(10).collect {
assertThat(it * 500).isEqualTo(currentTime)
}
}
}