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