Add disclaimer fragment
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
package gq.kirmanak.mealient.data.disclaimer
|
||||||
|
|
||||||
|
interface DisclaimerStorage {
|
||||||
|
suspend fun isDisclaimerAccepted(): Boolean
|
||||||
|
|
||||||
|
fun acceptDisclaimer()
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/src/main/res/layout/fragment_disclaimer.xml
Normal file
42
app/src/main/res/layout/fragment_disclaimer.xml
Normal 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>
|
||||||
@@ -3,12 +3,13 @@
|
|||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/nav_graph"
|
android:id="@+id/nav_graph"
|
||||||
app:startDestination="@id/authenticationFragment">
|
app:startDestination="@id/disclaimerFragment">
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/authenticationFragment"
|
android:id="@+id/authenticationFragment"
|
||||||
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
|
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
|
||||||
android:label="AuthenticationFragment">
|
android:label="AuthenticationFragment"
|
||||||
|
tools:layout="@layout/fragment_authentication">
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_authenticationFragment_to_recipesFragment"
|
android:id="@+id/action_authenticationFragment_to_recipesFragment"
|
||||||
app:destination="@id/recipesFragment"
|
app:destination="@id/recipesFragment"
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/recipeInfoFragment"
|
android:id="@+id/recipeInfoFragment"
|
||||||
android:name="gq.kirmanak.mealient.ui.recipes.info.RecipeInfoFragment"
|
android:name="gq.kirmanak.mealient.ui.recipes.info.RecipeInfoFragment"
|
||||||
android:label="RecipeInfoFragment">
|
android:label="RecipeInfoFragment"
|
||||||
|
tools:layout="@layout/fragment_recipe_info">
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_recipeInfoFragment_to_authenticationFragment"
|
android:id="@+id/action_recipeInfoFragment_to_authenticationFragment"
|
||||||
app:destination="@id/authenticationFragment"
|
app:destination="@id/authenticationFragment"
|
||||||
@@ -45,4 +47,15 @@
|
|||||||
android:name="recipe_id"
|
android:name="recipe_id"
|
||||||
app:argType="long" />
|
app:argType="long" />
|
||||||
</fragment>
|
</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>
|
</navigation>
|
||||||
@@ -11,4 +11,8 @@
|
|||||||
<string name="content_description_fragment_recipe_info_image">@string/content_description_view_holder_recipe_image</string>
|
<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_ingredients_header">Ingredients</string>
|
||||||
<string name="fragment_recipe_info_instructions_header">Instructions</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>
|
</resources>
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user