From f44f54522d01d87fd8c7495387566a093b45802b Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Mon, 4 Apr 2022 02:40:32 +0500 Subject: [PATCH] Extract Base URL from authentication --- .../mealient/data/auth/AuthDataSource.kt | 4 +- .../kirmanak/mealient/data/auth/AuthRepo.kt | 5 +- .../mealient/data/auth/AuthStorage.kt | 4 +- .../data/auth/impl/AuthDataSourceImpl.kt | 17 +++-- .../mealient/data/auth/impl/AuthRepoImpl.kt | 34 +-------- .../data/auth/impl/AuthStorageImpl.kt | 18 +---- .../data/auth/impl/AuthenticationError.kt | 8 -- .../mealient/data/baseurl/BaseURLStorage.kt | 10 +++ .../data/baseurl/BaseURLStorageImpl.kt | 23 ++++++ .../data/baseurl/VersionDataSource.kt | 9 +++ .../data/baseurl/VersionDataSourceImpl.kt | 39 ++++++++++ .../mealient/data/baseurl/VersionInfo.kt | 7 ++ .../mealient/data/baseurl/VersionResponse.kt | 14 ++++ .../mealient/data/baseurl/VersionService.kt | 8 ++ .../mealient/data/network/NetworkError.kt | 8 ++ .../data/network/RetrofitServiceFactory.kt | 27 +++---- .../mealient/data/network/ServiceFactory.kt | 2 +- .../recipes/impl/RecipeImageLoaderImpl.kt | 6 +- .../recipes/network/RecipeDataSourceImpl.kt | 4 +- .../gq/kirmanak/mealient/di/AuthModule.kt | 32 ++++---- .../gq/kirmanak/mealient/di/BaseURLModule.kt | 35 +++++++++ .../gq/kirmanak/mealient/di/RecipeModule.kt | 8 +- .../extensions/RemoteToLocalMappings.kt | 4 + .../gq/kirmanak/mealient/ui/ViewExtensions.kt | 30 ++++++++ .../ui/auth/AuthenticationFragment.kt | 54 ++----------- .../ui/auth/AuthenticationViewModel.kt | 6 +- .../mealient/ui/baseurl/BaseURLFragment.kt | 47 ++++++++++++ .../mealient/ui/baseurl/BaseURLScreenState.kt | 8 ++ .../mealient/ui/baseurl/BaseURLViewModel.kt | 46 ++++++++++++ .../ui/disclaimer/DisclaimerFragment.kt | 8 +- .../mealient/ui/recipes/RecipesFragment.kt | 19 ----- .../mealient/ui/splash/SplashViewModel.kt | 11 ++- .../res/layout/fragment_authentication.xml | 58 ++++++-------- app/src/main/res/layout/fragment_base_url.xml | 34 +++++++++ app/src/main/res/navigation/nav_graph.xml | 18 ++++- app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values/strings.xml | 8 +- .../data/auth/impl/AuthDataSourceImplTest.kt | 19 +++-- .../data/auth/impl/AuthRepoImplTest.kt | 47 ++---------- .../data/auth/impl/AuthStorageImplTest.kt | 29 ++----- .../data/baseurl/BaseURLStorageImplTest.kt | 65 ++++++++++++++++ .../data/baseurl/VersionDataSourceImplTest.kt | 71 ++++++++++++++++++ .../network/RetrofitServiceFactoryTest.kt | 68 +++++++++++++++++ .../recipes/impl/RecipeImageLoaderImplTest.kt | 18 +++-- .../recipes/impl/RecipesRemoteMediatorTest.kt | 2 +- .../ui/baseurl/BaseURLViewModelTest.kt | 75 +++++++++++++++++++ .../ui/disclaimer/DisclaimerViewModelTest.kt | 2 - 47 files changed, 760 insertions(+), 316 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthenticationError.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionResponse.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionService.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLScreenState.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt create mode 100644 app/src/main/res/layout/fragment_base_url.xml create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImplTest.kt create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt create mode 100644 app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt index e97ad2a..576ccab 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt @@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.auth interface AuthDataSource { /** - * Tries to acquire authentication token using the provided credentials on specified server. + * Tries to acquire authentication token using the provided credentials */ - suspend fun authenticate(username: String, password: String, baseUrl: String): String + suspend fun authenticate(username: String, password: String): String } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt index 1426dbe..da7e524 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthRepo.kt @@ -3,11 +3,8 @@ package gq.kirmanak.mealient.data.auth import kotlinx.coroutines.flow.Flow interface AuthRepo { - suspend fun authenticate(username: String, password: String, baseUrl: String) - suspend fun getBaseUrl(): String? - - suspend fun requireBaseUrl(): String + suspend fun authenticate(username: String, password: String) suspend fun getAuthHeader(): String? diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt index 215a5da..5bcacf5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthStorage.kt @@ -3,9 +3,7 @@ package gq.kirmanak.mealient.data.auth import kotlinx.coroutines.flow.Flow interface AuthStorage { - suspend fun storeAuthData(authHeader: String, baseUrl: String) - - suspend fun getBaseUrl(): String? + suspend fun storeAuthData(authHeader: String) suspend fun getAuthHeader(): String? diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index 0d3ddac..b6606f6 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -1,8 +1,8 @@ package gq.kirmanak.mealient.data.auth.impl import gq.kirmanak.mealient.data.auth.AuthDataSource -import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.* import gq.kirmanak.mealient.data.network.ErrorDetail +import gq.kirmanak.mealient.data.network.NetworkError.* import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull import kotlinx.coroutines.CancellationException @@ -20,13 +20,14 @@ class AuthDataSourceImpl @Inject constructor( private val json: Json, ) : AuthDataSource { - override suspend fun authenticate( - username: String, - password: String, - baseUrl: String - ): String { - Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl") - val authService = authServiceFactory.provideService(baseUrl) + override suspend fun authenticate(username: String, password: String): String { + Timber.v("authenticate() called with: username = $username, password = $password") + val authService = try { + authServiceFactory.provideService() + } catch (e: Exception) { + Timber.e(e, "authenticate: can't create Retrofit service") + throw MalformedUrl(e) + } val response = sendRequest(authService, username, password) val accessToken = parseToken(response) Timber.v("authenticate() returned: $accessToken") diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 89f0bfd..92c0d98 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -1,14 +1,10 @@ package gq.kirmanak.mealient.data.auth.impl -import android.net.Uri -import androidx.annotation.VisibleForTesting import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthStorage -import gq.kirmanak.mealient.data.auth.impl.AuthenticationError.MalformedUrl import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import okhttp3.HttpUrl.Companion.toHttpUrl import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -19,23 +15,13 @@ class AuthRepoImpl @Inject constructor( private val storage: AuthStorage, ) : AuthRepo { - override suspend fun authenticate( - username: String, - password: String, - baseUrl: String - ) { - Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl") - val url = parseBaseUrl(baseUrl) - val accessToken = dataSource.authenticate(username, password, url) + override suspend fun authenticate(username: String, password: String) { + Timber.v("authenticate() called with: username = $username, password = $password") + val accessToken = dataSource.authenticate(username, password) Timber.d("authenticate result is \"$accessToken\"") - storage.storeAuthData(AUTH_HEADER_FORMAT.format(accessToken), url) + storage.storeAuthData(AUTH_HEADER_FORMAT.format(accessToken)) } - override suspend fun getBaseUrl(): String? = storage.getBaseUrl() - - override suspend fun requireBaseUrl(): String = - checkNotNull(getBaseUrl()) { "Base URL is null when it was required" } - override suspend fun getAuthHeader(): String? = storage.getAuthHeader() override suspend fun requireAuthHeader(): String = @@ -51,18 +37,6 @@ class AuthRepoImpl @Inject constructor( storage.clearAuthData() } - @VisibleForTesting - fun parseBaseUrl(baseUrl: String): String = try { - val withScheme = Uri.parse(baseUrl).let { - if (it.scheme == null) it.buildUpon().scheme("https").build() - else it - }.toString() - withScheme.toHttpUrl().toString() - } catch (e: Throwable) { - Timber.e(e, "authenticate: can't parse base url $baseUrl") - throw MalformedUrl(e) - } - companion object { private const val AUTH_HEADER_FORMAT = "Bearer %s" } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt index 43d4618..8a5c575 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt @@ -13,20 +13,10 @@ class AuthStorageImpl @Inject constructor( ) : AuthStorage { private val authHeaderKey by preferencesStorage::authHeaderKey - private val baseUrlKey by preferencesStorage::baseUrlKey - override suspend fun storeAuthData(authHeader: String, baseUrl: String) { - Timber.v("storeAuthData() called with: authHeader = $authHeader, baseUrl = $baseUrl") - preferencesStorage.storeValues( - Pair(authHeaderKey, authHeader), - Pair(baseUrlKey, baseUrl), - ) - } - - override suspend fun getBaseUrl(): String? { - val baseUrl = preferencesStorage.getValue(baseUrlKey) - Timber.d("getBaseUrl: base url is $baseUrl") - return baseUrl + override suspend fun storeAuthData(authHeader: String) { + Timber.v("storeAuthData() called with: authHeader = $authHeader") + preferencesStorage.storeValues(Pair(authHeaderKey, authHeader)) } override suspend fun getAuthHeader(): String? { @@ -43,6 +33,6 @@ class AuthStorageImpl @Inject constructor( override suspend fun clearAuthData() { Timber.v("clearAuthData() called") - preferencesStorage.removeValues(authHeaderKey, baseUrlKey) + preferencesStorage.removeValues(authHeaderKey) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthenticationError.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthenticationError.kt deleted file mode 100644 index 740505a..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthenticationError.kt +++ /dev/null @@ -1,8 +0,0 @@ -package gq.kirmanak.mealient.data.auth.impl - -sealed class AuthenticationError(cause: Throwable) : RuntimeException(cause) { - class Unauthorized(cause: Throwable) : AuthenticationError(cause) - class NoServerConnection(cause: Throwable) : AuthenticationError(cause) - class NotMealie(cause: Throwable) : AuthenticationError(cause) - class MalformedUrl(cause: Throwable) : AuthenticationError(cause) -} diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt new file mode 100644 index 0000000..7864ea9 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt @@ -0,0 +1,10 @@ +package gq.kirmanak.mealient.data.baseurl + +interface BaseURLStorage { + + suspend fun getBaseURL(): String? + + suspend fun requireBaseURL(): String + + suspend fun storeBaseURL(baseURL: String) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt new file mode 100644 index 0000000..2f75d00 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorageImpl.kt @@ -0,0 +1,23 @@ +package gq.kirmanak.mealient.data.baseurl + +import gq.kirmanak.mealient.data.storage.PreferencesStorage +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BaseURLStorageImpl @Inject constructor( + private val preferencesStorage: PreferencesStorage, +) : BaseURLStorage { + + private val baseUrlKey by preferencesStorage::baseUrlKey + + override suspend fun getBaseURL(): String? = preferencesStorage.getValue(baseUrlKey) + + override suspend fun requireBaseURL(): String = checkNotNull(getBaseURL()) { + "Base URL was null when it was required" + } + + override suspend fun storeBaseURL(baseURL: String) { + preferencesStorage.storeValues(Pair(baseUrlKey, baseURL)) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt new file mode 100644 index 0000000..9bee6c2 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.data.baseurl + +import gq.kirmanak.mealient.data.network.NetworkError + +interface VersionDataSource { + + @Throws(NetworkError::class) + suspend fun getVersionInfo(baseUrl: String): VersionInfo +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt new file mode 100644 index 0000000..6e25370 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt @@ -0,0 +1,39 @@ +package gq.kirmanak.mealient.data.baseurl + +import gq.kirmanak.mealient.data.network.NetworkError +import gq.kirmanak.mealient.data.network.ServiceFactory +import gq.kirmanak.mealient.extensions.versionInfo +import kotlinx.serialization.SerializationException +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class VersionDataSourceImpl @Inject constructor( + private val serviceFactory: ServiceFactory, +) : VersionDataSource { + + override suspend fun getVersionInfo(baseUrl: String): VersionInfo { + Timber.v("getVersionInfo() called with: baseUrl = $baseUrl") + + val service = try { + serviceFactory.provideService(baseUrl) + } catch (e: Exception) { + Timber.e(e, "getVersionInfo: can't create service") + throw NetworkError.MalformedUrl(e) + } + + val response = try { + service.getVersion() + } catch (e: Exception) { + Timber.e(e, "getVersionInfo: can't request version") + when (e) { + is HttpException, is SerializationException -> throw NetworkError.NotMealie(e) + else -> throw NetworkError.NoServerConnection(e) + } + } + + return response.versionInfo() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt new file mode 100644 index 0000000..d706d60 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt @@ -0,0 +1,7 @@ +package gq.kirmanak.mealient.data.baseurl + +data class VersionInfo( + val production: Boolean, + val version: String, + val demoStatus: Boolean, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionResponse.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionResponse.kt new file mode 100644 index 0000000..9529415 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionResponse.kt @@ -0,0 +1,14 @@ +package gq.kirmanak.mealient.data.baseurl + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VersionResponse( + @SerialName("production") + val production: Boolean, + @SerialName("version") + val version: String, + @SerialName("demoStatus") + val demoStatus: Boolean, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionService.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionService.kt new file mode 100644 index 0000000..0271550 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionService.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.data.baseurl + +import retrofit2.http.GET + +interface VersionService { + @GET("api/debug/version") + suspend fun getVersion(): VersionResponse +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt new file mode 100644 index 0000000..05ef278 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.data.network + +sealed class NetworkError(cause: Throwable) : RuntimeException(cause) { + class Unauthorized(cause: Throwable) : NetworkError(cause) + class NoServerConnection(cause: Throwable) : NetworkError(cause) + class NotMealie(cause: Throwable) : NetworkError(cause) + class MalformedUrl(cause: Throwable) : NetworkError(cause) +} diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt index b08ed87..7b99bac 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt @@ -1,28 +1,29 @@ package gq.kirmanak.mealient.data.network +import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import timber.log.Timber -inline fun RetrofitBuilder.createServiceFactory() = - RetrofitServiceFactory(T::class.java, this) +inline fun RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) = + RetrofitServiceFactory(T::class.java, this, baseURLStorage) class RetrofitServiceFactory( private val serviceClass: Class, private val retrofitBuilder: RetrofitBuilder, + private val baseURLStorage: BaseURLStorage, ) : ServiceFactory { private val cache: MutableMap = mutableMapOf() - @Synchronized - override fun provideService(baseUrl: String): T { + override suspend fun provideService(baseUrl: String?): T { Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}") - val cached = cache[baseUrl] - return if (cached == null) { - Timber.d("provideService: cache is empty, creating new") - val new = retrofitBuilder.buildRetrofit(baseUrl).create(serviceClass) - cache[baseUrl] = new - new - } else { - cached - } + val url = baseUrl ?: baseURLStorage.requireBaseURL() + return synchronized(cache) { cache[url] ?: createService(url, serviceClass) } + } + + private fun createService(url: String, serviceClass: Class): T { + Timber.v("createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}") + val service = retrofitBuilder.buildRetrofit(url).create(serviceClass) + cache[url] = service + return service } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt index 1ae70d8..e46c2a8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt @@ -2,5 +2,5 @@ package gq.kirmanak.mealient.data.network interface ServiceFactory { - fun provideService(baseUrl: String): T + suspend fun provideService(baseUrl: String? = null): T } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt index b83de42..97f31a8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageLoaderImpl.kt @@ -3,7 +3,7 @@ package gq.kirmanak.mealient.data.recipes.impl import android.widget.ImageView import androidx.annotation.VisibleForTesting import gq.kirmanak.mealient.R -import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.recipes.RecipeImageLoader import gq.kirmanak.mealient.ui.ImageLoader import okhttp3.HttpUrl.Companion.toHttpUrlOrNull @@ -14,7 +14,7 @@ import javax.inject.Singleton @Singleton class RecipeImageLoaderImpl @Inject constructor( private val imageLoader: ImageLoader, - private val authRepo: AuthRepo + private val baseURLStorage: BaseURLStorage, ): RecipeImageLoader { override suspend fun loadRecipeImage(view: ImageView, slug: String?) { @@ -25,7 +25,7 @@ class RecipeImageLoaderImpl @Inject constructor( @VisibleForTesting suspend fun generateImageUrl(slug: String?): String? { Timber.v("generateImageUrl() called with: slug = $slug") - val result = authRepo.getBaseUrl() + val result = baseURLStorage.getBaseURL() ?.takeIf { it.isNotBlank() } ?.takeUnless { slug.isNullOrBlank() } ?.toHttpUrlOrNull() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt index 05cd801..e6b0f0a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt @@ -30,8 +30,8 @@ class RecipeDataSourceImpl @Inject constructor( private suspend fun getRecipeService(): RecipeService { Timber.v("getRecipeService() called") - return recipeServiceFactory.provideService(authRepo.requireBaseUrl()) + return recipeServiceFactory.provideService() } - private suspend fun getToken(): String = authRepo.requireAuthHeader() + private suspend fun getToken(): String? = authRepo.getAuthHeader() } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt index 2ddd619..18e247a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt @@ -12,6 +12,7 @@ import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl import gq.kirmanak.mealient.data.auth.impl.AuthService import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl +import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory @@ -21,24 +22,25 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) interface AuthModule { - companion object { + companion object { - @Provides - @Singleton - fun provideAuthServiceFactory(retrofitBuilder: RetrofitBuilder): ServiceFactory { - return retrofitBuilder.createServiceFactory() + @Provides + @Singleton + fun provideAuthServiceFactory( + retrofitBuilder: RetrofitBuilder, + baseURLStorage: BaseURLStorage, + ): ServiceFactory = retrofitBuilder.createServiceFactory(baseURLStorage) } - } - @Binds - @Singleton - fun bindAuthDataSource(authDataSourceImpl: AuthDataSourceImpl): AuthDataSource + @Binds + @Singleton + fun bindAuthDataSource(authDataSourceImpl: AuthDataSourceImpl): AuthDataSource - @Binds - @Singleton - fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage + @Binds + @Singleton + fun bindAuthStorage(authStorageImpl: AuthStorageImpl): AuthStorage - @Binds - @Singleton - fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo + @Binds + @Singleton + fun bindAuthRepo(authRepo: AuthRepoImpl): AuthRepo } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt new file mode 100644 index 0000000..0e5820a --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -0,0 +1,35 @@ +package gq.kirmanak.mealient.di + +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.data.baseurl.* +import gq.kirmanak.mealient.data.network.RetrofitBuilder +import gq.kirmanak.mealient.data.network.ServiceFactory +import gq.kirmanak.mealient.data.network.createServiceFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface BaseURLModule { + + companion object { + + @Provides + @Singleton + fun provideVersionServiceFactory( + retrofitBuilder: RetrofitBuilder, + baseURLStorage: BaseURLStorage, + ): ServiceFactory = retrofitBuilder.createServiceFactory(baseURLStorage) + } + + @Binds + @Singleton + fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource + + @Binds + @Singleton + fun bindBaseUrlStorage(baseURLStorageImpl: BaseURLStorageImpl): BaseURLStorage +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 21699a6..790d812 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -6,6 +6,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory @@ -44,9 +45,10 @@ interface RecipeModule { @Provides @Singleton - fun provideRecipeServiceFactory(retrofitBuilder: RetrofitBuilder): ServiceFactory { - return retrofitBuilder.createServiceFactory() - } + fun provideRecipeServiceFactory( + retrofitBuilder: RetrofitBuilder, + baseURLStorage: BaseURLStorage, + ): ServiceFactory = retrofitBuilder.createServiceFactory(baseURLStorage) @Provides @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt index 8a0bafa..394ff01 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt @@ -1,5 +1,7 @@ package gq.kirmanak.mealient.extensions +import gq.kirmanak.mealient.data.baseurl.VersionInfo +import gq.kirmanak.mealient.data.baseurl.VersionResponse import gq.kirmanak.mealient.data.recipes.db.entity.RecipeEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeIngredientEntity import gq.kirmanak.mealient.data.recipes.db.entity.RecipeInstructionEntity @@ -42,3 +44,5 @@ fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity( dateAdded = dateAdded, dateUpdated = dateUpdated, ) + +fun VersionResponse.versionInfo() = VersionInfo(production, version, demoStatus) \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt index 035ccc4..1b608d0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/ViewExtensions.kt @@ -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 ChannelResult.logErrors(methodName: String): ChannelResult { 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") } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index e95d28a..b182ba2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -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") - } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index f630531..ac8fedd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -14,12 +14,12 @@ class AuthenticationViewModel @Inject constructor( private val recipeRepo: RecipeRepo ) : ViewModel() { - fun authenticate(username: String, password: String, baseUrl: String): LiveData> { - Timber.v("authenticate() called with: username = $username, password = $password, baseUrl = $baseUrl") + fun authenticate(username: String, password: String): LiveData> { + Timber.v("authenticate() called with: username = $username, password = $password") val result = MutableLiveData>() 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) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt new file mode 100644 index 0000000..892e260 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -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() + + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLScreenState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLScreenState.kt new file mode 100644 index 0000000..bc6bf65 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLScreenState.kt @@ -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, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt new file mode 100644 index 0000000..d8d9227 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -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 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) + } +} \ 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 index 585e9f8..cf20d3b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -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?) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index f4df870..57d894f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -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() - private val authViewModel by viewModels() - private val authStatuses by lazy { authViewModel.authenticationStatuses() } - private val authStatusObserver = Observer { 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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt index 95a91f2..c845740 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt @@ -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() val nextDestination: LiveData = _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() } diff --git a/app/src/main/res/layout/fragment_authentication.xml b/app/src/main/res/layout/fragment_authentication.xml index 312f448..8355ea2 100644 --- a/app/src/main/res/layout/fragment_authentication.xml +++ b/app/src/main/res/layout/fragment_authentication.xml @@ -25,44 +25,28 @@ + android:id="@+id/password_input_layout" + style="@style/SmallMarginTextInputLayoutStyle" + android:hint="@string/fragment_authentication_input_hint_password" + app:layout_constraintBottom_toTopOf="@+id/button" + app:layout_constraintEnd_toEndOf="parent" + app:endIconMode="password_toggle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/email_input_layout"> - + - - - - - -