Merge pull request #68 from kirmanak/datastore
Extract datastore and logging modules
This commit is contained in:
@@ -1,9 +1,5 @@
|
|||||||
@file:Suppress("UnstableApiUsage")
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
import com.google.protobuf.gradle.builtins
|
|
||||||
import com.google.protobuf.gradle.generateProtoTasks
|
|
||||||
import com.google.protobuf.gradle.protobuf
|
|
||||||
import com.google.protobuf.gradle.protoc
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@@ -16,7 +12,6 @@ plugins {
|
|||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
id("com.google.firebase.crashlytics")
|
id("com.google.firebase.crashlytics")
|
||||||
alias(libs.plugins.appsweep)
|
alias(libs.plugins.appsweep)
|
||||||
alias(libs.plugins.protobuf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -72,6 +67,8 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
|
implementation(project(":datastore"))
|
||||||
|
implementation(project(":logging"))
|
||||||
|
|
||||||
implementation(libs.android.material.material)
|
implementation(libs.android.material.material)
|
||||||
|
|
||||||
@@ -105,8 +102,6 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.jetbrains.kotlinx.serialization)
|
implementation(libs.jetbrains.kotlinx.serialization)
|
||||||
|
|
||||||
implementation(libs.jakewharton.timber)
|
|
||||||
|
|
||||||
implementation(libs.androidx.paging.runtimeKtx)
|
implementation(libs.androidx.paging.runtimeKtx)
|
||||||
testImplementation(libs.androidx.paging.commonKtx)
|
testImplementation(libs.androidx.paging.commonKtx)
|
||||||
|
|
||||||
@@ -124,11 +119,6 @@ dependencies {
|
|||||||
implementation(libs.kirich1409.viewBinding)
|
implementation(libs.kirich1409.viewBinding)
|
||||||
|
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(libs.androidx.datastore.datastore)
|
|
||||||
|
|
||||||
implementation(libs.google.protobuf.javalite)
|
|
||||||
|
|
||||||
implementation(libs.androidx.security.crypto)
|
|
||||||
|
|
||||||
implementation(platform(libs.google.firebase.bom))
|
implementation(platform(libs.google.firebase.bom))
|
||||||
implementation(libs.google.firebase.analyticsKtx)
|
implementation(libs.google.firebase.analyticsKtx)
|
||||||
@@ -150,24 +140,4 @@ dependencies {
|
|||||||
debugImplementation(libs.squareup.leakcanary)
|
debugImplementation(libs.squareup.leakcanary)
|
||||||
|
|
||||||
debugImplementation(libs.chuckerteam.chucker)
|
debugImplementation(libs.chuckerteam.chucker)
|
||||||
}
|
|
||||||
|
|
||||||
protobuf {
|
|
||||||
protoc {
|
|
||||||
artifact = libs.google.protobuf.protoc.get().toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
generateProtoTasks {
|
|
||||||
all().forEach { task ->
|
|
||||||
task.builtins {
|
|
||||||
val java by registering {
|
|
||||||
option("lite")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kapt {
|
|
||||||
correctErrorTypes = true
|
|
||||||
}
|
}
|
||||||
@@ -2,14 +2,17 @@ package gq.kirmanak.mealient
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class App : Application() {
|
class App : Application() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Timber.plant(Timber.DebugTree())
|
logger.v { "onCreate() called" }
|
||||||
Timber.v("onCreate() called")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import gq.kirmanak.mealient.BuildConfig
|
import gq.kirmanak.mealient.BuildConfig
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -22,8 +22,8 @@ object DebugModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@IntoSet
|
@IntoSet
|
||||||
fun provideLoggingInterceptor(): Interceptor {
|
fun provideLoggingInterceptor(logger: Logger): Interceptor {
|
||||||
val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) }
|
val interceptor = HttpLoggingInterceptor { message -> logger.v(tag = "OkHttp") { message } }
|
||||||
interceptor.level = when {
|
interceptor.level = when {
|
||||||
BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY
|
BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY
|
||||||
else -> HttpLoggingInterceptor.Level.BASIC
|
else -> HttpLoggingInterceptor.Level.BASIC
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package gq.kirmanak.mealient.data.add
|
|
||||||
|
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface AddRecipeStorage {
|
|
||||||
|
|
||||||
val updates: Flow<AddRecipeRequest>
|
|
||||||
|
|
||||||
suspend fun save(addRecipeRequest: AddRecipeRequest)
|
|
||||||
|
|
||||||
suspend fun clear()
|
|
||||||
}
|
|
||||||
@@ -4,22 +4,25 @@ import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
|||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AddRecipeDataSourceImpl @Inject constructor(
|
class AddRecipeDataSourceImpl @Inject constructor(
|
||||||
private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>,
|
private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>,
|
||||||
|
private val logger: Logger,
|
||||||
) : AddRecipeDataSource {
|
) : AddRecipeDataSource {
|
||||||
|
|
||||||
override suspend fun addRecipe(recipe: AddRecipeRequest): String {
|
override suspend fun addRecipe(recipe: AddRecipeRequest): String {
|
||||||
Timber.v("addRecipe() called with: recipe = $recipe")
|
logger.v { "addRecipe() called with: recipe = $recipe" }
|
||||||
val service = addRecipeServiceFactory.provideService()
|
val service = addRecipeServiceFactory.provideService()
|
||||||
val response = logAndMapErrors(
|
val response = logAndMapErrors(
|
||||||
block = { service.addRecipe(recipe) }, logProvider = { "addRecipe: can't add recipe" }
|
logger,
|
||||||
|
block = { service.addRecipe(recipe) },
|
||||||
|
logProvider = { "addRecipe: can't add recipe" }
|
||||||
)
|
)
|
||||||
Timber.v("addRecipe() response = $response")
|
logger.v { "addRecipe() response = $response" }
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,16 @@ package gq.kirmanak.mealient.data.add.impl
|
|||||||
|
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeStorage
|
import gq.kirmanak.mealient.data.add.models.AddRecipeIngredient
|
||||||
|
import gq.kirmanak.mealient.data.add.models.AddRecipeInstruction
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||||
|
import gq.kirmanak.mealient.data.add.models.AddRecipeSettings
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -14,23 +19,45 @@ import javax.inject.Singleton
|
|||||||
class AddRecipeRepoImpl @Inject constructor(
|
class AddRecipeRepoImpl @Inject constructor(
|
||||||
private val addRecipeDataSource: AddRecipeDataSource,
|
private val addRecipeDataSource: AddRecipeDataSource,
|
||||||
private val addRecipeStorage: AddRecipeStorage,
|
private val addRecipeStorage: AddRecipeStorage,
|
||||||
|
private val logger: Logger,
|
||||||
) : AddRecipeRepo {
|
) : AddRecipeRepo {
|
||||||
|
|
||||||
override val addRecipeRequestFlow: Flow<AddRecipeRequest>
|
override val addRecipeRequestFlow: Flow<AddRecipeRequest>
|
||||||
get() = addRecipeStorage.updates
|
get() = addRecipeStorage.updates.map { it ->
|
||||||
|
AddRecipeRequest(
|
||||||
|
name = it.recipeName,
|
||||||
|
description = it.recipeDescription,
|
||||||
|
recipeYield = it.recipeYield,
|
||||||
|
recipeIngredient = it.recipeIngredients.map { AddRecipeIngredient(note = it) },
|
||||||
|
recipeInstructions = it.recipeInstructions.map { AddRecipeInstruction(text = it) },
|
||||||
|
settings = AddRecipeSettings(
|
||||||
|
public = it.isRecipePublic,
|
||||||
|
disableComments = it.areCommentsDisabled,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun preserve(recipe: AddRecipeRequest) {
|
override suspend fun preserve(recipe: AddRecipeRequest) {
|
||||||
Timber.v("preserveRecipe() called with: recipe = $recipe")
|
logger.v { "preserveRecipe() called with: recipe = $recipe" }
|
||||||
addRecipeStorage.save(recipe)
|
val input = AddRecipeDraft(
|
||||||
|
recipeName = recipe.name,
|
||||||
|
recipeDescription = recipe.description,
|
||||||
|
recipeYield = recipe.recipeYield,
|
||||||
|
recipeInstructions = recipe.recipeInstructions.map { it.text },
|
||||||
|
recipeIngredients = recipe.recipeIngredient.map { it.note },
|
||||||
|
isRecipePublic = recipe.settings.public,
|
||||||
|
areCommentsDisabled = recipe.settings.disableComments,
|
||||||
|
)
|
||||||
|
addRecipeStorage.save(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
Timber.v("clear() called")
|
logger.v { "clear() called" }
|
||||||
addRecipeStorage.clear()
|
addRecipeStorage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveRecipe(): String {
|
override suspend fun saveRecipe(): String {
|
||||||
Timber.v("saveRecipe() called")
|
logger.v { "saveRecipe() called" }
|
||||||
return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first())
|
return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package gq.kirmanak.mealient.data.add.impl
|
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeStorage
|
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeInput
|
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class AddRecipeStorageImpl @Inject constructor(
|
|
||||||
private val dataStore: DataStore<AddRecipeInput>,
|
|
||||||
) : AddRecipeStorage {
|
|
||||||
|
|
||||||
override val updates: Flow<AddRecipeRequest>
|
|
||||||
get() = dataStore.data.map { AddRecipeRequest(it) }
|
|
||||||
|
|
||||||
override suspend fun save(addRecipeRequest: AddRecipeRequest) {
|
|
||||||
Timber.v("saveRecipeInput() called with: addRecipeRequest = $addRecipeRequest")
|
|
||||||
dataStore.updateData { addRecipeRequest.toInput() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun clear() {
|
|
||||||
Timber.v("clearRecipeInput() called")
|
|
||||||
dataStore.updateData { AddRecipeInput.getDefaultInstance() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package gq.kirmanak.mealient.data.add.models
|
package gq.kirmanak.mealient.data.add.models
|
||||||
|
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@@ -20,27 +21,27 @@ data class AddRecipeRequest(
|
|||||||
@SerialName("assets") val assets: List<String> = emptyList(),
|
@SerialName("assets") val assets: List<String> = emptyList(),
|
||||||
@SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(),
|
@SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(),
|
||||||
) {
|
) {
|
||||||
constructor(input: AddRecipeInput) : this(
|
constructor(input: AddRecipeDraft) : this(
|
||||||
name = input.recipeName,
|
name = input.recipeName,
|
||||||
description = input.recipeDescription,
|
description = input.recipeDescription,
|
||||||
recipeYield = input.recipeYield,
|
recipeYield = input.recipeYield,
|
||||||
recipeIngredient = input.recipeIngredientsList.map { AddRecipeIngredient(note = it) },
|
recipeIngredient = input.recipeIngredients.map { AddRecipeIngredient(note = it) },
|
||||||
recipeInstructions = input.recipeInstructionsList.map { AddRecipeInstruction(text = it) },
|
recipeInstructions = input.recipeInstructions.map { AddRecipeInstruction(text = it) },
|
||||||
settings = AddRecipeSettings(
|
settings = AddRecipeSettings(
|
||||||
public = input.isRecipePublic,
|
public = input.isRecipePublic,
|
||||||
disableComments = input.areCommentsDisabled,
|
disableComments = input.areCommentsDisabled,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toInput(): AddRecipeInput = AddRecipeInput.newBuilder()
|
fun toDraft(): AddRecipeDraft = AddRecipeDraft(
|
||||||
.setRecipeName(name)
|
recipeName = name,
|
||||||
.setRecipeDescription(description)
|
recipeDescription = description,
|
||||||
.setRecipeYield(recipeYield)
|
recipeYield = recipeYield,
|
||||||
.setIsRecipePublic(settings.public)
|
recipeInstructions = recipeInstructions.map { it.text },
|
||||||
.setAreCommentsDisabled(settings.disableComments)
|
recipeIngredients = recipeIngredient.map { it.note },
|
||||||
.addAllRecipeIngredients(recipeIngredient.map { it.note })
|
isRecipePublic = settings.public,
|
||||||
.addAllRecipeInstructions(recipeInstructions.map { it.text })
|
areCommentsDisabled = settings.disableComments,
|
||||||
.build()
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
|
|||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
|
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
|
||||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -18,14 +18,15 @@ import javax.inject.Singleton
|
|||||||
class AuthDataSourceImpl @Inject constructor(
|
class AuthDataSourceImpl @Inject constructor(
|
||||||
private val authServiceFactory: ServiceFactory<AuthService>,
|
private val authServiceFactory: ServiceFactory<AuthService>,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
|
private val logger: Logger,
|
||||||
) : AuthDataSource {
|
) : AuthDataSource {
|
||||||
|
|
||||||
override suspend fun authenticate(username: String, password: String): String {
|
override suspend fun authenticate(username: String, password: String): String {
|
||||||
Timber.v("authenticate() called with: username = $username, password = $password")
|
logger.v { "authenticate() called with: username = $username, password = $password" }
|
||||||
val authService = authServiceFactory.provideService()
|
val authService = authServiceFactory.provideService()
|
||||||
val response = sendRequest(authService, username, password)
|
val response = sendRequest(authService, username, password)
|
||||||
val accessToken = parseToken(response)
|
val accessToken = parseToken(response)
|
||||||
Timber.v("authenticate() returned: $accessToken")
|
logger.v { "authenticate() returned: $accessToken" }
|
||||||
return accessToken
|
return accessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ class AuthDataSourceImpl @Inject constructor(
|
|||||||
username: String,
|
username: String,
|
||||||
password: String
|
password: String
|
||||||
): Response<GetTokenResponse> = logAndMapErrors(
|
): Response<GetTokenResponse> = logAndMapErrors(
|
||||||
|
logger,
|
||||||
block = { authService.getToken(username = username, password = password) },
|
block = { authService.getToken(username = username, password = password) },
|
||||||
logProvider = { "sendRequest: can't get token" },
|
logProvider = { "sendRequest: can't get token" },
|
||||||
)
|
)
|
||||||
@@ -44,7 +46,7 @@ class AuthDataSourceImpl @Inject constructor(
|
|||||||
response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null"))
|
response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null"))
|
||||||
} else {
|
} else {
|
||||||
val cause = HttpException(response)
|
val cause = HttpException(response)
|
||||||
val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json)
|
val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json, logger)
|
||||||
throw when (errorDetail?.detail) {
|
throw when (errorDetail?.detail) {
|
||||||
"Unauthorized" -> Unauthorized(cause)
|
"Unauthorized" -> Unauthorized(cause)
|
||||||
else -> NotMealie(cause)
|
else -> NotMealie(cause)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -14,13 +14,14 @@ import javax.inject.Singleton
|
|||||||
class AuthRepoImpl @Inject constructor(
|
class AuthRepoImpl @Inject constructor(
|
||||||
private val authStorage: AuthStorage,
|
private val authStorage: AuthStorage,
|
||||||
private val authDataSource: AuthDataSource,
|
private val authDataSource: AuthDataSource,
|
||||||
|
private val logger: Logger,
|
||||||
) : AuthRepo {
|
) : AuthRepo {
|
||||||
|
|
||||||
override val isAuthorizedFlow: Flow<Boolean>
|
override val isAuthorizedFlow: Flow<Boolean>
|
||||||
get() = authStorage.authHeaderFlow.map { it != null }
|
get() = authStorage.authHeaderFlow.map { it != null }
|
||||||
|
|
||||||
override suspend fun authenticate(email: String, password: String) {
|
override suspend fun authenticate(email: String, password: String) {
|
||||||
Timber.v("authenticate() called with: email = $email, password = $password")
|
logger.v { "authenticate() called with: email = $email, password = $password" }
|
||||||
authDataSource.authenticate(email, password)
|
authDataSource.authenticate(email, password)
|
||||||
.let { AUTH_HEADER_FORMAT.format(it) }
|
.let { AUTH_HEADER_FORMAT.format(it) }
|
||||||
.let { authStorage.setAuthHeader(it) }
|
.let { authStorage.setAuthHeader(it) }
|
||||||
@@ -35,14 +36,14 @@ class AuthRepoImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun logout() {
|
override suspend fun logout() {
|
||||||
Timber.v("logout() called")
|
logger.v { "logout() called" }
|
||||||
authStorage.setEmail(null)
|
authStorage.setEmail(null)
|
||||||
authStorage.setPassword(null)
|
authStorage.setPassword(null)
|
||||||
authStorage.setAuthHeader(null)
|
authStorage.setAuthHeader(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun invalidateAuthHeader() {
|
override suspend fun invalidateAuthHeader() {
|
||||||
Timber.v("invalidateAuthHeader() called")
|
logger.v { "invalidateAuthHeader() called" }
|
||||||
val email = authStorage.getEmail() ?: return
|
val email = authStorage.getEmail() ?: return
|
||||||
val password = authStorage.getPassword() ?: return
|
val password = authStorage.getPassword() ?: return
|
||||||
runCatchingExceptCancel { authenticate(email, password) }
|
runCatchingExceptCancel { authenticate(email, password) }
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import android.content.SharedPreferences
|
|||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.di.AuthModule.Companion.ENCRYPTED
|
import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED
|
||||||
import gq.kirmanak.mealient.extensions.prefsChangeFlow
|
import gq.kirmanak.mealient.extensions.prefsChangeFlow
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -19,11 +19,12 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class AuthStorageImpl @Inject constructor(
|
class AuthStorageImpl @Inject constructor(
|
||||||
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
@Named(ENCRYPTED) private val sharedPreferences: SharedPreferences,
|
||||||
|
private val logger: Logger,
|
||||||
) : AuthStorage {
|
) : AuthStorage {
|
||||||
|
|
||||||
override val authHeaderFlow: Flow<String?>
|
override val authHeaderFlow: Flow<String?>
|
||||||
get() = sharedPreferences
|
get() = sharedPreferences
|
||||||
.prefsChangeFlow { getString(AUTH_HEADER_KEY, null) }
|
.prefsChangeFlow(logger) { getString(AUTH_HEADER_KEY, null) }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||||
|
|
||||||
@@ -43,13 +44,13 @@ class AuthStorageImpl @Inject constructor(
|
|||||||
key: String,
|
key: String,
|
||||||
value: String?
|
value: String?
|
||||||
) = withContext(singleThreadDispatcher) {
|
) = withContext(singleThreadDispatcher) {
|
||||||
Timber.v("putString() called with: key = $key, value = $value")
|
logger.v { "putString() called with: key = $key, value = $value" }
|
||||||
sharedPreferences.edit(commit = true) { putString(key, value) }
|
sharedPreferences.edit(commit = true) { putString(key, value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getString(key: String) = withContext(singleThreadDispatcher) {
|
private suspend fun getString(key: String) = withContext(singleThreadDispatcher) {
|
||||||
val result = sharedPreferences.getString(key, null)
|
val result = sharedPreferences.getString(key, null)
|
||||||
Timber.v("getString() called with: key = $key, returned: $result")
|
logger.v { "getString() called with: key = $key, returned: $result" }
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,20 +5,22 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo
|
|||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||||
import gq.kirmanak.mealient.extensions.versionInfo
|
import gq.kirmanak.mealient.extensions.versionInfo
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class VersionDataSourceImpl @Inject constructor(
|
class VersionDataSourceImpl @Inject constructor(
|
||||||
private val serviceFactory: ServiceFactory<VersionService>,
|
private val serviceFactory: ServiceFactory<VersionService>,
|
||||||
|
private val logger: Logger,
|
||||||
) : VersionDataSource {
|
) : VersionDataSource {
|
||||||
|
|
||||||
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
|
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
|
||||||
Timber.v("getVersionInfo() called with: baseUrl = $baseUrl")
|
logger.v { "getVersionInfo() called with: baseUrl = $baseUrl" }
|
||||||
|
|
||||||
val service = serviceFactory.provideService(baseUrl)
|
val service = serviceFactory.provideService(baseUrl)
|
||||||
val response = logAndMapErrors(
|
val response = logAndMapErrors(
|
||||||
|
logger,
|
||||||
block = { service.getVersion() },
|
block = { service.getVersion() },
|
||||||
logProvider = { "getVersionInfo: can't request version" }
|
logProvider = { "getVersionInfo: can't request version" }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,15 +2,16 @@ package gq.kirmanak.mealient.data.disclaimer
|
|||||||
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class DisclaimerStorageImpl @Inject constructor(
|
class DisclaimerStorageImpl @Inject constructor(
|
||||||
private val preferencesStorage: PreferencesStorage,
|
private val preferencesStorage: PreferencesStorage,
|
||||||
|
private val logger: Logger,
|
||||||
) : DisclaimerStorage {
|
) : DisclaimerStorage {
|
||||||
|
|
||||||
private val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
private val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
|
||||||
@@ -19,14 +20,14 @@ class DisclaimerStorageImpl @Inject constructor(
|
|||||||
get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true }
|
get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true }
|
||||||
|
|
||||||
override suspend fun isDisclaimerAccepted(): Boolean {
|
override suspend fun isDisclaimerAccepted(): Boolean {
|
||||||
Timber.v("isDisclaimerAccepted() called")
|
logger.v { "isDisclaimerAccepted() called" }
|
||||||
val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false
|
val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false
|
||||||
Timber.v("isDisclaimerAccepted() returned: $isAccepted")
|
logger.v { "isDisclaimerAccepted() returned: $isAccepted" }
|
||||||
return isAccepted
|
return isAccepted
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun acceptDisclaimer() {
|
override suspend fun acceptDisclaimer() {
|
||||||
Timber.v("acceptDisclaimer() called")
|
logger.v { "acceptDisclaimer() called" }
|
||||||
preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true))
|
preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
package gq.kirmanak.mealient.data.network
|
package gq.kirmanak.mealient.data.network
|
||||||
|
|
||||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class RetrofitBuilder(
|
class RetrofitBuilder(
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val json: Json
|
private val json: Json,
|
||||||
|
private val logger: Logger,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
fun buildRetrofit(baseUrl: String): Retrofit {
|
fun buildRetrofit(baseUrl: String): Retrofit {
|
||||||
Timber.v("buildRetrofit() called with: baseUrl = $baseUrl")
|
logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" }
|
||||||
val contentType = "application/json".toMediaType()
|
val contentType = "application/json".toMediaType()
|
||||||
val converterFactory = json.asConverterFactory(contentType)
|
val converterFactory = json.asConverterFactory(contentType)
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
|
|||||||
@@ -2,30 +2,34 @@ package gq.kirmanak.mealient.data.network
|
|||||||
|
|
||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
|
||||||
inline fun <reified T> RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) =
|
inline fun <reified T> RetrofitBuilder.createServiceFactory(
|
||||||
RetrofitServiceFactory(T::class.java, this, baseURLStorage)
|
baseURLStorage: BaseURLStorage,
|
||||||
|
logger: Logger
|
||||||
|
) =
|
||||||
|
RetrofitServiceFactory(T::class.java, this, baseURLStorage, logger)
|
||||||
|
|
||||||
class RetrofitServiceFactory<T>(
|
class RetrofitServiceFactory<T>(
|
||||||
private val serviceClass: Class<T>,
|
private val serviceClass: Class<T>,
|
||||||
private val retrofitBuilder: RetrofitBuilder,
|
private val retrofitBuilder: RetrofitBuilder,
|
||||||
private val baseURLStorage: BaseURLStorage,
|
private val baseURLStorage: BaseURLStorage,
|
||||||
|
private val logger: Logger,
|
||||||
) : ServiceFactory<T> {
|
) : ServiceFactory<T> {
|
||||||
|
|
||||||
private val cache: MutableMap<String, T> = mutableMapOf()
|
private val cache: MutableMap<String, T> = mutableMapOf()
|
||||||
|
|
||||||
override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel {
|
override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel {
|
||||||
Timber.v("provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}")
|
logger.v { "provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}" }
|
||||||
val url = baseUrl ?: baseURLStorage.requireBaseURL()
|
val url = baseUrl ?: baseURLStorage.requireBaseURL()
|
||||||
synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
|
synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
Timber.e(it, "provideService: can't provide service for $baseUrl")
|
logger.e(it) { "provideService: can't provide service for $baseUrl" }
|
||||||
throw NetworkError.MalformedUrl(it)
|
throw NetworkError.MalformedUrl(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createService(url: String, serviceClass: Class<T>): T {
|
private fun createService(url: String, serviceClass: Class<T>): T {
|
||||||
Timber.v("createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}")
|
logger.v { "createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}" }
|
||||||
val service = retrofitBuilder.buildRetrofit(url).create(serviceClass)
|
val service = retrofitBuilder.buildRetrofit(url).create(serviceClass)
|
||||||
cache[url] = service
|
cache[url] = service
|
||||||
return service
|
return service
|
||||||
|
|||||||
@@ -11,20 +11,21 @@ import gq.kirmanak.mealient.extensions.recipeEntity
|
|||||||
import gq.kirmanak.mealient.extensions.toRecipeEntity
|
import gq.kirmanak.mealient.extensions.toRecipeEntity
|
||||||
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
|
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
|
||||||
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
|
import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class RecipeStorageImpl @Inject constructor(
|
class RecipeStorageImpl @Inject constructor(
|
||||||
private val db: AppDb
|
private val db: AppDb,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecipeStorage {
|
) : RecipeStorage {
|
||||||
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
|
private val recipeDao: RecipeDao by lazy { db.recipeDao() }
|
||||||
|
|
||||||
override suspend fun saveRecipes(
|
override suspend fun saveRecipes(
|
||||||
recipes: List<GetRecipeSummaryResponse>
|
recipes: List<GetRecipeSummaryResponse>
|
||||||
) = db.withTransaction {
|
) = db.withTransaction {
|
||||||
Timber.v("saveRecipes() called with $recipes")
|
logger.v { "saveRecipes() called with $recipes" }
|
||||||
|
|
||||||
val tagEntities = mutableSetOf<TagEntity>()
|
val tagEntities = mutableSetOf<TagEntity>()
|
||||||
tagEntities.addAll(recipeDao.queryAllTags())
|
tagEntities.addAll(recipeDao.queryAllTags())
|
||||||
@@ -91,12 +92,12 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
|
|
||||||
|
|
||||||
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
|
override fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> {
|
||||||
Timber.v("queryRecipes() called")
|
logger.v { "queryRecipes() called" }
|
||||||
return recipeDao.queryRecipesByPages()
|
return recipeDao.queryRecipesByPages()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) {
|
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) {
|
||||||
Timber.v("refreshAll() called with: recipes = $recipes")
|
logger.v { "refreshAll() called with: recipes = $recipes" }
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
recipeDao.removeAllRecipes()
|
recipeDao.removeAllRecipes()
|
||||||
saveRecipes(recipes)
|
saveRecipes(recipes)
|
||||||
@@ -104,7 +105,7 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clearAllLocalData() {
|
override suspend fun clearAllLocalData() {
|
||||||
Timber.v("clearAllLocalData() called")
|
logger.v { "clearAllLocalData() called" }
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
recipeDao.removeAllRecipes()
|
recipeDao.removeAllRecipes()
|
||||||
recipeDao.removeAllCategories()
|
recipeDao.removeAllCategories()
|
||||||
@@ -113,7 +114,7 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
|
override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) {
|
||||||
Timber.v("saveRecipeInfo() called with: recipe = $recipe")
|
logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
recipeDao.insertRecipe(recipe.toRecipeEntity())
|
recipeDao.insertRecipe(recipe.toRecipeEntity())
|
||||||
|
|
||||||
@@ -132,11 +133,11 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo {
|
override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo {
|
||||||
Timber.v("queryRecipeInfo() called with: recipeId = $recipeId")
|
logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
|
||||||
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
|
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
|
||||||
"Can't find recipe by id $recipeId in DB"
|
"Can't find recipe by id $recipeId in DB"
|
||||||
}
|
}
|
||||||
Timber.v("queryRecipeInfo() returned: $fullRecipeInfo")
|
logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" }
|
||||||
return fullRecipeInfo
|
return fullRecipeInfo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
package gq.kirmanak.mealient.data.recipes.impl
|
package gq.kirmanak.mealient.data.recipes.impl
|
||||||
|
|
||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class RecipeImageUrlProviderImpl @Inject constructor(
|
class RecipeImageUrlProviderImpl @Inject constructor(
|
||||||
private val baseURLStorage: BaseURLStorage,
|
private val baseURLStorage: BaseURLStorage,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecipeImageUrlProvider {
|
) : RecipeImageUrlProvider {
|
||||||
|
|
||||||
override suspend fun generateImageUrl(slug: String?): String? {
|
override suspend fun generateImageUrl(slug: String?): String? {
|
||||||
Timber.v("generateImageUrl() called with: slug = $slug")
|
logger.v { "generateImageUrl() called with: slug = $slug" }
|
||||||
slug?.takeUnless { it.isBlank() } ?: return null
|
slug?.takeUnless { it.isBlank() } ?: return null
|
||||||
val imagePath = IMAGE_PATH_FORMAT.format(slug)
|
val imagePath = IMAGE_PATH_FORMAT.format(slug)
|
||||||
val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() }
|
val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() }
|
||||||
@@ -21,7 +22,7 @@ class RecipeImageUrlProviderImpl @Inject constructor(
|
|||||||
?.addPathSegments(imagePath)
|
?.addPathSegments(imagePath)
|
||||||
?.build()
|
?.build()
|
||||||
?.toString()
|
?.toString()
|
||||||
Timber.v("getRecipeImageUrl() returned: $result")
|
logger.v { "getRecipeImageUrl() returned: $result" }
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
|||||||
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo
|
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -21,9 +21,10 @@ class RecipeRepoImpl @Inject constructor(
|
|||||||
private val storage: RecipeStorage,
|
private val storage: RecipeStorage,
|
||||||
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
||||||
private val dataSource: RecipeDataSource,
|
private val dataSource: RecipeDataSource,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecipeRepo {
|
) : RecipeRepo {
|
||||||
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
|
override fun createPager(): Pager<Int, RecipeSummaryEntity> {
|
||||||
Timber.v("createPager() called")
|
logger.v { "createPager() called" }
|
||||||
val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true)
|
val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true)
|
||||||
return Pager(
|
return Pager(
|
||||||
config = pagingConfig,
|
config = pagingConfig,
|
||||||
@@ -33,17 +34,17 @@ class RecipeRepoImpl @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun clearLocalData() {
|
override suspend fun clearLocalData() {
|
||||||
Timber.v("clearLocalData() called")
|
logger.v { "clearLocalData() called" }
|
||||||
storage.clearAllLocalData()
|
storage.clearAllLocalData()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
|
override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo {
|
||||||
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
|
logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
|
||||||
|
|
||||||
runCatchingExceptCancel {
|
runCatchingExceptCancel {
|
||||||
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
|
storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug))
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
Timber.e(it, "loadRecipeInfo: can't update full recipe info")
|
logger.e(it) { "loadRecipeInfo: can't update full recipe info" }
|
||||||
}
|
}
|
||||||
|
|
||||||
return storage.queryRecipeInfo(recipeId)
|
return storage.queryRecipeInfo(recipeId)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
|||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
private val storage: RecipeStorage,
|
private val storage: RecipeStorage,
|
||||||
private val network: RecipeDataSource,
|
private val network: RecipeDataSource,
|
||||||
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
private val pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>,
|
||||||
|
private val logger: Logger,
|
||||||
) : RemoteMediator<Int, RecipeSummaryEntity>() {
|
) : RemoteMediator<Int, RecipeSummaryEntity>() {
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
@@ -27,10 +28,10 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
loadType: LoadType,
|
loadType: LoadType,
|
||||||
state: PagingState<Int, RecipeSummaryEntity>
|
state: PagingState<Int, RecipeSummaryEntity>
|
||||||
): MediatorResult {
|
): MediatorResult {
|
||||||
Timber.v("load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state")
|
logger.v { "load() called with: lastRequestEnd = $lastRequestEnd, loadType = $loadType, state = $state" }
|
||||||
|
|
||||||
if (loadType == PREPEND) {
|
if (loadType == PREPEND) {
|
||||||
Timber.i("load: early exit, PREPEND isn't supported")
|
logger.i { "load: early exit, PREPEND isn't supported" }
|
||||||
return MediatorResult.Success(endOfPaginationReached = true)
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
else storage.saveRecipes(recipes)
|
else storage.saveRecipes(recipes)
|
||||||
recipes.size
|
recipes.size
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
Timber.e(it, "load: can't load recipes")
|
logger.e(it) { "load: can't load recipes" }
|
||||||
return MediatorResult.Error(it)
|
return MediatorResult.Error(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +54,7 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
// Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858
|
// Read that trick here https://github.com/android/architecture-components-samples/issues/889#issuecomment-880847858
|
||||||
pagingSourceFactory.invalidate()
|
pagingSourceFactory.invalidate()
|
||||||
|
|
||||||
Timber.d("load: expectedCount = $limit, received $count")
|
logger.d { "load: expectedCount = $limit, received $count" }
|
||||||
lastRequestEnd = start + count
|
lastRequestEnd = start + count
|
||||||
return MediatorResult.Success(endOfPaginationReached = count < limit)
|
return MediatorResult.Success(endOfPaginationReached = count < limit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,32 @@ package gq.kirmanak.mealient.data.recipes.network
|
|||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class RecipeDataSourceImpl @Inject constructor(
|
class RecipeDataSourceImpl @Inject constructor(
|
||||||
private val recipeServiceFactory: ServiceFactory<RecipeService>,
|
private val recipeServiceFactory: ServiceFactory<RecipeService>,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecipeDataSource {
|
) : RecipeDataSource {
|
||||||
|
|
||||||
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
|
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
|
||||||
Timber.v("requestRecipes() called with: start = $start, limit = $limit")
|
logger.v { "requestRecipes() called with: start = $start, limit = $limit" }
|
||||||
val recipeSummary = getRecipeService().getRecipeSummary(start, limit)
|
val recipeSummary = getRecipeService().getRecipeSummary(start, limit)
|
||||||
Timber.v("requestRecipes() returned: $recipeSummary")
|
logger.v { "requestRecipes() returned: $recipeSummary" }
|
||||||
return recipeSummary
|
return recipeSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
|
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
|
||||||
Timber.v("requestRecipeInfo() called with: slug = $slug")
|
logger.v { "requestRecipeInfo() called with: slug = $slug" }
|
||||||
val recipeInfo = getRecipeService().getRecipe(slug)
|
val recipeInfo = getRecipeService().getRecipe(slug)
|
||||||
Timber.v("requestRecipeInfo() returned: $recipeInfo")
|
logger.v { "requestRecipeInfo() returned: $recipeInfo" }
|
||||||
return recipeInfo
|
return recipeInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getRecipeService(): RecipeService {
|
private suspend fun getRecipeService(): RecipeService {
|
||||||
Timber.v("getRecipeService() called")
|
logger.v { "getRecipeService() called" }
|
||||||
return recipeServiceFactory.provideService()
|
return recipeServiceFactory.provideService()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import androidx.datastore.preferences.core.Preferences
|
|||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class PreferencesStorageImpl @Inject constructor(
|
class PreferencesStorageImpl @Inject constructor(
|
||||||
private val dataStore: DataStore<Preferences>
|
private val dataStore: DataStore<Preferences>,
|
||||||
|
private val logger: Logger,
|
||||||
) : PreferencesStorage {
|
) : PreferencesStorage {
|
||||||
|
|
||||||
override val baseUrlKey = stringPreferencesKey("baseUrl")
|
override val baseUrlKey = stringPreferencesKey("baseUrl")
|
||||||
@@ -21,7 +22,7 @@ class PreferencesStorageImpl @Inject constructor(
|
|||||||
|
|
||||||
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
|
override suspend fun <T> getValue(key: Preferences.Key<T>): T? {
|
||||||
val value = dataStore.data.first()[key]
|
val value = dataStore.data.first()[key]
|
||||||
Timber.v("getValue() returned: $value for $key")
|
logger.v { "getValue() returned: $value for $key" }
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,23 +30,23 @@ class PreferencesStorageImpl @Inject constructor(
|
|||||||
checkNotNull(getValue(key)) { "Value at $key is null when it was required" }
|
checkNotNull(getValue(key)) { "Value at $key is null when it was required" }
|
||||||
|
|
||||||
override suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>) {
|
override suspend fun <T> storeValues(vararg pairs: Pair<Preferences.Key<T>, T>) {
|
||||||
Timber.v("storeValues() called with: pairs = ${pairs.contentToString()}")
|
logger.v { "storeValues() called with: pairs = ${pairs.contentToString()}" }
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
pairs.forEach { preferences += it.toPreferencesPair() }
|
pairs.forEach { preferences += it.toPreferencesPair() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?> {
|
override fun <T> valueUpdates(key: Preferences.Key<T>): Flow<T?> {
|
||||||
Timber.v("valueUpdates() called with: key = $key")
|
logger.v { "valueUpdates() called with: key = $key" }
|
||||||
return dataStore.data
|
return dataStore.data
|
||||||
.map { it[key] }
|
.map { it[key] }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.onEach { Timber.d("valueUpdates: new value at $key is $it") }
|
.onEach { logger.d { "valueUpdates: new value at $key is $it" } }
|
||||||
.onCompletion { Timber.i(it, "valueUpdates: finished") }
|
.onCompletion { logger.i(it) { "valueUpdates: finished" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <T> removeValues(vararg keys: Preferences.Key<T>) {
|
override suspend fun <T> removeValues(vararg keys: Preferences.Key<T>) {
|
||||||
Timber.v("removeValues() called with: key = ${keys.contentToString()}")
|
logger.v { "removeValues() called with: key = ${keys.contentToString()}" }
|
||||||
dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
keys.forEach { preferences -= it }
|
keys.forEach { preferences -= it }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
package gq.kirmanak.mealient.di
|
package gq.kirmanak.mealient.di
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.core.DataStore
|
|
||||||
import androidx.datastore.core.DataStoreFactory
|
|
||||||
import androidx.datastore.dataStoreFile
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeStorage
|
|
||||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeDataSourceImpl
|
import gq.kirmanak.mealient.data.add.impl.AddRecipeDataSourceImpl
|
||||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeRepoImpl
|
import gq.kirmanak.mealient.data.add.impl.AddRecipeRepoImpl
|
||||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeService
|
import gq.kirmanak.mealient.data.add.impl.AddRecipeService
|
||||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeStorageImpl
|
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeInput
|
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeInputSerializer
|
|
||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -34,22 +28,18 @@ interface AddRecipeModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideAddRecipeInputStore(
|
|
||||||
@ApplicationContext context: Context
|
|
||||||
): DataStore<AddRecipeInput> = DataStoreFactory.create(AddRecipeInputSerializer) {
|
|
||||||
context.dataStoreFile("add_recipe_input")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAddRecipeServiceFactory(
|
fun provideAddRecipeServiceFactory(
|
||||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||||
json: Json,
|
json: Json,
|
||||||
|
logger: Logger,
|
||||||
baseURLStorage: BaseURLStorage,
|
baseURLStorage: BaseURLStorage,
|
||||||
): ServiceFactory<AddRecipeService> {
|
): ServiceFactory<AddRecipeService> {
|
||||||
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage)
|
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||||
|
baseURLStorage,
|
||||||
|
logger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ package gq.kirmanak.mealient.di
|
|||||||
|
|
||||||
import android.accounts.AccountManager
|
import android.accounts.AccountManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
|
||||||
import androidx.security.crypto.MasterKeys
|
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
@@ -22,6 +19,7 @@ import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
|||||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -32,16 +30,19 @@ import javax.inject.Singleton
|
|||||||
interface AuthModule {
|
interface AuthModule {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ENCRYPTED = "encrypted"
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAuthServiceFactory(
|
fun provideAuthServiceFactory(
|
||||||
@Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
@Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||||
json: Json,
|
json: Json,
|
||||||
|
logger: Logger,
|
||||||
baseURLStorage: BaseURLStorage,
|
baseURLStorage: BaseURLStorage,
|
||||||
): ServiceFactory<AuthService> {
|
): ServiceFactory<AuthService> {
|
||||||
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage)
|
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||||
|
baseURLStorage,
|
||||||
|
logger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -49,23 +50,6 @@ interface AuthModule {
|
|||||||
fun provideAccountManager(@ApplicationContext context: Context): AccountManager {
|
fun provideAccountManager(@ApplicationContext context: Context): AccountManager {
|
||||||
return AccountManager.get(context)
|
return AccountManager.get(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
@Named(ENCRYPTED)
|
|
||||||
fun provideEncryptedSharedPreferences(
|
|
||||||
@ApplicationContext applicationContext: Context,
|
|
||||||
): SharedPreferences {
|
|
||||||
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
|
|
||||||
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
|
|
||||||
return EncryptedSharedPreferences.create(
|
|
||||||
ENCRYPTED,
|
|
||||||
mainKeyAlias,
|
|
||||||
applicationContext,
|
|
||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
|||||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -29,9 +30,13 @@ interface BaseURLModule {
|
|||||||
fun provideVersionServiceFactory(
|
fun provideVersionServiceFactory(
|
||||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||||
json: Json,
|
json: Json,
|
||||||
|
logger: Logger,
|
||||||
baseURLStorage: BaseURLStorage,
|
baseURLStorage: BaseURLStorage,
|
||||||
): ServiceFactory<VersionService> {
|
): ServiceFactory<VersionService> {
|
||||||
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage)
|
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||||
|
baseURLStorage,
|
||||||
|
logger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dagger.hilt.EntryPoint
|
|||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
@@ -13,6 +14,8 @@ import javax.inject.Named
|
|||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
interface GlideModuleEntryPoint {
|
interface GlideModuleEntryPoint {
|
||||||
|
|
||||||
|
fun provideLogger(): Logger
|
||||||
|
|
||||||
@Named(AUTH_OK_HTTP)
|
@Named(AUTH_OK_HTTP)
|
||||||
fun provideOkHttp(): OkHttpClient
|
fun provideOkHttp(): OkHttpClient
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
|||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
|
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
|
||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeService
|
import gq.kirmanak.mealient.data.recipes.network.RecipeService
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
|
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -61,9 +62,13 @@ interface RecipeModule {
|
|||||||
fun provideRecipeServiceFactory(
|
fun provideRecipeServiceFactory(
|
||||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||||
json: Json,
|
json: Json,
|
||||||
|
logger: Logger,
|
||||||
baseURLStorage: BaseURLStorage,
|
baseURLStorage: BaseURLStorage,
|
||||||
): ServiceFactory<RecipeService> {
|
): ServiceFactory<RecipeService> {
|
||||||
return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage)
|
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||||
|
baseURLStorage,
|
||||||
|
logger
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
package gq.kirmanak.mealient.extensions
|
package gq.kirmanak.mealient.extensions
|
||||||
|
|
||||||
import gq.kirmanak.mealient.data.network.NetworkError
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
import kotlinx.serialization.json.decodeFromStream
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
inline fun <T, reified R> Response<T>.decodeErrorBodyOrNull(json: Json): R? =
|
inline fun <T, reified R> Response<T>.decodeErrorBodyOrNull(json: Json, logger: Logger): R? =
|
||||||
errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull<R>(it) }
|
errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull<R>(it, logger) }
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream): T? =
|
inline fun <reified T> Json.decodeFromStreamOrNull(stream: InputStream, logger: Logger): T? =
|
||||||
runCatching { decodeFromStream<T>(stream) }
|
runCatching { decodeFromStream<T>(stream) }
|
||||||
.onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") }
|
.onFailure { logger.e(it) { "decodeFromStreamOrNull: can't decode" } }
|
||||||
.getOrNull()
|
.getOrNull()
|
||||||
|
|
||||||
fun Throwable.mapToNetworkError(): NetworkError = when (this) {
|
fun Throwable.mapToNetworkError(): NetworkError = when (this) {
|
||||||
@@ -24,8 +24,12 @@ fun Throwable.mapToNetworkError(): NetworkError = when (this) {
|
|||||||
else -> NetworkError.NoServerConnection(this)
|
else -> NetworkError.NoServerConnection(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <T> logAndMapErrors(block: () -> T, logProvider: () -> String): T =
|
inline fun <T> logAndMapErrors(
|
||||||
|
logger: Logger,
|
||||||
|
block: () -> T,
|
||||||
|
noinline logProvider: () -> String
|
||||||
|
): T =
|
||||||
runCatchingExceptCancel(block).getOrElse {
|
runCatchingExceptCancel(block).getOrElse {
|
||||||
Timber.e(it, logProvider())
|
logger.e(it, messageSupplier = logProvider)
|
||||||
throw it.mapToNetworkError()
|
throw it.mapToNetworkError()
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleOwner
|
|||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.channels.ChannelResult
|
import kotlinx.coroutines.channels.ChannelResult
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.channels.onClosed
|
import kotlinx.coroutines.channels.onClosed
|
||||||
@@ -24,61 +25,60 @@ import kotlinx.coroutines.flow.callbackFlow
|
|||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
fun SwipeRefreshLayout.refreshRequestFlow(): Flow<Unit> = callbackFlow {
|
fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow<Unit> = callbackFlow {
|
||||||
Timber.v("refreshRequestFlow() called")
|
logger.v { "refreshRequestFlow() called" }
|
||||||
val listener = SwipeRefreshLayout.OnRefreshListener {
|
val listener = SwipeRefreshLayout.OnRefreshListener {
|
||||||
Timber.v("refreshRequestFlow: listener called")
|
logger.v { "refreshRequestFlow: listener called" }
|
||||||
trySend(Unit).logErrors("refreshesFlow")
|
trySend(Unit).logErrors("refreshesFlow", logger)
|
||||||
}
|
}
|
||||||
setOnRefreshListener(listener)
|
setOnRefreshListener(listener)
|
||||||
awaitClose {
|
awaitClose {
|
||||||
Timber.v("Removing refresh request listener")
|
logger.v { "Removing refresh request listener" }
|
||||||
setOnRefreshListener(null)
|
setOnRefreshListener(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Activity.setSystemUiVisibility(isVisible: Boolean) {
|
fun Activity.setSystemUiVisibility(isVisible: Boolean, logger: Logger) {
|
||||||
Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible")
|
logger.v { "setSystemUiVisibility() called with: isVisible = $isVisible" }
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible, logger)
|
||||||
else setSystemUiVisibilityV1(isVisible)
|
else setSystemUiVisibilityV1(isVisible, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean) {
|
private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean, logger: Logger) {
|
||||||
Timber.v("setSystemUiVisibilityV1() called with: isVisible = $isVisible")
|
logger.v { "setSystemUiVisibilityV1() called with: isVisible = $isVisible" }
|
||||||
window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN
|
window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.R)
|
@RequiresApi(Build.VERSION_CODES.R)
|
||||||
private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean) {
|
private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean, logger: Logger) {
|
||||||
Timber.v("setSystemUiVisibilityV30() called with: isVisible = $isVisible")
|
logger.v { "setSystemUiVisibilityV30() called with: isVisible = $isVisible" }
|
||||||
val systemBars = WindowInsets.Type.systemBars()
|
val systemBars = WindowInsets.Type.systemBars()
|
||||||
window.insetsController?.apply { if (isVisible) show(systemBars) else hide(systemBars) }
|
window.insetsController?.apply { if (isVisible) show(systemBars) else hide(systemBars) }
|
||||||
?: Timber.w("setSystemUiVisibilityV30: insets controller is null")
|
?: logger.w { "setSystemUiVisibilityV30: insets controller is null" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean) {
|
fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean, logger: Logger) {
|
||||||
Timber.v("setActionBarVisibility() called with: isVisible = $isVisible")
|
logger.v { "setActionBarVisibility() called with: isVisible = $isVisible" }
|
||||||
supportActionBar?.apply { if (isVisible) show() else hide() }
|
supportActionBar?.apply { if (isVisible) show() else hide() }
|
||||||
?: Timber.w("setActionBarVisibility: action bar is null")
|
?: logger.w { "setActionBarVisibility: action bar is null" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TextView.textChangesFlow(): Flow<CharSequence?> = callbackFlow {
|
fun TextView.textChangesFlow(logger: Logger): Flow<CharSequence?> = callbackFlow {
|
||||||
Timber.v("textChangesFlow() called")
|
logger.v { "textChangesFlow() called" }
|
||||||
val textWatcher = doAfterTextChanged {
|
val textWatcher = doAfterTextChanged {
|
||||||
trySend(it).logErrors("textChangesFlow")
|
trySend(it).logErrors("textChangesFlow", logger)
|
||||||
}
|
}
|
||||||
awaitClose {
|
awaitClose {
|
||||||
Timber.d("textChangesFlow: flow is closing")
|
logger.d { "textChangesFlow: flow is closing" }
|
||||||
removeTextChangedListener(textWatcher)
|
removeTextChangedListener(textWatcher)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> ChannelResult<T>.logErrors(methodName: String): ChannelResult<T> {
|
fun <T> ChannelResult<T>.logErrors(methodName: String, logger: Logger): ChannelResult<T> {
|
||||||
onFailure { Timber.e(it, "$methodName: can't send event") }
|
onFailure { logger.e(it) { "$methodName: can't send event" } }
|
||||||
onClosed { Timber.e(it, "$methodName: flow has been closed") }
|
onClosed { logger.e(it) { "$methodName: flow has been closed" } }
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,29 +87,31 @@ fun EditText.checkIfInputIsEmpty(
|
|||||||
lifecycleOwner: LifecycleOwner,
|
lifecycleOwner: LifecycleOwner,
|
||||||
@StringRes stringId: Int,
|
@StringRes stringId: Int,
|
||||||
trim: Boolean = true,
|
trim: Boolean = true,
|
||||||
|
logger: Logger,
|
||||||
): String? {
|
): String? {
|
||||||
val input = if (trim) text?.trim() else text
|
val input = if (trim) text?.trim() else text
|
||||||
val text = input?.toString().orEmpty()
|
val text = input?.toString().orEmpty()
|
||||||
Timber.d("Input text is \"$text\"")
|
logger.d { "Input text is \"$text\"" }
|
||||||
return text.ifEmpty {
|
return text.ifEmpty {
|
||||||
inputLayout.error = resources.getString(stringId)
|
inputLayout.error = resources.getString(stringId)
|
||||||
lifecycleOwner.lifecycleScope.launch {
|
lifecycleOwner.lifecycleScope.launch {
|
||||||
waitUntilNotEmpty()
|
waitUntilNotEmpty(logger)
|
||||||
inputLayout.error = null
|
inputLayout.error = null
|
||||||
}
|
}
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun EditText.waitUntilNotEmpty() {
|
suspend fun EditText.waitUntilNotEmpty(logger: Logger) {
|
||||||
textChangesFlow().filterNotNull().first { it.isNotEmpty() }
|
textChangesFlow(logger).filterNotNull().first { it.isNotEmpty() }
|
||||||
Timber.v("waitUntilNotEmpty() returned")
|
logger.v { "waitUntilNotEmpty() returned" }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> SharedPreferences.prefsChangeFlow(
|
fun <T> SharedPreferences.prefsChangeFlow(
|
||||||
|
logger: Logger,
|
||||||
valueReader: SharedPreferences.() -> T,
|
valueReader: SharedPreferences.() -> T,
|
||||||
): Flow<T> = callbackFlow {
|
): Flow<T> = callbackFlow {
|
||||||
fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow")
|
fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow", logger)
|
||||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> sendValue() }
|
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> sendValue() }
|
||||||
sendValue()
|
sendValue()
|
||||||
registerOnSharedPreferenceChangeListener(listener)
|
registerOnSharedPreferenceChangeListener(listener)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import com.bumptech.glide.module.AppGlideModule
|
|||||||
import dagger.hilt.android.EntryPointAccessors.fromApplication
|
import dagger.hilt.android.EntryPointAccessors.fromApplication
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.di.GlideModuleEntryPoint
|
import gq.kirmanak.mealient.di.GlideModuleEntryPoint
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
@@ -18,13 +18,13 @@ class MealieGlideModule : AppGlideModule() {
|
|||||||
|
|
||||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||||
super.registerComponents(context, glide, registry)
|
super.registerComponents(context, glide, registry)
|
||||||
Timber.v("registerComponents() called with: context = $context, glide = $glide, registry = $registry")
|
getLogger(context).v { "registerComponents() called with: context = $context, glide = $glide, registry = $registry" }
|
||||||
replaceOkHttp(context, registry)
|
replaceOkHttp(context, registry)
|
||||||
appendRecipeLoader(registry, context)
|
appendRecipeLoader(registry, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun appendRecipeLoader(registry: Registry, context: Context) {
|
private fun appendRecipeLoader(registry: Registry, context: Context) {
|
||||||
Timber.v("appendRecipeLoader() called with: registry = $registry, context = $context")
|
getLogger(context).v { "appendRecipeLoader() called with: registry = $registry, context = $context" }
|
||||||
registry.append(
|
registry.append(
|
||||||
RecipeSummaryEntity::class.java,
|
RecipeSummaryEntity::class.java,
|
||||||
InputStream::class.java,
|
InputStream::class.java,
|
||||||
@@ -33,17 +33,15 @@ class MealieGlideModule : AppGlideModule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun replaceOkHttp(context: Context, registry: Registry) {
|
private fun replaceOkHttp(context: Context, registry: Registry) {
|
||||||
Timber.v("replaceOkHttp() called with: context = $context, registry = $registry")
|
getLogger(context).v { "replaceOkHttp() called with: context = $context, registry = $registry" }
|
||||||
val okHttp = getEntryPoint(context).provideOkHttp()
|
val okHttp = getEntryPoint(context).provideOkHttp()
|
||||||
registry.replace(
|
registry.replace(
|
||||||
GlideUrl::class.java,
|
GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttp)
|
||||||
InputStream::class.java,
|
|
||||||
OkHttpUrlLoader.Factory(okHttp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEntryPoint(context: Context): GlideModuleEntryPoint {
|
private fun getEntryPoint(context: Context): GlideModuleEntryPoint =
|
||||||
Timber.v("getEntryPoint() called with: context = $context")
|
fromApplication(context, GlideModuleEntryPoint::class.java)
|
||||||
return fromApplication(context, GlideModuleEntryPoint::class.java)
|
|
||||||
}
|
private fun getLogger(context: Context): Logger = getEntryPoint(context).provideLogger()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,18 +13,23 @@ import com.google.android.material.shape.MaterialShapeDrawable
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.databinding.MainActivityBinding
|
import gq.kirmanak.mealient.databinding.MainActivityBinding
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var binding: MainActivityBinding
|
private lateinit var binding: MainActivityBinding
|
||||||
private val viewModel by viewModels<MainActivityViewModel>()
|
private val viewModel by viewModels<MainActivityViewModel>()
|
||||||
private val title: String by lazy { getString(R.string.app_name) }
|
private val title: String by lazy { getString(R.string.app_name) }
|
||||||
private val uiState: MainActivityUiState get() = viewModel.uiState
|
private val uiState: MainActivityUiState get() = viewModel.uiState
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
|
||||||
binding = MainActivityBinding.inflate(layoutInflater)
|
binding = MainActivityBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
@@ -36,7 +41,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
|
private fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
|
||||||
Timber.v("onNavigationItemSelected() called with: menuItem = $menuItem")
|
logger.v { "onNavigationItemSelected() called with: menuItem = $menuItem" }
|
||||||
menuItem.isChecked = true
|
menuItem.isChecked = true
|
||||||
val deepLink = when (menuItem.itemId) {
|
val deepLink = when (menuItem.itemId) {
|
||||||
R.id.add_recipe -> ADD_RECIPE_DEEP_LINK
|
R.id.add_recipe -> ADD_RECIPE_DEEP_LINK
|
||||||
@@ -49,19 +54,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onUiStateChange(uiState: MainActivityUiState) {
|
private fun onUiStateChange(uiState: MainActivityUiState) {
|
||||||
Timber.v("onUiStateChange() called with: uiState = $uiState")
|
logger.v { "onUiStateChange() called with: uiState = $uiState" }
|
||||||
supportActionBar?.title = if (uiState.titleVisible) title else null
|
supportActionBar?.title = if (uiState.titleVisible) title else null
|
||||||
binding.navigationView.isVisible = uiState.navigationVisible
|
binding.navigationView.isVisible = uiState.navigationVisible
|
||||||
invalidateOptionsMenu()
|
invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setToolbarRoundCorner() {
|
private fun setToolbarRoundCorner() {
|
||||||
Timber.v("setToolbarRoundCorner() called")
|
logger.v { "setToolbarRoundCorner() called" }
|
||||||
val drawables = listOf(
|
val drawables = listOf(
|
||||||
binding.toolbarHolder.background as? MaterialShapeDrawable,
|
binding.toolbarHolder.background as? MaterialShapeDrawable,
|
||||||
binding.toolbar.background as? MaterialShapeDrawable,
|
binding.toolbar.background as? MaterialShapeDrawable,
|
||||||
)
|
)
|
||||||
Timber.d("setToolbarRoundCorner: drawables = $drawables")
|
logger.d { "setToolbarRoundCorner: drawables = $drawables" }
|
||||||
val radius = resources.getDimension(R.dimen.main_activity_toolbar_corner_radius)
|
val radius = resources.getDimension(R.dimen.main_activity_toolbar_corner_radius)
|
||||||
for (drawable in drawables) {
|
for (drawable in drawables) {
|
||||||
drawable?.apply {
|
drawable?.apply {
|
||||||
@@ -72,7 +77,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
Timber.v("onCreateOptionsMenu() called with: menu = $menu")
|
logger.v { "onCreateOptionsMenu() called with: menu = $menu" }
|
||||||
menuInflater.inflate(R.menu.main_toolbar, menu)
|
menuInflater.inflate(R.menu.main_toolbar, menu)
|
||||||
menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
|
menu.findItem(R.id.logout).isVisible = uiState.canShowLogout
|
||||||
menu.findItem(R.id.login).isVisible = uiState.canShowLogin
|
menu.findItem(R.id.login).isVisible = uiState.canShowLogin
|
||||||
@@ -80,7 +85,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
Timber.v("onOptionsItemSelected() called with: item = $item")
|
logger.v { "onOptionsItemSelected() called with: item = $item" }
|
||||||
val result = when (item.itemId) {
|
val result = when (item.itemId) {
|
||||||
R.id.login -> {
|
R.id.login -> {
|
||||||
navigateDeepLink(AUTH_DEEP_LINK)
|
navigateDeepLink(AUTH_DEEP_LINK)
|
||||||
@@ -96,7 +101,7 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateDeepLink(deepLink: String) {
|
private fun navigateDeepLink(deepLink: String) {
|
||||||
Timber.v("navigateDeepLink() called with: deepLink = $deepLink")
|
logger.v { "navigateDeepLink() called with: deepLink = $deepLink" }
|
||||||
findNavController(binding.navHost.id).navigate(deepLink.toUri())
|
findNavController(binding.navHost.id).navigate(deepLink.toUri())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ package gq.kirmanak.mealient.ui.activity
|
|||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainActivityViewModel @Inject constructor(
|
class MainActivityViewModel @Inject constructor(
|
||||||
private val authRepo: AuthRepo,
|
private val authRepo: AuthRepo,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableLiveData(MainActivityUiState())
|
private val _uiState = MutableLiveData(MainActivityUiState())
|
||||||
@@ -32,7 +33,7 @@ class MainActivityViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
Timber.v("logout() called")
|
logger.v { "logout() called" }
|
||||||
viewModelScope.launch { authRepo.logout() }
|
viewModelScope.launch { authRepo.logout() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,9 @@ import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding
|
|||||||
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding
|
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding
|
||||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||||
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
||||||
@@ -30,9 +31,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
private val viewModel by viewModels<AddRecipeViewModel>()
|
private val viewModel by viewModels<AddRecipeViewModel>()
|
||||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
activityViewModel.updateUiState {
|
activityViewModel.updateUiState {
|
||||||
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
|
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
|
||||||
}
|
}
|
||||||
@@ -42,12 +46,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun observeAddRecipeResult() {
|
private fun observeAddRecipeResult() {
|
||||||
Timber.v("observeAddRecipeResult() called")
|
logger.v { "observeAddRecipeResult() called" }
|
||||||
collectWhenViewResumed(viewModel.addRecipeResult, ::onRecipeSaveResult)
|
collectWhenViewResumed(viewModel.addRecipeResult, ::onRecipeSaveResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRecipeSaveResult(isSuccessful: Boolean) = with(binding) {
|
private fun onRecipeSaveResult(isSuccessful: Boolean) = with(binding) {
|
||||||
Timber.v("onRecipeSaveResult() called with: isSuccessful = $isSuccessful")
|
logger.v { "onRecipeSaveResult() called with: isSuccessful = $isSuccessful" }
|
||||||
|
|
||||||
listOf(clearButton, saveRecipeButton).forEach { it.isEnabled = true }
|
listOf(clearButton, saveRecipeButton).forEach { it.isEnabled = true }
|
||||||
|
|
||||||
@@ -60,12 +64,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() = with(binding) {
|
private fun setupViews() = with(binding) {
|
||||||
Timber.v("setupViews() called")
|
logger.v { "setupViews() called" }
|
||||||
saveRecipeButton.setOnClickListener {
|
saveRecipeButton.setOnClickListener {
|
||||||
recipeNameInput.checkIfInputIsEmpty(
|
recipeNameInput.checkIfInputIsEmpty(
|
||||||
inputLayout = recipeNameInputLayout,
|
inputLayout = recipeNameInputLayout,
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
stringId = R.string.fragment_add_recipe_name_error
|
stringId = R.string.fragment_add_recipe_name_error,
|
||||||
|
logger = logger,
|
||||||
) ?: return@setOnClickListener
|
) ?: return@setOnClickListener
|
||||||
|
|
||||||
listOf(saveRecipeButton, clearButton).forEach { it.isEnabled = false }
|
listOf(saveRecipeButton, clearButton).forEach { it.isEnabled = false }
|
||||||
@@ -98,7 +103,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun inflateInputRow(flow: Flow, @StringRes hintId: Int, text: String? = null) {
|
private fun inflateInputRow(flow: Flow, @StringRes hintId: Int, text: String? = null) {
|
||||||
Timber.v("inflateInputRow() called with: flow = $flow, hintId = $hintId, text = $text")
|
logger.v { "inflateInputRow() called with: flow = $flow, hintId = $hintId, text = $text" }
|
||||||
val fragmentRoot = binding.holder
|
val fragmentRoot = binding.holder
|
||||||
val inputBinding = ViewSingleInputBinding.inflate(layoutInflater, fragmentRoot, false)
|
val inputBinding = ViewSingleInputBinding.inflate(layoutInflater, fragmentRoot, false)
|
||||||
val root = inputBinding.root
|
val root = inputBinding.root
|
||||||
@@ -116,7 +121,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveValues() = with(binding) {
|
private fun saveValues() = with(binding) {
|
||||||
Timber.v("saveValues() called")
|
logger.v { "saveValues() called" }
|
||||||
val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) }
|
val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) }
|
||||||
val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) }
|
val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) }
|
||||||
val settings = AddRecipeSettings(
|
val settings = AddRecipeSettings(
|
||||||
@@ -144,7 +149,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) {
|
private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) {
|
||||||
Timber.v("onSavedInputLoaded() called with: request = $request")
|
logger.v { "onSavedInputLoaded() called with: request = $request" }
|
||||||
recipeNameInput.setText(request.name)
|
recipeNameInput.setText(request.name)
|
||||||
recipeDescriptionInput.setText(request.description)
|
recipeDescriptionInput.setText(request.description)
|
||||||
recipeYieldInput.setText(request.recipeYield)
|
recipeYieldInput.setText(request.recipeYield)
|
||||||
@@ -159,13 +164,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun Iterable<String>.showIn(flow: Flow, @StringRes hintId: Int) {
|
private fun Iterable<String>.showIn(flow: Flow, @StringRes hintId: Int) {
|
||||||
Timber.v("showIn() called with: flow = $flow, hintId = $hintId")
|
logger.v { "showIn() called with: flow = $flow, hintId = $hintId" }
|
||||||
flow.removeAllViews()
|
flow.removeAllViews()
|
||||||
forEach { inflateInputRow(flow = flow, hintId = hintId, text = it) }
|
forEach { inflateInputRow(flow = flow, hintId = hintId, text = it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Flow.removeAllViews() {
|
private fun Flow.removeAllViews() {
|
||||||
Timber.v("removeAllViews() called")
|
logger.v { "removeAllViews() called" }
|
||||||
for (id in referencedIds.iterator()) {
|
for (id in referencedIds.iterator()) {
|
||||||
val view = binding.holder.findViewById<View>(id) ?: continue
|
val view = binding.holder.findViewById<View>(id) ?: continue
|
||||||
removeView(view)
|
removeView(view)
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AddRecipeViewModel @Inject constructor(
|
class AddRecipeViewModel @Inject constructor(
|
||||||
private val addRecipeRepo: AddRecipeRepo,
|
private val addRecipeRepo: AddRecipeRepo,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED)
|
private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED)
|
||||||
@@ -27,19 +28,19 @@ class AddRecipeViewModel @Inject constructor(
|
|||||||
get() = _preservedAddRecipeRequestChannel.receiveAsFlow()
|
get() = _preservedAddRecipeRequestChannel.receiveAsFlow()
|
||||||
|
|
||||||
fun loadPreservedRequest() {
|
fun loadPreservedRequest() {
|
||||||
Timber.v("loadPreservedRequest() called")
|
logger.v { "loadPreservedRequest() called" }
|
||||||
viewModelScope.launch { doLoadPreservedRequest() }
|
viewModelScope.launch { doLoadPreservedRequest() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun doLoadPreservedRequest() {
|
private suspend fun doLoadPreservedRequest() {
|
||||||
Timber.v("doLoadPreservedRequest() called")
|
logger.v { "doLoadPreservedRequest() called" }
|
||||||
val request = addRecipeRepo.addRecipeRequestFlow.first()
|
val request = addRecipeRepo.addRecipeRequestFlow.first()
|
||||||
Timber.d("doLoadPreservedRequest: request = $request")
|
logger.d { "doLoadPreservedRequest: request = $request" }
|
||||||
_preservedAddRecipeRequestChannel.send(request)
|
_preservedAddRecipeRequestChannel.send(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
Timber.v("clear() called")
|
logger.v { "clear() called" }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
addRecipeRepo.clear()
|
addRecipeRepo.clear()
|
||||||
doLoadPreservedRequest()
|
doLoadPreservedRequest()
|
||||||
@@ -47,16 +48,16 @@ class AddRecipeViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun preserve(request: AddRecipeRequest) {
|
fun preserve(request: AddRecipeRequest) {
|
||||||
Timber.v("preserve() called with: request = $request")
|
logger.v { "preserve() called with: request = $request" }
|
||||||
viewModelScope.launch { addRecipeRepo.preserve(request) }
|
viewModelScope.launch { addRecipeRepo.preserve(request) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveRecipe() {
|
fun saveRecipe() {
|
||||||
Timber.v("saveRecipe() called")
|
logger.v { "saveRecipe() called" }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val isSuccessful = runCatchingExceptCancel { addRecipeRepo.saveRecipe() }
|
val isSuccessful = runCatchingExceptCancel { addRecipeRepo.saveRecipe() }
|
||||||
.fold(onSuccess = { true }, onFailure = { false })
|
.fold(onSuccess = { true }, onFailure = { false })
|
||||||
Timber.d("saveRecipe: isSuccessful = $isSuccessful")
|
logger.d { "saveRecipe: isSuccessful = $isSuccessful" }
|
||||||
_addRecipeResultChannel.send(isSuccessful)
|
_addRecipeResultChannel.send(isSuccessful)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,24 @@ import gq.kirmanak.mealient.R
|
|||||||
import gq.kirmanak.mealient.data.network.NetworkError
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
||||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.OperationUiState
|
import gq.kirmanak.mealient.ui.OperationUiState
|
||||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentAuthenticationBinding::bind)
|
private val binding by viewBinding(FragmentAuthenticationBinding::bind)
|
||||||
private val viewModel by viewModels<AuthenticationViewModel>()
|
private val viewModel by viewModels<AuthenticationViewModel>()
|
||||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
binding.button.setOnClickListener { onLoginClicked() }
|
binding.button.setOnClickListener { onLoginClicked() }
|
||||||
activityViewModel.updateUiState {
|
activityViewModel.updateUiState {
|
||||||
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false)
|
it.copy(loginButtonVisible = false, titleVisible = true, navigationVisible = false)
|
||||||
@@ -33,12 +38,13 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoginClicked(): Unit = with(binding) {
|
private fun onLoginClicked(): Unit = with(binding) {
|
||||||
Timber.v("onLoginClicked() called")
|
logger.v { "onLoginClicked() called" }
|
||||||
|
|
||||||
val email: String = emailInput.checkIfInputIsEmpty(
|
val email: String = emailInput.checkIfInputIsEmpty(
|
||||||
inputLayout = emailInputLayout,
|
inputLayout = emailInputLayout,
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
stringId = R.string.fragment_authentication_email_input_empty,
|
stringId = R.string.fragment_authentication_email_input_empty,
|
||||||
|
logger = logger,
|
||||||
) ?: return
|
) ?: return
|
||||||
|
|
||||||
val pass: String = passwordInput.checkIfInputIsEmpty(
|
val pass: String = passwordInput.checkIfInputIsEmpty(
|
||||||
@@ -46,13 +52,14 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) {
|
|||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
stringId = R.string.fragment_authentication_password_input_empty,
|
stringId = R.string.fragment_authentication_password_input_empty,
|
||||||
trim = false,
|
trim = false,
|
||||||
|
logger = logger,
|
||||||
) ?: return
|
) ?: return
|
||||||
|
|
||||||
viewModel.authenticate(email, pass)
|
viewModel.authenticate(email, pass)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
|
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
|
||||||
Timber.v("onUiStateChange() called with: authUiState = $uiState")
|
logger.v { "onUiStateChange() called with: authUiState = $uiState" }
|
||||||
if (uiState.isSuccess) {
|
if (uiState.isSuccess) {
|
||||||
findNavController().popBackStack()
|
findNavController().popBackStack()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -7,21 +7,22 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.OperationUiState
|
import gq.kirmanak.mealient.ui.OperationUiState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AuthenticationViewModel @Inject constructor(
|
class AuthenticationViewModel @Inject constructor(
|
||||||
private val authRepo: AuthRepo,
|
private val authRepo: AuthRepo,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
||||||
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
|
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
|
||||||
|
|
||||||
fun authenticate(email: String, password: String) {
|
fun authenticate(email: String, password: String) {
|
||||||
Timber.v("authenticate() called with: email = $email, password = $password")
|
logger.v { "authenticate() called with: email = $email, password = $password" }
|
||||||
_uiState.value = OperationUiState.Progress()
|
_uiState.value = OperationUiState.Progress()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
|
val result = runCatchingExceptCancel { authRepo.authenticate(email, password) }
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import gq.kirmanak.mealient.R
|
|||||||
import gq.kirmanak.mealient.data.network.NetworkError
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
||||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.OperationUiState
|
import gq.kirmanak.mealient.ui.OperationUiState
|
||||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
||||||
@@ -23,9 +24,12 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
|||||||
private val viewModel by viewModels<BaseURLViewModel>()
|
private val viewModel by viewModels<BaseURLViewModel>()
|
||||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
binding.button.setOnClickListener(::onProceedClick)
|
binding.button.setOnClickListener(::onProceedClick)
|
||||||
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
|
viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange)
|
||||||
activityViewModel.updateUiState {
|
activityViewModel.updateUiState {
|
||||||
@@ -34,17 +38,18 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onProceedClick(view: View) {
|
private fun onProceedClick(view: View) {
|
||||||
Timber.v("onProceedClick() called with: view = $view")
|
logger.v { "onProceedClick() called with: view = $view" }
|
||||||
val url = binding.urlInput.checkIfInputIsEmpty(
|
val url = binding.urlInput.checkIfInputIsEmpty(
|
||||||
inputLayout = binding.urlInputLayout,
|
inputLayout = binding.urlInputLayout,
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner,
|
||||||
stringId = R.string.fragment_baseurl_url_input_empty,
|
stringId = R.string.fragment_baseurl_url_input_empty,
|
||||||
|
logger = logger,
|
||||||
) ?: return
|
) ?: return
|
||||||
viewModel.saveBaseUrl(url)
|
viewModel.saveBaseUrl(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
|
private fun onUiStateChange(uiState: OperationUiState<Unit>) = with(binding) {
|
||||||
Timber.v("onUiStateChange() called with: uiState = $uiState")
|
logger.v { "onUiStateChange() called with: uiState = $uiState" }
|
||||||
if (uiState.isSuccess) {
|
if (uiState.isSuccess) {
|
||||||
findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment())
|
findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment())
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,22 +8,23 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.OperationUiState
|
import gq.kirmanak.mealient.ui.OperationUiState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BaseURLViewModel @Inject constructor(
|
class BaseURLViewModel @Inject constructor(
|
||||||
private val baseURLStorage: BaseURLStorage,
|
private val baseURLStorage: BaseURLStorage,
|
||||||
private val versionDataSource: VersionDataSource,
|
private val versionDataSource: VersionDataSource,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
private val _uiState = MutableLiveData<OperationUiState<Unit>>(OperationUiState.Initial())
|
||||||
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
|
val uiState: LiveData<OperationUiState<Unit>> get() = _uiState
|
||||||
|
|
||||||
fun saveBaseUrl(baseURL: String) {
|
fun saveBaseUrl(baseURL: String) {
|
||||||
Timber.v("saveBaseUrl() called with: baseURL = $baseURL")
|
logger.v { "saveBaseUrl() called with: baseURL = $baseURL" }
|
||||||
_uiState.value = OperationUiState.Progress()
|
_uiState.value = OperationUiState.Progress()
|
||||||
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
|
val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) }
|
||||||
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
|
val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL)
|
||||||
@@ -31,13 +32,13 @@ class BaseURLViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun checkBaseURL(baseURL: String) {
|
private suspend fun checkBaseURL(baseURL: String) {
|
||||||
Timber.v("checkBaseURL() called with: baseURL = $baseURL")
|
logger.v { "checkBaseURL() called with: baseURL = $baseURL" }
|
||||||
val result = runCatchingExceptCancel {
|
val result = runCatchingExceptCancel {
|
||||||
// If it returns proper version info then it must be a Mealie
|
// If it returns proper version info then it must be a Mealie
|
||||||
versionDataSource.getVersionInfo(baseURL)
|
versionDataSource.getVersionInfo(baseURL)
|
||||||
baseURLStorage.storeBaseURL(baseURL)
|
baseURLStorage.storeBaseURL(baseURL)
|
||||||
}
|
}
|
||||||
Timber.i("checkBaseURL: result is $result")
|
logger.i { "checkBaseURL: result is $result" }
|
||||||
_uiState.value = OperationUiState.fromResult(result)
|
_uiState.value = OperationUiState.fromResult(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,40 +10,45 @@ import by.kirich1409.viewbindingdelegate.viewBinding
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding
|
import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
|
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) {
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentDisclaimerBinding::bind)
|
private val binding by viewBinding(FragmentDisclaimerBinding::bind)
|
||||||
private val viewModel by viewModels<DisclaimerViewModel>()
|
private val viewModel by viewModels<DisclaimerViewModel>()
|
||||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
|
||||||
viewModel.isAccepted.observe(this, ::onAcceptStateChange)
|
viewModel.isAccepted.observe(this, ::onAcceptStateChange)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAcceptStateChange(isAccepted: Boolean) {
|
private fun onAcceptStateChange(isAccepted: Boolean) {
|
||||||
Timber.v("onAcceptStateChange() called with: isAccepted = $isAccepted")
|
logger.v { "onAcceptStateChange() called with: isAccepted = $isAccepted" }
|
||||||
if (isAccepted) navigateNext()
|
if (isAccepted) navigateNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateNext() {
|
private fun navigateNext() {
|
||||||
Timber.v("navigateNext() called")
|
logger.v { "navigateNext() called" }
|
||||||
findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment())
|
findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
binding.okay.setOnClickListener {
|
binding.okay.setOnClickListener {
|
||||||
Timber.v("onViewCreated: okay clicked")
|
logger.v { "onViewCreated: okay clicked" }
|
||||||
viewModel.acceptDisclaimer()
|
viewModel.acceptDisclaimer()
|
||||||
}
|
}
|
||||||
viewModel.okayCountDown.observe(viewLifecycleOwner) {
|
viewModel.okayCountDown.observe(viewLifecycleOwner) {
|
||||||
Timber.d("onViewCreated: new count $it")
|
logger.d { "onViewCreated: new count $it" }
|
||||||
binding.okay.text = if (it > 0) resources.getQuantityString(
|
binding.okay.text = if (it > 0) resources.getQuantityString(
|
||||||
R.plurals.fragment_disclaimer_button_okay_timer, it, it
|
R.plurals.fragment_disclaimer_button_okay_timer, it, it
|
||||||
) else getString(R.string.fragment_disclaimer_button_okay)
|
) else getString(R.string.fragment_disclaimer_button_okay)
|
||||||
|
|||||||
@@ -4,19 +4,20 @@ import androidx.annotation.VisibleForTesting
|
|||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.take
|
import kotlinx.coroutines.flow.take
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class DisclaimerViewModel @Inject constructor(
|
class DisclaimerViewModel @Inject constructor(
|
||||||
private val disclaimerStorage: DisclaimerStorage
|
private val disclaimerStorage: DisclaimerStorage,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val isAccepted: LiveData<Boolean>
|
val isAccepted: LiveData<Boolean>
|
||||||
@@ -26,12 +27,12 @@ class DisclaimerViewModel @Inject constructor(
|
|||||||
private var isCountDownStarted = false
|
private var isCountDownStarted = false
|
||||||
|
|
||||||
fun acceptDisclaimer() {
|
fun acceptDisclaimer() {
|
||||||
Timber.v("acceptDisclaimer() called")
|
logger.v { "acceptDisclaimer() called" }
|
||||||
viewModelScope.launch { disclaimerStorage.acceptDisclaimer() }
|
viewModelScope.launch { disclaimerStorage.acceptDisclaimer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startCountDown() {
|
fun startCountDown() {
|
||||||
Timber.v("startCountDown() called")
|
logger.v { "startCountDown() called" }
|
||||||
if (isCountDownStarted) return
|
if (isCountDownStarted) return
|
||||||
isCountDownStarted = true
|
isCountDownStarted = true
|
||||||
tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS)
|
tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS)
|
||||||
@@ -48,7 +49,7 @@ class DisclaimerViewModel @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
fun tickerFlow(period: Long, timeUnit: TimeUnit) = flow {
|
fun tickerFlow(period: Long, timeUnit: TimeUnit) = flow {
|
||||||
Timber.v("tickerFlow() called with: period = $period, timeUnit = $timeUnit")
|
logger.v { "tickerFlow() called with: period = $period, timeUnit = $timeUnit" }
|
||||||
val periodMillis = timeUnit.toMillis(period)
|
val periodMillis = timeUnit.toMillis(period)
|
||||||
var counter = 0
|
var counter = 0
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -4,25 +4,42 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class RecipeViewHolder(
|
class RecipeViewHolder private constructor(
|
||||||
|
private val logger: Logger,
|
||||||
private val binding: ViewHolderRecipeBinding,
|
private val binding: ViewHolderRecipeBinding,
|
||||||
private val recipeImageLoader: RecipeImageLoader,
|
private val recipeImageLoader: RecipeImageLoader,
|
||||||
private val clickListener: (RecipeSummaryEntity) -> Unit,
|
private val clickListener: (RecipeSummaryEntity) -> Unit,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
recipeImageLoader: RecipeImageLoader,
|
||||||
|
binding: ViewHolderRecipeBinding,
|
||||||
|
clickListener: (RecipeSummaryEntity) -> Unit,
|
||||||
|
) = RecipeViewHolder(logger, binding, recipeImageLoader, clickListener)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private val loadingPlaceholder by lazy {
|
private val loadingPlaceholder by lazy {
|
||||||
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(item: RecipeSummaryEntity?) {
|
fun bind(item: RecipeSummaryEntity?) {
|
||||||
Timber.v("bind() called with: item = $item")
|
logger.v { "bind() called with: item = $item" }
|
||||||
binding.name.text = item?.name ?: loadingPlaceholder
|
binding.name.text = item?.name ?: loadingPlaceholder
|
||||||
recipeImageLoader.loadRecipeImage(binding.image, item)
|
recipeImageLoader.loadRecipeImage(binding.image, item)
|
||||||
item?.let { entity ->
|
item?.let { entity ->
|
||||||
binding.root.setOnClickListener {
|
binding.root.setOnClickListener {
|
||||||
Timber.d("bind: item clicked $entity")
|
logger.d { "bind: item clicked $entity" }
|
||||||
clickListener(entity)
|
clickListener(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,27 +13,34 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
|||||||
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
import gq.kirmanak.mealient.databinding.FragmentRecipesBinding
|
||||||
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
||||||
import gq.kirmanak.mealient.extensions.refreshRequestFlow
|
import gq.kirmanak.mealient.extensions.refreshRequestFlow
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
import gq.kirmanak.mealient.ui.activity.MainActivityViewModel
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
|
import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentRecipesBinding::bind)
|
private val binding by viewBinding(FragmentRecipesBinding::bind)
|
||||||
private val viewModel by viewModels<RecipeViewModel>()
|
private val viewModel by viewModels<RecipeViewModel>()
|
||||||
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
private val activityViewModel by activityViewModels<MainActivityViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var recipeImageLoader: RecipeImageLoader
|
lateinit var recipeImageLoader: RecipeImageLoader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var recipePagingAdapterFactory: RecipesPagingAdapter.Factory
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var recipePreloaderFactory: RecipePreloaderFactory
|
lateinit var recipePreloaderFactory: RecipePreloaderFactory
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
activityViewModel.updateUiState {
|
activityViewModel.updateUiState {
|
||||||
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
|
it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true)
|
||||||
}
|
}
|
||||||
@@ -41,7 +48,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
|
private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) {
|
||||||
Timber.v("navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity")
|
logger.v { "navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity" }
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment(
|
RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment(
|
||||||
recipeSlug = recipeSummaryEntity.slug,
|
recipeSlug = recipeSummaryEntity.slug,
|
||||||
@@ -51,29 +58,32 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupRecipeAdapter() {
|
private fun setupRecipeAdapter() {
|
||||||
Timber.v("setupRecipeAdapter() called")
|
logger.v { "setupRecipeAdapter() called" }
|
||||||
val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo)
|
val recipesAdapter = recipePagingAdapterFactory.build(
|
||||||
|
recipeImageLoader = recipeImageLoader,
|
||||||
|
clickListener = ::navigateToRecipeInfo
|
||||||
|
)
|
||||||
with(binding.recipes) {
|
with(binding.recipes) {
|
||||||
adapter = recipesAdapter
|
adapter = recipesAdapter
|
||||||
addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
|
addOnScrollListener(recipePreloaderFactory.create(recipesAdapter))
|
||||||
}
|
}
|
||||||
collectWhenViewResumed(viewModel.pagingData) {
|
collectWhenViewResumed(viewModel.pagingData) {
|
||||||
Timber.v("setupRecipeAdapter: received data update")
|
logger.v { "setupRecipeAdapter: received data update" }
|
||||||
recipesAdapter.submitData(lifecycle, it)
|
recipesAdapter.submitData(lifecycle, it)
|
||||||
}
|
}
|
||||||
collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) {
|
collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) {
|
||||||
Timber.v("setupRecipeAdapter: pages updated")
|
logger.v { "setupRecipeAdapter: pages updated" }
|
||||||
binding.refresher.isRefreshing = false
|
binding.refresher.isRefreshing = false
|
||||||
}
|
}
|
||||||
collectWhenViewResumed(binding.refresher.refreshRequestFlow()) {
|
collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) {
|
||||||
Timber.v("setupRecipeAdapter: received refresh request")
|
logger.v { "setupRecipeAdapter: received refresh request" }
|
||||||
recipesAdapter.refresh()
|
recipesAdapter.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Timber.v("onDestroyView() called")
|
logger.v { "onDestroyView() called" }
|
||||||
// Prevent RV leaking through mObservers list in adapter
|
// Prevent RV leaking through mObservers list in adapter
|
||||||
binding.recipes.adapter = null
|
binding.recipes.adapter = null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,40 @@ import androidx.paging.PagingDataAdapter
|
|||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class RecipesPagingAdapter(
|
class RecipesPagingAdapter private constructor(
|
||||||
|
private val logger: Logger,
|
||||||
private val recipeImageLoader: RecipeImageLoader,
|
private val recipeImageLoader: RecipeImageLoader,
|
||||||
|
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
|
||||||
private val clickListener: (RecipeSummaryEntity) -> Unit
|
private val clickListener: (RecipeSummaryEntity) -> Unit
|
||||||
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
) : PagingDataAdapter<RecipeSummaryEntity, RecipeViewHolder>(RecipeDiffCallback) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
private val recipeViewHolderFactory: RecipeViewHolder.Factory,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
recipeImageLoader: RecipeImageLoader,
|
||||||
|
clickListener: (RecipeSummaryEntity) -> Unit,
|
||||||
|
) = RecipesPagingAdapter(logger, recipeImageLoader, recipeViewHolderFactory, clickListener)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecipeViewHolder, position: Int) {
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
holder.bind(item)
|
holder.bind(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeViewHolder {
|
||||||
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
|
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
|
val binding = ViewHolderRecipeBinding.inflate(inflater, parent, false)
|
||||||
return RecipeViewHolder(binding, recipeImageLoader, clickListener)
|
return recipeViewHolderFactory.build(recipeImageLoader, binding, clickListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {
|
private object RecipeDiffCallback : DiffUtil.ItemCallback<RecipeSummaryEntity>() {
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ import com.bumptech.glide.Glide
|
|||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import dagger.hilt.android.scopes.FragmentScoped
|
import dagger.hilt.android.scopes.FragmentScoped
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@FragmentScoped
|
@FragmentScoped
|
||||||
class RecipeImageLoaderImpl @Inject constructor(
|
class RecipeImageLoaderImpl @Inject constructor(
|
||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
private val requestOptions: RequestOptions,
|
private val requestOptions: RequestOptions,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecipeImageLoader {
|
) : RecipeImageLoader {
|
||||||
|
|
||||||
override fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?) {
|
override fun loadRecipeImage(view: ImageView, recipe: RecipeSummaryEntity?) {
|
||||||
Timber.v("loadRecipeImage() called with: view = $view, recipe = $recipe")
|
logger.v { "loadRecipeImage() called with: view = $view, recipe = $recipe" }
|
||||||
Glide.with(fragment).load(recipe).apply(requestOptions).into(view)
|
Glide.with(fragment).load(recipe).apply(requestOptions).into(view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,16 +7,32 @@ import com.bumptech.glide.load.model.ModelLoader
|
|||||||
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
||||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import timber.log.Timber
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class RecipeModelLoader(
|
class RecipeModelLoader private constructor(
|
||||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||||
|
private val logger: Logger,
|
||||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||||
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||||
|
private val logger: Logger,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun build(
|
||||||
|
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||||
|
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||||
|
) = RecipeModelLoader(recipeImageUrlProvider, logger, concreteLoader, cache)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun handles(model: RecipeSummaryEntity): Boolean = true
|
override fun handles(model: RecipeSummaryEntity): Boolean = true
|
||||||
|
|
||||||
override fun getUrl(
|
override fun getUrl(
|
||||||
@@ -25,7 +41,7 @@ class RecipeModelLoader(
|
|||||||
height: Int,
|
height: Int,
|
||||||
options: Options?
|
options: Options?
|
||||||
): String? {
|
): String? {
|
||||||
Timber.v("getUrl() called with: model = $model, width = $width, height = $height, options = $options")
|
logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" }
|
||||||
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) }
|
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
package gq.kirmanak.mealient.ui.recipes.images
|
package gq.kirmanak.mealient.ui.recipes.images
|
||||||
|
|
||||||
import com.bumptech.glide.load.model.*
|
import com.bumptech.glide.load.model.*
|
||||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class RecipeModelLoaderFactory @Inject constructor(
|
class RecipeModelLoaderFactory @Inject constructor(
|
||||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
private val recipeModelLoaderFactory: RecipeModelLoader.Factory,
|
||||||
|
private val logger: Logger,
|
||||||
) : ModelLoaderFactory<RecipeSummaryEntity, InputStream> {
|
) : ModelLoaderFactory<RecipeSummaryEntity, InputStream> {
|
||||||
|
|
||||||
private val cache = ModelCache<RecipeSummaryEntity, GlideUrl>()
|
private val cache = ModelCache<RecipeSummaryEntity, GlideUrl>()
|
||||||
|
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<RecipeSummaryEntity, InputStream> {
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<RecipeSummaryEntity, InputStream> {
|
||||||
Timber.v("build() called with: multiFactory = $multiFactory")
|
logger.v { "build() called with: multiFactory = $multiFactory" }
|
||||||
val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java)
|
val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java)
|
||||||
return RecipeModelLoader(recipeImageUrlProvider, concreteLoader, cache)
|
return recipeModelLoaderFactory.build(concreteLoader, cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun teardown() {
|
override fun teardown() {
|
||||||
|
|||||||
@@ -8,22 +8,23 @@ import com.bumptech.glide.RequestBuilder
|
|||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import dagger.hilt.android.scopes.FragmentScoped
|
import dagger.hilt.android.scopes.FragmentScoped
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RecipePreloadModelProvider(
|
class RecipePreloadModelProvider(
|
||||||
private val adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
private val adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
private val requestOptions: RequestOptions,
|
private val requestOptions: RequestOptions,
|
||||||
|
private val logger: Logger,
|
||||||
) : ListPreloader.PreloadModelProvider<RecipeSummaryEntity> {
|
) : ListPreloader.PreloadModelProvider<RecipeSummaryEntity> {
|
||||||
|
|
||||||
override fun getPreloadItems(position: Int): List<RecipeSummaryEntity> {
|
override fun getPreloadItems(position: Int): List<RecipeSummaryEntity> {
|
||||||
Timber.v("getPreloadItems() called with: position = $position")
|
logger.v { "getPreloadItems() called with: position = $position" }
|
||||||
return adapter.peek(position)?.let { listOf(it) } ?: emptyList()
|
return adapter.peek(position)?.let { listOf(it) } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPreloadRequestBuilder(item: RecipeSummaryEntity): RequestBuilder<*> {
|
override fun getPreloadRequestBuilder(item: RecipeSummaryEntity): RequestBuilder<*> {
|
||||||
Timber.v("getPreloadRequestBuilder() called with: item = $item")
|
logger.v { "getPreloadRequestBuilder() called with: item = $item" }
|
||||||
return Glide.with(fragment).load(item).apply(requestOptions)
|
return Glide.with(fragment).load(item).apply(requestOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +32,11 @@ class RecipePreloadModelProvider(
|
|||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
private val fragment: Fragment,
|
private val fragment: Fragment,
|
||||||
private val requestOptions: RequestOptions,
|
private val requestOptions: RequestOptions,
|
||||||
|
private val logger: Logger,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun create(
|
fun create(
|
||||||
adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
adapter: PagingDataAdapter<RecipeSummaryEntity, *>,
|
||||||
) = RecipePreloadModelProvider(adapter, fragment, requestOptions)
|
) = RecipePreloadModelProvider(adapter, fragment, requestOptions, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,8 +14,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
|
import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
@@ -24,8 +24,17 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
|||||||
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
|
private val binding by viewBinding(FragmentRecipeInfoBinding::bind)
|
||||||
private val arguments by navArgs<RecipeInfoFragmentArgs>()
|
private val arguments by navArgs<RecipeInfoFragmentArgs>()
|
||||||
private val viewModel by viewModels<RecipeInfoViewModel>()
|
private val viewModel by viewModels<RecipeInfoViewModel>()
|
||||||
private val ingredientsAdapter = RecipeIngredientsAdapter()
|
private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() }
|
||||||
private val instructionsAdapter = RecipeInstructionsAdapter()
|
private val instructionsAdapter by lazy { recipeInstructionsAdapterFactory.build() }
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var recipeInstructionsAdapterFactory: RecipeInstructionsAdapter.Factory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var recipeIngredientsAdapterFactory: RecipeIngredientsAdapter.Factory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var recipeImageLoader: RecipeImageLoader
|
lateinit var recipeImageLoader: RecipeImageLoader
|
||||||
@@ -35,13 +44,13 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
|||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
Timber.v("onCreateView() called")
|
logger.v { "onCreateView() called" }
|
||||||
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
|
return FragmentRecipeInfoBinding.inflate(inflater, container, false).root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called")
|
logger.v { "onViewCreated() called" }
|
||||||
|
|
||||||
with(binding) {
|
with(binding) {
|
||||||
ingredientsList.adapter = ingredientsAdapter
|
ingredientsList.adapter = ingredientsAdapter
|
||||||
@@ -55,7 +64,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
|
private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) {
|
||||||
Timber.v("onUiStateChange() called")
|
logger.v { "onUiStateChange() called" }
|
||||||
ingredientsHolder.isVisible = uiState.areIngredientsVisible
|
ingredientsHolder.isVisible = uiState.areIngredientsVisible
|
||||||
instructionsGroup.isVisible = uiState.areInstructionsVisible
|
instructionsGroup.isVisible = uiState.areInstructionsVisible
|
||||||
uiState.recipeInfo?.let {
|
uiState.recipeInfo?.let {
|
||||||
@@ -72,7 +81,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() {
|
|||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Timber.v("onDestroyView() called")
|
logger.v { "onDestroyView() called" }
|
||||||
// Prevent RV leaking through mObservers list in adapter
|
// Prevent RV leaking through mObservers list in adapter
|
||||||
with(binding) {
|
with(binding) {
|
||||||
ingredientsList.adapter = null
|
ingredientsList.adapter = null
|
||||||
|
|||||||
@@ -7,32 +7,33 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class RecipeInfoViewModel @Inject constructor(
|
class RecipeInfoViewModel @Inject constructor(
|
||||||
private val recipeRepo: RecipeRepo,
|
private val recipeRepo: RecipeRepo,
|
||||||
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _uiState = MutableLiveData(RecipeInfoUiState())
|
private val _uiState = MutableLiveData(RecipeInfoUiState())
|
||||||
val uiState: LiveData<RecipeInfoUiState> get() = _uiState
|
val uiState: LiveData<RecipeInfoUiState> get() = _uiState
|
||||||
|
|
||||||
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
|
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) {
|
||||||
Timber.v("loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug")
|
logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
|
||||||
_uiState.value = RecipeInfoUiState()
|
_uiState.value = RecipeInfoUiState()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
|
runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) }
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
Timber.d("loadRecipeInfo: received recipe info = $it")
|
logger.d { "loadRecipeInfo: received recipe info = $it" }
|
||||||
_uiState.value = RecipeInfoUiState(
|
_uiState.value = RecipeInfoUiState(
|
||||||
areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
|
areIngredientsVisible = it.recipeIngredients.isNotEmpty(),
|
||||||
areInstructionsVisible = it.recipeInstructions.isNotEmpty(),
|
areInstructionsVisible = it.recipeInstructions.isNotEmpty(),
|
||||||
recipeInfo = it,
|
recipeInfo = it,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.onFailure { Timber.e(it, "loadRecipeInfo: can't load recipe info") }
|
.onFailure { logger.e(it) { "loadRecipeInfo: can't load recipe info" } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,40 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||||
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
|
import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
|
import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class RecipeIngredientsAdapter :
|
class RecipeIngredientsAdapter private constructor(
|
||||||
ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
|
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
|
||||||
|
private val logger: Logger,
|
||||||
|
) : ListAdapter<RecipeIngredientEntity, RecipeIngredientViewHolder>(RecipeIngredientDiffCallback) {
|
||||||
|
|
||||||
class RecipeIngredientViewHolder(
|
@Singleton
|
||||||
private val binding: ViewHolderIngredientBinding
|
class Factory @Inject constructor(
|
||||||
|
private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory,
|
||||||
|
private val logger: Logger,
|
||||||
|
) {
|
||||||
|
fun build() = RecipeIngredientsAdapter(recipeIngredientViewHolderFactory, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecipeIngredientViewHolder private constructor(
|
||||||
|
private val binding: ViewHolderIngredientBinding,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun build(binding: ViewHolderIngredientBinding) =
|
||||||
|
RecipeIngredientViewHolder(binding, logger)
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(item: RecipeIngredientEntity) {
|
fun bind(item: RecipeIngredientEntity) {
|
||||||
Timber.v("bind() called with: item = $item")
|
logger.v { "bind() called with: item = $item" }
|
||||||
binding.checkBox.text = item.note
|
binding.checkBox.text = item.note
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,17 +58,17 @@ class RecipeIngredientsAdapter :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeIngredientViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeIngredientViewHolder {
|
||||||
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
|
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return RecipeIngredientViewHolder(
|
return recipeIngredientViewHolderFactory.build(
|
||||||
ViewHolderIngredientBinding.inflate(inflater, parent, false)
|
ViewHolderIngredientBinding.inflate(inflater, parent, false)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecipeIngredientViewHolder, position: Int) {
|
||||||
Timber.v("onBindViewHolder() called with: holder = $holder, position = $position")
|
logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
Timber.d("onBindViewHolder: item is $item")
|
logger.d { "onBindViewHolder: item is $item" }
|
||||||
holder.bind(item)
|
holder.bind(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,11 +8,23 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||||
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
|
import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
|
import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder
|
||||||
import timber.log.Timber
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class RecipeInstructionsAdapter :
|
class RecipeInstructionsAdapter private constructor(
|
||||||
ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) {
|
private val logger: Logger,
|
||||||
|
private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory,
|
||||||
|
) : ListAdapter<RecipeInstructionEntity, RecipeInstructionViewHolder>(RecipeInstructionDiffCallback) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(
|
||||||
|
private val logger: Logger,
|
||||||
|
private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory,
|
||||||
|
) {
|
||||||
|
fun build() = RecipeInstructionsAdapter(logger, recipeInstructionViewHolderFactory)
|
||||||
|
}
|
||||||
|
|
||||||
private object RecipeInstructionDiffCallback :
|
private object RecipeInstructionDiffCallback :
|
||||||
DiffUtil.ItemCallback<RecipeInstructionEntity>() {
|
DiffUtil.ItemCallback<RecipeInstructionEntity>() {
|
||||||
@@ -27,11 +39,19 @@ class RecipeInstructionsAdapter :
|
|||||||
): Boolean = oldItem == newItem
|
): Boolean = oldItem == newItem
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecipeInstructionViewHolder(
|
class RecipeInstructionViewHolder private constructor(
|
||||||
private val binding: ViewHolderInstructionBinding
|
private val binding: ViewHolderInstructionBinding,
|
||||||
|
private val logger: Logger,
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class Factory @Inject constructor(private val logger: Logger) {
|
||||||
|
fun build(binding: ViewHolderInstructionBinding) =
|
||||||
|
RecipeInstructionViewHolder(binding, logger)
|
||||||
|
}
|
||||||
|
|
||||||
fun bind(item: RecipeInstructionEntity, position: Int) {
|
fun bind(item: RecipeInstructionEntity, position: Int) {
|
||||||
Timber.v("bind() called with: item = $item, position = $position")
|
logger.v { "bind() called with: item = $item, position = $position" }
|
||||||
binding.step.text = binding.root.resources.getString(
|
binding.step.text = binding.root.resources.getString(
|
||||||
R.string.view_holder_recipe_instructions_step, position + 1
|
R.string.view_holder_recipe_instructions_step, position + 1
|
||||||
)
|
)
|
||||||
@@ -40,17 +60,17 @@ class RecipeInstructionsAdapter :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecipeInstructionViewHolder {
|
||||||
Timber.v("onCreateViewHolder() called with: parent = $parent, viewType = $viewType")
|
logger.v { "onCreateViewHolder() called with: parent = $parent, viewType = $viewType" }
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
return RecipeInstructionViewHolder(
|
return recipeInstructionViewHolderFactory.build(
|
||||||
ViewHolderInstructionBinding.inflate(inflater, parent, false)
|
ViewHolderInstructionBinding.inflate(inflater, parent, false),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecipeInstructionViewHolder, position: Int) {
|
||||||
Timber.v("onBindViewHolder() called with: holder = $holder, position = $position")
|
logger.v { "onBindViewHolder() called with: holder = $holder, position = $position" }
|
||||||
val item = getItem(position)
|
val item = getItem(position)
|
||||||
Timber.d("onBindViewHolder: item is $item")
|
logger.d { "onBindViewHolder: item is $item" }
|
||||||
holder.bind(item, position)
|
holder.bind(item, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,38 +11,43 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.extensions.setActionBarVisibility
|
import gq.kirmanak.mealient.extensions.setActionBarVisibility
|
||||||
import gq.kirmanak.mealient.extensions.setSystemUiVisibility
|
import gq.kirmanak.mealient.extensions.setSystemUiVisibility
|
||||||
import timber.log.Timber
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SplashFragment : Fragment(R.layout.fragment_splash) {
|
class SplashFragment : Fragment(R.layout.fragment_splash) {
|
||||||
|
|
||||||
private val viewModel by viewModels<SplashViewModel>()
|
private val viewModel by viewModels<SplashViewModel>()
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState")
|
logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" }
|
||||||
viewModel.nextDestination.observe(this, ::onNextDestination)
|
viewModel.nextDestination.observe(this, ::onNextDestination)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onNextDestination(navDirections: NavDirections) {
|
private fun onNextDestination(navDirections: NavDirections) {
|
||||||
Timber.v("onNextDestination() called with: navDirections = $navDirections")
|
logger.v { "onNextDestination() called with: navDirections = $navDirections" }
|
||||||
findNavController().navigate(navDirections)
|
findNavController().navigate(navDirections)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState")
|
logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" }
|
||||||
changeFullscreenState(true)
|
changeFullscreenState(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Timber.v("onDestroyView() called")
|
logger.v { "onDestroyView() called" }
|
||||||
changeFullscreenState(false)
|
changeFullscreenState(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun changeFullscreenState(isFullscreen: Boolean) {
|
private fun changeFullscreenState(isFullscreen: Boolean) {
|
||||||
Timber.v("changeFullscreenState() called with: isFullscreen = $isFullscreen")
|
logger.v { "changeFullscreenState() called with: isFullscreen = $isFullscreen" }
|
||||||
(activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen)
|
(activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen, logger)
|
||||||
activity?.setSystemUiVisibility(!isFullscreen)
|
activity?.setSystemUiVisibility(!isFullscreen, logger)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||||
import gq.kirmanak.mealient.data.network.NetworkError
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
@@ -22,13 +23,16 @@ class AddRecipeDataSourceImplTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var service: AddRecipeService
|
lateinit var service: AddRecipeService
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: AddRecipeDataSourceImpl
|
lateinit var subject: AddRecipeDataSourceImpl
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
coEvery { serviceProvider.provideService(any()) } returns service
|
coEvery { serviceProvider.provideService(any()) } returns service
|
||||||
subject = AddRecipeDataSourceImpl(serviceProvider)
|
subject = AddRecipeDataSourceImpl(serviceProvider, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = NetworkError.NotMealie::class)
|
@Test(expected = NetworkError.NotMealie::class)
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
package gq.kirmanak.mealient.data.add.models
|
package gq.kirmanak.mealient.data.add.models
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class AddRecipeRequestTest {
|
class AddRecipeRequestTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when construct from input then fills fields correctly`() {
|
fun `when construct from input then fills fields correctly`() {
|
||||||
val input = AddRecipeInput.newBuilder()
|
val input = AddRecipeDraft(
|
||||||
.setRecipeName("Recipe name")
|
recipeName = "Recipe name",
|
||||||
.setRecipeDescription("Recipe description")
|
recipeDescription = "Recipe description",
|
||||||
.setRecipeYield("Recipe yield")
|
recipeYield = "Recipe yield",
|
||||||
.addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2"))
|
recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"),
|
||||||
.addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2"))
|
recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"),
|
||||||
.setIsRecipePublic(false)
|
isRecipePublic = false,
|
||||||
.setAreCommentsDisabled(true)
|
areCommentsDisabled = true,
|
||||||
.build()
|
)
|
||||||
|
|
||||||
val expected = AddRecipeRequest(
|
val expected = AddRecipeRequest(
|
||||||
name = "Recipe name",
|
name = "Recipe name",
|
||||||
@@ -58,16 +59,16 @@ class AddRecipeRequestTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val expected = AddRecipeInput.newBuilder()
|
val expected = AddRecipeDraft(
|
||||||
.setRecipeName("Recipe name")
|
recipeName = "Recipe name",
|
||||||
.setRecipeDescription("Recipe description")
|
recipeDescription = "Recipe description",
|
||||||
.setRecipeYield("Recipe yield")
|
recipeYield = "Recipe yield",
|
||||||
.addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2"))
|
recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"),
|
||||||
.addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2"))
|
recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"),
|
||||||
.setIsRecipePublic(false)
|
isRecipePublic = false,
|
||||||
.setAreCommentsDisabled(true)
|
areCommentsDisabled = true,
|
||||||
.build()
|
)
|
||||||
|
|
||||||
assertThat(request.toInput()).isEqualTo(expected)
|
assertThat(request.toDraft()).isEqualTo(expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import gq.kirmanak.mealient.data.network.NetworkError.*
|
import gq.kirmanak.mealient.data.network.NetworkError.*
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.di.NetworkModule
|
import gq.kirmanak.mealient.di.NetworkModule
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
||||||
@@ -26,12 +27,15 @@ class AuthDataSourceImplTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var authServiceFactory: ServiceFactory<AuthService>
|
lateinit var authServiceFactory: ServiceFactory<AuthService>
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: AuthDataSourceImpl
|
lateinit var subject: AuthDataSourceImpl
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson())
|
subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson(), logger)
|
||||||
coEvery { authServiceFactory.provideService(any()) } returns authService
|
coEvery { authServiceFactory.provideService(any()) } returns authService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
|
||||||
@@ -26,12 +27,15 @@ class AuthRepoImplTest {
|
|||||||
@MockK(relaxUnitFun = true)
|
@MockK(relaxUnitFun = true)
|
||||||
lateinit var storage: AuthStorage
|
lateinit var storage: AuthStorage
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: AuthRepo
|
lateinit var subject: AuthRepo
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = AuthRepoImpl(storage, dataSource)
|
subject = AuthRepoImpl(storage, dataSource, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import gq.kirmanak.mealient.data.auth.AuthStorage
|
|||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.AUTH_HEADER_KEY
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.EMAIL_KEY
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.EMAIL_KEY
|
||||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.PASSWORD_KEY
|
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl.Companion.PASSWORD_KEY
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
|
||||||
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
import gq.kirmanak.mealient.test.HiltRobolectricTest
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -29,14 +32,18 @@ class AuthStorageImplTest : HiltRobolectricTest() {
|
|||||||
@ApplicationContext
|
@ApplicationContext
|
||||||
lateinit var context: Context
|
lateinit var context: Context
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: AuthStorage
|
lateinit var subject: AuthStorage
|
||||||
|
|
||||||
lateinit var sharedPreferences: SharedPreferences
|
lateinit var sharedPreferences: SharedPreferences
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE)
|
sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE)
|
||||||
subject = AuthStorageImpl(sharedPreferences)
|
subject = AuthStorageImpl(sharedPreferences, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.data.baseurl.impl.VersionResponse
|
|||||||
import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
||||||
import gq.kirmanak.mealient.data.network.NetworkError
|
import gq.kirmanak.mealient.data.network.NetworkError
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||||
import gq.kirmanak.mealient.test.toJsonResponseBody
|
import gq.kirmanak.mealient.test.toJsonResponseBody
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
@@ -28,12 +29,15 @@ class VersionDataSourceImplTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var versionServiceFactory: ServiceFactory<VersionService>
|
lateinit var versionServiceFactory: ServiceFactory<VersionService>
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: VersionDataSource
|
lateinit var subject: VersionDataSource
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = VersionDataSourceImpl(versionServiceFactory)
|
subject = VersionDataSourceImpl(versionServiceFactory, logger)
|
||||||
coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } returns versionService
|
coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } returns versionService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.network
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||||
import io.mockk.*
|
import io.mockk.*
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
@@ -27,12 +28,15 @@ class RetrofitServiceFactoryTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var versionService: VersionService
|
lateinit var versionService: VersionService
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: ServiceFactory<VersionService>
|
lateinit var subject: ServiceFactory<VersionService>
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = retrofitBuilder.createServiceFactory(baseURLStorage)
|
subject = retrofitBuilder.createServiceFactory(baseURLStorage, logger)
|
||||||
coEvery { retrofitBuilder.buildRetrofit(any()) } returns retrofit
|
coEvery { retrofitBuilder.buildRetrofit(any()) } returns retrofit
|
||||||
every { retrofit.create(eq(VersionService::class.java)) } returns versionService
|
every { retrofit.create(eq(VersionService::class.java)) } returns versionService
|
||||||
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL
|
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.data.recipes.impl
|
|||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
@@ -18,10 +19,13 @@ class RecipeImageUrlProviderImplTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var baseURLStorage: BaseURLStorage
|
lateinit var baseURLStorage: BaseURLStorage
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = RecipeImageUrlProviderImpl(baseURLStorage)
|
subject = RecipeImageUrlProviderImpl(baseURLStorage, logger)
|
||||||
prepareBaseURL("https://google.com/")
|
prepareBaseURL("https://google.com/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
|||||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
|
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.GET_CAKE_RESPONSE
|
import gq.kirmanak.mealient.test.RecipeImplTestData.GET_CAKE_RESPONSE
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
@@ -32,12 +33,15 @@ class RecipeRepoImplTest {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>
|
lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: RecipeRepo
|
lateinit var subject: RecipeRepo
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource)
|
subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
|
|||||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES
|
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
@@ -38,10 +39,13 @@ class RecipesRemoteMediatorTest {
|
|||||||
@MockK(relaxUnitFun = true)
|
@MockK(relaxUnitFun = true)
|
||||||
lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>
|
lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory<Int, RecipeSummaryEntity>
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = RecipesRemoteMediator(storage, dataSource, pagingSourceFactory)
|
subject = RecipesRemoteMediator(storage, dataSource, pagingSourceFactory, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
|||||||
import dagger.hilt.android.testing.HiltAndroidRule
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
import dagger.hilt.android.testing.HiltTestApplication
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.BeforeClass
|
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
@@ -13,13 +12,6 @@ import org.robolectric.annotation.Config
|
|||||||
@Config(application = HiltTestApplication::class, manifest = Config.NONE)
|
@Config(application = HiltTestApplication::class, manifest = Config.NONE)
|
||||||
abstract class HiltRobolectricTest {
|
abstract class HiltRobolectricTest {
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
@JvmStatic
|
|
||||||
fun setupTimber() = plantPrintLn()
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
var hiltRule = HiltAndroidRule(this)
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,9 @@ package gq.kirmanak.mealient.test
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.BeforeClass
|
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@Config(application = Application::class, manifest = Config.NONE)
|
@Config(application = Application::class, manifest = Config.NONE)
|
||||||
abstract class RobolectricTest {
|
abstract class RobolectricTest
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@BeforeClass
|
|
||||||
@JvmStatic
|
|
||||||
fun setupTimber() = plantPrintLn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,15 +2,6 @@ package gq.kirmanak.mealient.test
|
|||||||
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
fun String.toJsonResponseBody() = toResponseBody("application/json".toMediaType())
|
fun String.toJsonResponseBody() = toResponseBody("application/json".toMediaType())
|
||||||
|
|
||||||
fun plantPrintLn() {
|
|
||||||
Timber.plant(object : Timber.Tree() {
|
|
||||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
|
||||||
println(message)
|
|
||||||
t?.printStackTrace()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.add
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
@@ -27,13 +28,16 @@ class AddRecipeViewModelTest {
|
|||||||
@MockK(relaxUnitFun = true)
|
@MockK(relaxUnitFun = true)
|
||||||
lateinit var addRecipeRepo: AddRecipeRepo
|
lateinit var addRecipeRepo: AddRecipeRepo
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: AddRecipeViewModel
|
lateinit var subject: AddRecipeViewModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
Dispatchers.setMain(UnconfinedTestDispatcher())
|
Dispatchers.setMain(UnconfinedTestDispatcher())
|
||||||
subject = AddRecipeViewModel(addRecipeRepo)
|
subject = AddRecipeViewModel(addRecipeRepo, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@After
|
@After
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.baseurl
|
|||||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||||
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
||||||
import gq.kirmanak.mealient.data.baseurl.VersionInfo
|
import gq.kirmanak.mealient.data.baseurl.VersionInfo
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
|
||||||
import gq.kirmanak.mealient.test.RobolectricTest
|
import gq.kirmanak.mealient.test.RobolectricTest
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
@@ -24,12 +25,15 @@ class BaseURLViewModelTest : RobolectricTest() {
|
|||||||
@MockK
|
@MockK
|
||||||
lateinit var versionDataSource: VersionDataSource
|
lateinit var versionDataSource: VersionDataSource
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: BaseURLViewModel
|
lateinit var subject: BaseURLViewModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = BaseURLViewModel(baseURLStorage, versionDataSource)
|
subject = BaseURLViewModel(baseURLStorage, versionDataSource, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.disclaimer
|
|||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
import io.mockk.MockKAnnotations
|
import io.mockk.MockKAnnotations
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -17,12 +18,15 @@ class DisclaimerViewModelTest {
|
|||||||
@MockK(relaxUnitFun = true)
|
@MockK(relaxUnitFun = true)
|
||||||
lateinit var storage: DisclaimerStorage
|
lateinit var storage: DisclaimerStorage
|
||||||
|
|
||||||
|
@MockK(relaxUnitFun = true)
|
||||||
|
lateinit var logger: Logger
|
||||||
|
|
||||||
lateinit var subject: DisclaimerViewModel
|
lateinit var subject: DisclaimerViewModel
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
subject = DisclaimerViewModel(storage)
|
subject = DisclaimerViewModel(storage, logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -39,5 +39,4 @@ dependencies {
|
|||||||
testImplementation(libs.google.truth)
|
testImplementation(libs.google.truth)
|
||||||
|
|
||||||
testImplementation(libs.io.mockk)
|
testImplementation(libs.io.mockk)
|
||||||
|
|
||||||
}
|
}
|
||||||
1
datastore/.gitignore
vendored
Normal file
1
datastore/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
62
datastore/build.gradle.kts
Normal file
62
datastore/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import com.google.protobuf.gradle.builtins
|
||||||
|
import com.google.protobuf.gradle.generateProtoTasks
|
||||||
|
import com.google.protobuf.gradle.protobuf
|
||||||
|
import com.google.protobuf.gradle.protoc
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("gq.kirmanak.mealient.library")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
id("dagger.hilt.android.plugin")
|
||||||
|
alias(libs.plugins.protobuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "gq.kirmanak.mealient.datastore"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":logging"))
|
||||||
|
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
implementation(libs.androidx.datastore.datastore)
|
||||||
|
|
||||||
|
implementation(libs.google.protobuf.javalite)
|
||||||
|
|
||||||
|
implementation(libs.androidx.security.crypto)
|
||||||
|
|
||||||
|
implementation(libs.google.dagger.hiltAndroid)
|
||||||
|
kapt(libs.google.dagger.hiltCompiler)
|
||||||
|
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||||
|
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||||
|
|
||||||
|
implementation(libs.jetbrains.kotlinx.datetime)
|
||||||
|
|
||||||
|
implementation(libs.jetbrains.kotlinx.coroutinesAndroid)
|
||||||
|
testImplementation(libs.jetbrains.kotlinx.coroutinesTest)
|
||||||
|
|
||||||
|
testImplementation(libs.androidx.test.junit)
|
||||||
|
|
||||||
|
testImplementation(libs.google.truth)
|
||||||
|
|
||||||
|
testImplementation(libs.io.mockk)
|
||||||
|
}
|
||||||
|
|
||||||
|
protobuf {
|
||||||
|
protoc {
|
||||||
|
artifact = libs.google.protobuf.protoc.get().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
generateProtoTasks {
|
||||||
|
all().forEach { task ->
|
||||||
|
task.builtins {
|
||||||
|
val java by registering {
|
||||||
|
option("lite")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package gq.kirmanak.mealient.datastore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import androidx.datastore.core.DataStoreFactory
|
||||||
|
import androidx.datastore.dataStoreFile
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKeys
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInput
|
||||||
|
import gq.kirmanak.mealient.datastore.recipe.AddRecipeInputSerializer
|
||||||
|
import javax.inject.Named
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface DataStoreModule {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ENCRYPTED = "encrypted"
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAddRecipeInputStore(
|
||||||
|
@ApplicationContext context: Context
|
||||||
|
): DataStore<AddRecipeInput> = DataStoreFactory.create(AddRecipeInputSerializer) {
|
||||||
|
context.dataStoreFile("add_recipe_input")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
@Named(ENCRYPTED)
|
||||||
|
fun provideEncryptedSharedPreferences(
|
||||||
|
@ApplicationContext applicationContext: Context,
|
||||||
|
): SharedPreferences {
|
||||||
|
val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
|
||||||
|
val mainKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
|
||||||
|
return EncryptedSharedPreferences.create(
|
||||||
|
ENCRYPTED,
|
||||||
|
mainKeyAlias,
|
||||||
|
applicationContext,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gq.kirmanak.mealient.datastore.recipe
|
||||||
|
|
||||||
|
data class AddRecipeDraft(
|
||||||
|
val recipeName: String,
|
||||||
|
val recipeDescription: String,
|
||||||
|
val recipeYield: String,
|
||||||
|
val recipeInstructions: List<String>,
|
||||||
|
val recipeIngredients: List<String>,
|
||||||
|
val isRecipePublic: Boolean,
|
||||||
|
val areCommentsDisabled: Boolean,
|
||||||
|
)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gq.kirmanak.mealient.data.add.models
|
package gq.kirmanak.mealient.datastore.recipe
|
||||||
|
|
||||||
import androidx.datastore.core.CorruptionException
|
import androidx.datastore.core.CorruptionException
|
||||||
import androidx.datastore.core.Serializer
|
import androidx.datastore.core.Serializer
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package gq.kirmanak.mealient.datastore.recipe
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AddRecipeStorage {
|
||||||
|
|
||||||
|
val updates: Flow<AddRecipeDraft>
|
||||||
|
|
||||||
|
suspend fun save(addRecipeDraft: AddRecipeDraft)
|
||||||
|
|
||||||
|
suspend fun clear()
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package gq.kirmanak.mealient.datastore.recipe
|
||||||
|
|
||||||
|
import androidx.datastore.core.DataStore
|
||||||
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AddRecipeStorageImpl @Inject constructor(
|
||||||
|
private val dataStore: DataStore<AddRecipeInput>,
|
||||||
|
private val logger: Logger,
|
||||||
|
) : AddRecipeStorage {
|
||||||
|
|
||||||
|
override val updates: Flow<AddRecipeDraft>
|
||||||
|
get() = dataStore.data.map {
|
||||||
|
AddRecipeDraft(
|
||||||
|
recipeName = it.recipeName,
|
||||||
|
recipeDescription = it.recipeDescription,
|
||||||
|
recipeYield = it.recipeYield,
|
||||||
|
recipeInstructions = it.recipeInstructionsList,
|
||||||
|
recipeIngredients = it.recipeIngredientsList,
|
||||||
|
isRecipePublic = it.isRecipePublic,
|
||||||
|
areCommentsDisabled = it.areCommentsDisabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(addRecipeDraft: AddRecipeDraft) {
|
||||||
|
logger.v { "save() called with: addRecipeDraft = $addRecipeDraft" }
|
||||||
|
val input = AddRecipeInput.newBuilder()
|
||||||
|
.setRecipeName(addRecipeDraft.recipeName)
|
||||||
|
.setRecipeDescription(addRecipeDraft.recipeDescription)
|
||||||
|
.setRecipeYield(addRecipeDraft.recipeYield)
|
||||||
|
.setIsRecipePublic(addRecipeDraft.isRecipePublic)
|
||||||
|
.setAreCommentsDisabled(addRecipeDraft.areCommentsDisabled)
|
||||||
|
.addAllRecipeIngredients(addRecipeDraft.recipeIngredients)
|
||||||
|
.addAllRecipeInstructions(addRecipeDraft.recipeInstructions)
|
||||||
|
.build()
|
||||||
|
dataStore.updateData { input }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun clear() {
|
||||||
|
logger.v { "clear() called" }
|
||||||
|
dataStore.updateData { AddRecipeInput.getDefaultInstance() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
option java_package = "gq.kirmanak.mealient.data.add.models";
|
option java_package = "gq.kirmanak.mealient.datastore.recipe";
|
||||||
option java_multiple_files = true;
|
option java_multiple_files = true;
|
||||||
|
|
||||||
message AddRecipeInput {
|
message AddRecipeInput {
|
||||||
@@ -41,8 +41,6 @@ retrofitKotlinxSerialization = "0.8.0"
|
|||||||
kotlinxSerialization = "1.3.3"
|
kotlinxSerialization = "1.3.3"
|
||||||
# https://github.com/square/okhttp/tags
|
# https://github.com/square/okhttp/tags
|
||||||
okhttp = "4.10.0"
|
okhttp = "4.10.0"
|
||||||
# https://github.com/JakeWharton/timber/releases
|
|
||||||
timber = "5.0.1"
|
|
||||||
# https://developer.android.com/jetpack/androidx/releases/paging
|
# https://developer.android.com/jetpack/androidx/releases/paging
|
||||||
paging = "3.1.1"
|
paging = "3.1.1"
|
||||||
# https://developer.android.com/jetpack/androidx/releases/room
|
# https://developer.android.com/jetpack/androidx/releases/room
|
||||||
@@ -140,7 +138,6 @@ androidx-test-junit = { group = "androidx.test.ext", name = "junit-ktx", version
|
|||||||
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
|
androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
|
||||||
|
|
||||||
jakewharton-retrofitSerialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
|
jakewharton-retrofitSerialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "retrofitKotlinxSerialization" }
|
||||||
jakewharton-timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
|
||||||
|
|
||||||
squareup-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
squareup-retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||||
|
|
||||||
|
|||||||
1
logging/.gitignore
vendored
Normal file
1
logging/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
14
logging/build.gradle.kts
Normal file
14
logging/build.gradle.kts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
id("gq.kirmanak.mealient.library")
|
||||||
|
id("dagger.hilt.android.plugin")
|
||||||
|
id("kotlin-kapt")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "gq.kirmanak.mealient.logging"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.google.dagger.hiltAndroid)
|
||||||
|
kapt(libs.google.dagger.hiltCompiler)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
interface Appender {
|
||||||
|
|
||||||
|
fun isLoggable(logLevel: LogLevel): Boolean
|
||||||
|
|
||||||
|
fun isLoggable(logLevel: LogLevel, tag: String): Boolean
|
||||||
|
|
||||||
|
fun log(logLevel: LogLevel, tag: String, message: String)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
enum class LogLevel { VERBOSE, DEBUG, INFO, WARNING, ERROR }
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class LogcatAppender @Inject constructor() : Appender {
|
||||||
|
|
||||||
|
private val isLoggable: Boolean by lazy { BuildConfig.DEBUG }
|
||||||
|
|
||||||
|
override fun isLoggable(logLevel: LogLevel): Boolean = isLoggable
|
||||||
|
|
||||||
|
override fun isLoggable(logLevel: LogLevel, tag: String): Boolean = isLoggable
|
||||||
|
|
||||||
|
override fun log(logLevel: LogLevel, tag: String, message: String) {
|
||||||
|
// Tag length limit was removed in API 26.
|
||||||
|
val logTag = if (tag.length <= MAX_TAG_LENGTH || Build.VERSION.SDK_INT >= 26) {
|
||||||
|
tag
|
||||||
|
} else {
|
||||||
|
tag.substring(0, MAX_TAG_LENGTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.length < MAX_LOG_LENGTH) {
|
||||||
|
Log.println(logLevel.priority, logTag, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by line, then ensure each line can fit into Log's maximum length.
|
||||||
|
var i = 0
|
||||||
|
val length = message.length
|
||||||
|
while (i < length) {
|
||||||
|
var newline = message.indexOf('\n', i)
|
||||||
|
newline = if (newline != -1) newline else length
|
||||||
|
do {
|
||||||
|
val end = newline.coerceAtMost(i + MAX_LOG_LENGTH)
|
||||||
|
val part = message.substring(i, end)
|
||||||
|
Log.println(logLevel.priority, logTag, part)
|
||||||
|
i = end
|
||||||
|
} while (i < newline)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_LOG_LENGTH = 4000
|
||||||
|
private const val MAX_TAG_LENGTH = 23
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val LogLevel.priority: Int
|
||||||
|
get() = when (this) {
|
||||||
|
LogLevel.VERBOSE -> Log.VERBOSE
|
||||||
|
LogLevel.DEBUG -> Log.DEBUG
|
||||||
|
LogLevel.INFO -> Log.INFO
|
||||||
|
LogLevel.WARNING -> Log.WARN
|
||||||
|
LogLevel.ERROR -> Log.ERROR
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
typealias MessageSupplier = () -> String
|
||||||
|
|
||||||
|
interface Logger {
|
||||||
|
|
||||||
|
fun v(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
|
||||||
|
|
||||||
|
fun d(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
|
||||||
|
|
||||||
|
fun i(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
|
||||||
|
|
||||||
|
fun w(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
|
||||||
|
|
||||||
|
fun e(throwable: Throwable? = null, tag: String? = null, messageSupplier: MessageSupplier)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class LoggerImpl @Inject constructor(
|
||||||
|
private val appenders: Set<@JvmSuppressWildcards Appender>,
|
||||||
|
) : Logger {
|
||||||
|
|
||||||
|
override fun v(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
|
||||||
|
log(LogLevel.VERBOSE, tag, messageSupplier, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun d(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
|
||||||
|
log(LogLevel.DEBUG, tag, messageSupplier, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun i(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
|
||||||
|
log(LogLevel.INFO, tag, messageSupplier, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun w(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
|
||||||
|
log(LogLevel.WARNING, tag, messageSupplier, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun e(throwable: Throwable?, tag: String?, messageSupplier: MessageSupplier) {
|
||||||
|
log(LogLevel.ERROR, tag, messageSupplier, throwable)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun log(
|
||||||
|
logLevel: LogLevel,
|
||||||
|
tag: String?,
|
||||||
|
messageSupplier: MessageSupplier,
|
||||||
|
t: Throwable?
|
||||||
|
) {
|
||||||
|
var logTag: String? = null
|
||||||
|
var message: String? = null
|
||||||
|
for (appender in appenders) {
|
||||||
|
if (appender.isLoggable(logLevel).not()) continue
|
||||||
|
|
||||||
|
logTag = logTag ?: tag ?: Throwable().stackTrace
|
||||||
|
.first { element -> !IGNORED_CLASSES.any { element.className.contains(it) } }
|
||||||
|
.let(::createStackElementTag)
|
||||||
|
|
||||||
|
if (appender.isLoggable(logLevel, logTag).not()) continue
|
||||||
|
|
||||||
|
message = message ?: (messageSupplier() + createStackTrace(t))
|
||||||
|
|
||||||
|
appender.log(logLevel, logTag, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStackTrace(throwable: Throwable?): String =
|
||||||
|
throwable?.let { Log.getStackTraceString(it) }
|
||||||
|
?.takeUnless { it.isBlank() }
|
||||||
|
?.let { "\n" + it }
|
||||||
|
.orEmpty()
|
||||||
|
|
||||||
|
private fun createStackElementTag(element: StackTraceElement): String {
|
||||||
|
var tag = element.className.substringAfterLast('.')
|
||||||
|
val m = ANONYMOUS_CLASS.matcher(tag)
|
||||||
|
if (m.find()) {
|
||||||
|
tag = m.replaceAll("")
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val IGNORED_CLASSES = listOf(Logger::class.java.name, LoggerImpl::class.java.name)
|
||||||
|
private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package gq.kirmanak.mealient.logging
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import dagger.multibindings.IntoSet
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
interface LoggingModule {
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
fun bindLogger(loggerImpl: LoggerImpl): Logger
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
@IntoSet
|
||||||
|
fun bindLogcatAppender(logcatAppender: LogcatAppender): Appender
|
||||||
|
}
|
||||||
@@ -21,3 +21,5 @@ rootProject.name = "Mealient"
|
|||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":database")
|
include(":database")
|
||||||
|
include(":datastore")
|
||||||
|
include(":logging")
|
||||||
|
|||||||
Reference in New Issue
Block a user