Extract Base URL from authentication
This commit is contained in:
@@ -4,13 +4,16 @@ import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.ChannelResult
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
@@ -18,6 +21,8 @@ import kotlinx.coroutines.channels.onClosed
|
||||
import kotlinx.coroutines.channels.onFailure
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@@ -79,4 +84,29 @@ fun <T> ChannelResult<T>.logErrors(methodName: String): ChannelResult<T> {
|
||||
onFailure { Timber.e(it, "$methodName: can't send event") }
|
||||
onClosed { Timber.e(it, "$methodName: flow has been closed") }
|
||||
return this
|
||||
}
|
||||
|
||||
fun EditText.checkIfInputIsEmpty(
|
||||
inputLayout: TextInputLayout,
|
||||
lifecycleCoroutineScope: LifecycleCoroutineScope,
|
||||
errorText: () -> String
|
||||
): String? {
|
||||
Timber.v("checkIfInputIsEmpty() called with: input = $this, inputLayout = $inputLayout, errorText = $errorText")
|
||||
val text = text?.toString()
|
||||
Timber.d("Input text is \"$text\"")
|
||||
if (text.isNullOrEmpty()) {
|
||||
inputLayout.error = errorText()
|
||||
lifecycleCoroutineScope.launchWhenResumed {
|
||||
waitUntilNotEmpty()
|
||||
inputLayout.error = null
|
||||
}
|
||||
return null
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
suspend fun EditText.waitUntilNotEmpty() {
|
||||
Timber.v("waitUntilNotEmpty() called with: input = $this")
|
||||
textChangesFlow().filterNotNull().first { it.isNotEmpty() }
|
||||
Timber.v("waitUntilNotEmpty() returned")
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.ui.auth
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
@@ -10,14 +9,11 @@ import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.*
|
||||
import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
|
||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
||||
import gq.kirmanak.mealient.ui.textChangesFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import gq.kirmanak.mealient.ui.checkIfInputIsEmpty
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -57,61 +53,23 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
||||
private fun onLoginClicked(): Unit = with(binding) {
|
||||
Timber.v("onLoginClicked() called")
|
||||
|
||||
val email: String = checkIfInputIsEmpty(emailInput, emailInputLayout) {
|
||||
val email: String = emailInput.checkIfInputIsEmpty(emailInputLayout, lifecycleScope) {
|
||||
getString(R.string.fragment_authentication_email_input_empty)
|
||||
} ?: return
|
||||
|
||||
val pass: String = checkIfInputIsEmpty(passwordInput, passwordInputLayout) {
|
||||
val pass: String = passwordInput.checkIfInputIsEmpty(passwordInputLayout, lifecycleScope) {
|
||||
getString(R.string.fragment_authentication_password_input_empty)
|
||||
} ?: return
|
||||
|
||||
val url: String = checkIfInputIsEmpty(urlInput, urlInputLayout) {
|
||||
getString(R.string.fragment_authentication_url_input_empty)
|
||||
} ?: return
|
||||
|
||||
button.isClickable = false
|
||||
viewModel.authenticate(email, pass, url).observe(viewLifecycleOwner) {
|
||||
viewModel.authenticate(email, pass).observe(viewLifecycleOwner) {
|
||||
Timber.d("onLoginClicked: result $it")
|
||||
passwordInputLayout.error = when (it.exceptionOrNull()) {
|
||||
is Unauthorized -> getString(R.string.fragment_authentication_credentials_incorrect)
|
||||
else -> null
|
||||
}
|
||||
urlInputLayout.error = when (val exception = it.exceptionOrNull()) {
|
||||
is NoServerConnection -> getString(R.string.fragment_authentication_no_connection)
|
||||
is NotMealie -> getString(R.string.fragment_authentication_unexpected_response)
|
||||
is MalformedUrl -> {
|
||||
val cause = exception.cause?.message ?: exception.message
|
||||
getString(R.string.fragment_authentication_url_invalid, cause)
|
||||
}
|
||||
is Unauthorized, null -> null
|
||||
else -> getString(R.string.fragment_authentication_unknown_error)
|
||||
}
|
||||
|
||||
button.isClickable = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkIfInputIsEmpty(
|
||||
input: EditText,
|
||||
inputLayout: TextInputLayout,
|
||||
errorText: () -> String
|
||||
): String? {
|
||||
Timber.v("checkIfInputIsEmpty() called with: input = $input, inputLayout = $inputLayout, errorText = $errorText")
|
||||
val text = input.text?.toString()
|
||||
Timber.d("Input text is \"$text\"")
|
||||
if (text.isNullOrEmpty()) {
|
||||
inputLayout.error = errorText()
|
||||
viewLifecycleOwner.lifecycleScope.launchWhenResumed {
|
||||
waitUntilNotEmpty(input)
|
||||
inputLayout.error = null
|
||||
}
|
||||
return null
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private suspend fun waitUntilNotEmpty(input: EditText) {
|
||||
Timber.v("waitUntilNotEmpty() called with: input = $input")
|
||||
input.textChangesFlow().filterNotNull().first { it.isNotEmpty() }
|
||||
Timber.v("waitUntilNotEmpty() returned")
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ class AuthenticationViewModel @Inject constructor(
|
||||
private val recipeRepo: RecipeRepo
|
||||
) : ViewModel() {
|
||||
|
||||
fun authenticate(username: String, password: String, baseUrl: String): LiveData<Result<Unit>> {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl")
|
||||
fun authenticate(username: String, password: String): LiveData<Result<Unit>> {
|
||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
||||
val result = MutableLiveData<Result<Unit>>()
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
authRepo.authenticate(username, password, baseUrl)
|
||||
authRepo.authenticate(username, password)
|
||||
}.onFailure {
|
||||
Timber.e(it, "authenticate: can't authenticate")
|
||||
result.value = Result.failure(it)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package gq.kirmanak.mealient.ui.baseurl
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||
|
||||
private val binding by viewBinding(FragmentBaseUrlBinding::bind)
|
||||
private val viewModel by viewModels<BaseURLViewModel>()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
viewModel.screenState.observe(viewLifecycleOwner, ::updateState)
|
||||
binding.button.setOnClickListener {
|
||||
viewModel.saveBaseUrl(binding.urlInput.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateState(baseURLScreenState: BaseURLScreenState) {
|
||||
Timber.v("updateState() called with: baseURLScreenState = $baseURLScreenState")
|
||||
if (baseURLScreenState.navigateNext) {
|
||||
findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment())
|
||||
return
|
||||
}
|
||||
binding.urlInputLayout.error = when (val exception = baseURLScreenState.error) {
|
||||
is NetworkError.NoServerConnection -> getString(R.string.fragment_base_url_no_connection)
|
||||
is NetworkError.NotMealie -> getString(R.string.fragment_base_url_unexpected_response)
|
||||
is NetworkError.MalformedUrl -> {
|
||||
val cause = exception.cause?.message ?: exception.message
|
||||
getString(R.string.fragment_base_url_malformed_url, cause)
|
||||
}
|
||||
null -> null
|
||||
else -> getString(R.string.fragment_base_url_unknown_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package gq.kirmanak.mealient.ui.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
|
||||
data class BaseURLScreenState(
|
||||
val error: NetworkError? = null,
|
||||
val navigateNext: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
package gq.kirmanak.mealient.ui.baseurl
|
||||
|
||||
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.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class BaseURLViewModel @Inject constructor(
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
private val versionDataSource: VersionDataSource,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _screenState = MutableLiveData(BaseURLScreenState())
|
||||
var currentScreenState: BaseURLScreenState
|
||||
get() = _screenState.value!!
|
||||
private set(value) {
|
||||
_screenState.value = value
|
||||
}
|
||||
val screenState: LiveData<BaseURLScreenState> by ::_screenState
|
||||
|
||||
fun saveBaseUrl(baseURL: String) {
|
||||
Timber.v("saveBaseUrl() called with: baseURL = $baseURL")
|
||||
viewModelScope.launch { checkBaseURL(baseURL) }
|
||||
}
|
||||
|
||||
private suspend fun checkBaseURL(baseURL: String) {
|
||||
val version = try {
|
||||
versionDataSource.getVersionInfo(baseURL)
|
||||
} catch (e: NetworkError) {
|
||||
Timber.e(e, "checkBaseURL: can't get version info")
|
||||
currentScreenState = BaseURLScreenState(e, false)
|
||||
return
|
||||
}
|
||||
Timber.d("checkBaseURL: version is $version")
|
||||
baseURLStorage.storeBaseURL(baseURL)
|
||||
currentScreenState = BaseURLScreenState(null, true)
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,14 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
|
||||
Timber.v("listenToAcceptStatus() called")
|
||||
viewModel.isAccepted.observe(this) {
|
||||
Timber.d("listenToAcceptStatus: new status = $it")
|
||||
if (it) navigateToAuth()
|
||||
if (it) navigateNext()
|
||||
}
|
||||
viewModel.checkIsAccepted()
|
||||
}
|
||||
|
||||
private fun navigateToAuth() {
|
||||
Timber.v("navigateToAuth() called")
|
||||
findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToAuthenticationFragment())
|
||||
private fun navigateNext() {
|
||||
Timber.v("navigateNext() called")
|
||||
findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment())
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
@@ -13,7 +12,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.recipes.db.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||
import gq.kirmanak.mealient.ui.auth.AuthenticationViewModel
|
||||
import gq.kirmanak.mealient.ui.refreshesLiveData
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import timber.log.Timber
|
||||
@@ -23,22 +21,10 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||
private val binding by viewBinding(FragmentRecipesBinding::bind)
|
||||
private val viewModel by viewModels<RecipeViewModel>()
|
||||
|
||||
private val authViewModel by viewModels<AuthenticationViewModel>()
|
||||
private val authStatuses by lazy { authViewModel.authenticationStatuses() }
|
||||
private val authStatusObserver = Observer<Boolean> { onAuthStatusChange(it) }
|
||||
private fun onAuthStatusChange(isAuthenticated: Boolean) {
|
||||
Timber.v("onAuthStatusChange() called with: isAuthenticated = $isAuthenticated")
|
||||
if (!isAuthenticated) {
|
||||
authStatuses.removeObserver(authStatusObserver)
|
||||
navigateToAuthFragment()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
||||
setupRecipeAdapter()
|
||||
authStatuses.observe(viewLifecycleOwner, authStatusObserver)
|
||||
(requireActivity() as? AppCompatActivity)?.supportActionBar?.title = null
|
||||
}
|
||||
|
||||
@@ -52,11 +38,6 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToAuthFragment() {
|
||||
Timber.v("navigateToAuthFragment() called")
|
||||
findNavController().navigate(RecipesFragmentDirections.actionRecipesFragmentToAuthenticationFragment())
|
||||
}
|
||||
|
||||
private fun setupRecipeAdapter() {
|
||||
Timber.v("setupRecipeAdapter() called")
|
||||
binding.recipes.adapter = viewModel.adapter
|
||||
|
||||
@@ -6,17 +6,16 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavDirections
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SplashViewModel @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
private val disclaimerStorage: DisclaimerStorage
|
||||
private val disclaimerStorage: DisclaimerStorage,
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
) : ViewModel() {
|
||||
private val _nextDestination = MutableLiveData<NavDirections>()
|
||||
val nextDestination: LiveData<NavDirections> = _nextDestination
|
||||
@@ -26,8 +25,8 @@ class SplashViewModel @Inject constructor(
|
||||
delay(1000)
|
||||
_nextDestination.value = if (!disclaimerStorage.isDisclaimerAccepted())
|
||||
SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment()
|
||||
else if (!authRepo.authenticationStatuses().first())
|
||||
SplashFragmentDirections.actionSplashFragmentToAuthenticationFragment()
|
||||
else if (baseURLStorage.getBaseURL() == null)
|
||||
SplashFragmentDirections.actionSplashFragmentToBaseURLFragment()
|
||||
else
|
||||
SplashFragmentDirections.actionSplashFragmentToRecipesFragment()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user