diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9b3c2f9..b0adc69 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,9 +1,5 @@ @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.util.* @@ -16,7 +12,6 @@ plugins { id("com.google.gms.google-services") id("com.google.firebase.crashlytics") alias(libs.plugins.appsweep) - alias(libs.plugins.protobuf) } android { @@ -72,6 +67,8 @@ android { dependencies { implementation(project(":database")) + implementation(project(":datastore")) + implementation(project(":logging")) implementation(libs.android.material.material) @@ -105,8 +102,6 @@ dependencies { implementation(libs.jetbrains.kotlinx.serialization) - implementation(libs.jakewharton.timber) - implementation(libs.androidx.paging.runtimeKtx) testImplementation(libs.androidx.paging.commonKtx) @@ -124,11 +119,6 @@ dependencies { implementation(libs.kirich1409.viewBinding) 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(libs.google.firebase.analyticsKtx) @@ -150,24 +140,4 @@ dependencies { debugImplementation(libs.squareup.leakcanary) 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 } \ No newline at end of file diff --git a/app/src/debug/java/gq/kirmanak/mealient/App.kt b/app/src/debug/java/gq/kirmanak/mealient/App.kt index a1575c6..7854133 100644 --- a/app/src/debug/java/gq/kirmanak/mealient/App.kt +++ b/app/src/debug/java/gq/kirmanak/mealient/App.kt @@ -2,14 +2,17 @@ package gq.kirmanak.mealient import android.app.Application import dagger.hilt.android.HiltAndroidApp -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject @HiltAndroidApp class App : Application() { + @Inject + lateinit var logger: Logger + override fun onCreate() { super.onCreate() - Timber.plant(Timber.DebugTree()) - Timber.v("onCreate() called") + logger.v { "onCreate() called" } } } diff --git a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt b/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt index 126ff17..62086a0 100644 --- a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt +++ b/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt @@ -11,9 +11,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import dagger.multibindings.IntoSet import gq.kirmanak.mealient.BuildConfig +import gq.kirmanak.mealient.logging.Logger import okhttp3.Interceptor import okhttp3.logging.HttpLoggingInterceptor -import timber.log.Timber import javax.inject.Singleton @Module @@ -22,8 +22,8 @@ object DebugModule { @Provides @Singleton @IntoSet - fun provideLoggingInterceptor(): Interceptor { - val interceptor = HttpLoggingInterceptor { message -> Timber.tag("OkHttp").v(message) } + fun provideLoggingInterceptor(logger: Logger): Interceptor { + val interceptor = HttpLoggingInterceptor { message -> logger.v(tag = "OkHttp") { message } } interceptor.level = when { BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY else -> HttpLoggingInterceptor.Level.BASIC diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeStorage.kt deleted file mode 100644 index 283416a..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeStorage.kt +++ /dev/null @@ -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 - - suspend fun save(addRecipeRequest: AddRecipeRequest) - - suspend fun clear() -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt index 53ca6b7..7799b79 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt @@ -4,22 +4,25 @@ import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.extensions.logAndMapErrors -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @Singleton class AddRecipeDataSourceImpl @Inject constructor( private val addRecipeServiceFactory: ServiceFactory, + private val logger: Logger, ) : AddRecipeDataSource { 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 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 } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt index 372a8b6..23b7698 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt @@ -2,11 +2,16 @@ package gq.kirmanak.mealient.data.add.impl import gq.kirmanak.mealient.data.add.AddRecipeDataSource 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.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.first -import timber.log.Timber +import kotlinx.coroutines.flow.map import javax.inject.Inject import javax.inject.Singleton @@ -14,23 +19,45 @@ import javax.inject.Singleton class AddRecipeRepoImpl @Inject constructor( private val addRecipeDataSource: AddRecipeDataSource, private val addRecipeStorage: AddRecipeStorage, + private val logger: Logger, ) : AddRecipeRepo { override val addRecipeRequestFlow: Flow - 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) { - Timber.v("preserveRecipe() called with: recipe = $recipe") - addRecipeStorage.save(recipe) + logger.v { "preserveRecipe() called with: recipe = $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() { - Timber.v("clear() called") + logger.v { "clear() called" } addRecipeStorage.clear() } override suspend fun saveRecipe(): String { - Timber.v("saveRecipe() called") + logger.v { "saveRecipe() called" } return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first()) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeStorageImpl.kt deleted file mode 100644 index 921d414..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeStorageImpl.kt +++ /dev/null @@ -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, -) : AddRecipeStorage { - - override val updates: Flow - 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() } - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt index deb538e..08307af 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt @@ -1,5 +1,6 @@ package gq.kirmanak.mealient.data.add.models +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,27 +21,27 @@ data class AddRecipeRequest( @SerialName("assets") val assets: List = emptyList(), @SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(), ) { - constructor(input: AddRecipeInput) : this( + constructor(input: AddRecipeDraft) : this( name = input.recipeName, description = input.recipeDescription, recipeYield = input.recipeYield, - recipeIngredient = input.recipeIngredientsList.map { AddRecipeIngredient(note = it) }, - recipeInstructions = input.recipeInstructionsList.map { AddRecipeInstruction(text = it) }, + recipeIngredient = input.recipeIngredients.map { AddRecipeIngredient(note = it) }, + recipeInstructions = input.recipeInstructions.map { AddRecipeInstruction(text = it) }, settings = AddRecipeSettings( public = input.isRecipePublic, disableComments = input.areCommentsDisabled, ) ) - fun toInput(): AddRecipeInput = AddRecipeInput.newBuilder() - .setRecipeName(name) - .setRecipeDescription(description) - .setRecipeYield(recipeYield) - .setIsRecipePublic(settings.public) - .setAreCommentsDisabled(settings.disableComments) - .addAllRecipeIngredients(recipeIngredient.map { it.note }) - .addAllRecipeInstructions(recipeInstructions.map { it.text }) - .build() + fun toDraft(): AddRecipeDraft = AddRecipeDraft( + recipeName = name, + recipeDescription = description, + recipeYield = recipeYield, + recipeInstructions = recipeInstructions.map { it.text }, + recipeIngredients = recipeIngredient.map { it.note }, + isRecipePublic = settings.public, + areCommentsDisabled = settings.disableComments, + ) } @Serializable diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index b634a7d..3af42d0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -7,10 +7,10 @@ import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull import gq.kirmanak.mealient.extensions.logAndMapErrors +import gq.kirmanak.mealient.logging.Logger import kotlinx.serialization.json.Json import retrofit2.HttpException import retrofit2.Response -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -18,14 +18,15 @@ import javax.inject.Singleton class AuthDataSourceImpl @Inject constructor( private val authServiceFactory: ServiceFactory, private val json: Json, + private val logger: Logger, ) : AuthDataSource { 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 response = sendRequest(authService, username, password) val accessToken = parseToken(response) - Timber.v("authenticate() returned: $accessToken") + logger.v { "authenticate() returned: $accessToken" } return accessToken } @@ -34,6 +35,7 @@ class AuthDataSourceImpl @Inject constructor( username: String, password: String ): Response = logAndMapErrors( + logger, block = { authService.getToken(username = username, password = password) }, logProvider = { "sendRequest: can't get token" }, ) @@ -44,7 +46,7 @@ class AuthDataSourceImpl @Inject constructor( response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null")) } else { val cause = HttpException(response) - val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json) + val errorDetail: ErrorDetail? = response.decodeErrorBodyOrNull(json, logger) throw when (errorDetail?.detail) { "Unauthorized" -> Unauthorized(cause) else -> NotMealie(cause) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 6b8dbb8..3780a7e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -4,9 +4,9 @@ import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -14,13 +14,14 @@ import javax.inject.Singleton class AuthRepoImpl @Inject constructor( private val authStorage: AuthStorage, private val authDataSource: AuthDataSource, + private val logger: Logger, ) : AuthRepo { override val isAuthorizedFlow: Flow get() = authStorage.authHeaderFlow.map { it != null } 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) .let { AUTH_HEADER_FORMAT.format(it) } .let { authStorage.setAuthHeader(it) } @@ -35,14 +36,14 @@ class AuthRepoImpl @Inject constructor( } override suspend fun logout() { - Timber.v("logout() called") + logger.v { "logout() called" } authStorage.setEmail(null) authStorage.setPassword(null) authStorage.setAuthHeader(null) } override suspend fun invalidateAuthHeader() { - Timber.v("invalidateAuthHeader() called") + logger.v { "invalidateAuthHeader() called" } val email = authStorage.getEmail() ?: return val password = authStorage.getPassword() ?: return runCatchingExceptCancel { authenticate(email, password) } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt index 6570e95..aa83e67 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImpl.kt @@ -4,13 +4,13 @@ import android.content.SharedPreferences import androidx.annotation.VisibleForTesting import androidx.core.content.edit 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.logging.Logger import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.concurrent.Executors import javax.inject.Inject import javax.inject.Named @@ -19,11 +19,12 @@ import javax.inject.Singleton @Singleton class AuthStorageImpl @Inject constructor( @Named(ENCRYPTED) private val sharedPreferences: SharedPreferences, + private val logger: Logger, ) : AuthStorage { override val authHeaderFlow: Flow get() = sharedPreferences - .prefsChangeFlow { getString(AUTH_HEADER_KEY, null) } + .prefsChangeFlow(logger) { getString(AUTH_HEADER_KEY, null) } .distinctUntilChanged() private val singleThreadDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() @@ -43,13 +44,13 @@ class AuthStorageImpl @Inject constructor( key: String, value: String? ) = 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) } } private suspend fun getString(key: String) = withContext(singleThreadDispatcher) { 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 } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt index 27e65d7..6493395 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt @@ -5,20 +5,22 @@ import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.extensions.logAndMapErrors import gq.kirmanak.mealient.extensions.versionInfo -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @Singleton class VersionDataSourceImpl @Inject constructor( private val serviceFactory: ServiceFactory, + private val logger: Logger, ) : VersionDataSource { 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 response = logAndMapErrors( + logger, block = { service.getVersion() }, logProvider = { "getVersionInfo: can't request version" } ) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt index 78fc2a7..17bacac 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/disclaimer/DisclaimerStorageImpl.kt @@ -2,15 +2,16 @@ package gq.kirmanak.mealient.data.disclaimer import androidx.datastore.preferences.core.Preferences import gq.kirmanak.mealient.data.storage.PreferencesStorage +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class DisclaimerStorageImpl @Inject constructor( private val preferencesStorage: PreferencesStorage, + private val logger: Logger, ) : DisclaimerStorage { private val isDisclaimerAcceptedKey: Preferences.Key @@ -19,14 +20,14 @@ class DisclaimerStorageImpl @Inject constructor( get() = preferencesStorage.valueUpdates(isDisclaimerAcceptedKey).map { it == true } override suspend fun isDisclaimerAccepted(): Boolean { - Timber.v("isDisclaimerAccepted() called") + logger.v { "isDisclaimerAccepted() called" } val isAccepted = preferencesStorage.getValue(isDisclaimerAcceptedKey) ?: false - Timber.v("isDisclaimerAccepted() returned: $isAccepted") + logger.v { "isDisclaimerAccepted() returned: $isAccepted" } return isAccepted } override suspend fun acceptDisclaimer() { - Timber.v("acceptDisclaimer() called") + logger.v { "acceptDisclaimer() called" } preferencesStorage.storeValues(Pair(isDisclaimerAcceptedKey, true)) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt index 539405e..e36694e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt @@ -1,21 +1,22 @@ package gq.kirmanak.mealient.data.network import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import gq.kirmanak.mealient.logging.Logger import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Retrofit -import timber.log.Timber class RetrofitBuilder( private val okHttpClient: OkHttpClient, - private val json: Json + private val json: Json, + private val logger: Logger, ) { @OptIn(ExperimentalSerializationApi::class) 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 converterFactory = json.asConverterFactory(contentType) return Retrofit.Builder() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt index 7a7ea95..969fac0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt @@ -2,30 +2,34 @@ package gq.kirmanak.mealient.data.network import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.extensions.runCatchingExceptCancel -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger -inline fun RetrofitBuilder.createServiceFactory(baseURLStorage: BaseURLStorage) = - RetrofitServiceFactory(T::class.java, this, baseURLStorage) +inline fun RetrofitBuilder.createServiceFactory( + baseURLStorage: BaseURLStorage, + logger: Logger +) = + RetrofitServiceFactory(T::class.java, this, baseURLStorage, logger) class RetrofitServiceFactory( private val serviceClass: Class, private val retrofitBuilder: RetrofitBuilder, private val baseURLStorage: BaseURLStorage, + private val logger: Logger, ) : ServiceFactory { private val cache: MutableMap = mutableMapOf() 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() synchronized(cache) { cache[url] ?: createService(url, serviceClass) } }.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) } private fun createService(url: String, serviceClass: Class): 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) cache[url] = service return service diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt index ab6886e..5d75b30 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt @@ -11,20 +11,21 @@ import gq.kirmanak.mealient.extensions.recipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity import gq.kirmanak.mealient.extensions.toRecipeInstructionEntity -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @Singleton class RecipeStorageImpl @Inject constructor( - private val db: AppDb + private val db: AppDb, + private val logger: Logger, ) : RecipeStorage { private val recipeDao: RecipeDao by lazy { db.recipeDao() } override suspend fun saveRecipes( recipes: List ) = db.withTransaction { - Timber.v("saveRecipes() called with $recipes") + logger.v { "saveRecipes() called with $recipes" } val tagEntities = mutableSetOf() tagEntities.addAll(recipeDao.queryAllTags()) @@ -91,12 +92,12 @@ class RecipeStorageImpl @Inject constructor( override fun queryRecipes(): PagingSource { - Timber.v("queryRecipes() called") + logger.v { "queryRecipes() called" } return recipeDao.queryRecipesByPages() } override suspend fun refreshAll(recipes: List) { - Timber.v("refreshAll() called with: recipes = $recipes") + logger.v { "refreshAll() called with: recipes = $recipes" } db.withTransaction { recipeDao.removeAllRecipes() saveRecipes(recipes) @@ -104,7 +105,7 @@ class RecipeStorageImpl @Inject constructor( } override suspend fun clearAllLocalData() { - Timber.v("clearAllLocalData() called") + logger.v { "clearAllLocalData() called" } db.withTransaction { recipeDao.removeAllRecipes() recipeDao.removeAllCategories() @@ -113,7 +114,7 @@ class RecipeStorageImpl @Inject constructor( } override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) { - Timber.v("saveRecipeInfo() called with: recipe = $recipe") + logger.v { "saveRecipeInfo() called with: recipe = $recipe" } db.withTransaction { recipeDao.insertRecipe(recipe.toRecipeEntity()) @@ -132,11 +133,11 @@ class RecipeStorageImpl @Inject constructor( } 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)) { "Can't find recipe by id $recipeId in DB" } - Timber.v("queryRecipeInfo() returned: $fullRecipeInfo") + logger.v { "queryRecipeInfo() returned: $fullRecipeInfo" } return fullRecipeInfo } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt index 94a83e6..d5b30fa 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt @@ -1,18 +1,19 @@ package gq.kirmanak.mealient.data.recipes.impl import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.logging.Logger import okhttp3.HttpUrl.Companion.toHttpUrlOrNull -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class RecipeImageUrlProviderImpl @Inject constructor( private val baseURLStorage: BaseURLStorage, + private val logger: Logger, ) : RecipeImageUrlProvider { 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 val imagePath = IMAGE_PATH_FORMAT.format(slug) val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() } @@ -21,7 +22,7 @@ class RecipeImageUrlProviderImpl @Inject constructor( ?.addPathSegments(imagePath) ?.build() ?.toString() - Timber.v("getRecipeImageUrl() returned: $result") + logger.v { "getRecipeImageUrl() returned: $result" } return result } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 99c43d9..942fe5f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -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.RecipeSummaryEntity import gq.kirmanak.mealient.extensions.runCatchingExceptCancel -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @@ -21,9 +21,10 @@ class RecipeRepoImpl @Inject constructor( private val storage: RecipeStorage, private val pagingSourceFactory: InvalidatingPagingSourceFactory, private val dataSource: RecipeDataSource, + private val logger: Logger, ) : RecipeRepo { override fun createPager(): Pager { - Timber.v("createPager() called") + logger.v { "createPager() called" } val pagingConfig = PagingConfig(pageSize = 5, enablePlaceholders = true) return Pager( config = pagingConfig, @@ -33,17 +34,17 @@ class RecipeRepoImpl @Inject constructor( } override suspend fun clearLocalData() { - Timber.v("clearLocalData() called") + logger.v { "clearLocalData() called" } storage.clearAllLocalData() } 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 { storage.saveRecipeInfo(dataSource.requestRecipeInfo(recipeSlug)) }.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) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 324ea30..06e785a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -8,7 +8,7 @@ import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.extensions.runCatchingExceptCancel -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @@ -18,6 +18,7 @@ class RecipesRemoteMediator @Inject constructor( private val storage: RecipeStorage, private val network: RecipeDataSource, private val pagingSourceFactory: InvalidatingPagingSourceFactory, + private val logger: Logger, ) : RemoteMediator() { @VisibleForTesting @@ -27,10 +28,10 @@ class RecipesRemoteMediator @Inject constructor( loadType: LoadType, state: PagingState ): 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) { - Timber.i("load: early exit, PREPEND isn't supported") + logger.i { "load: early exit, PREPEND isn't supported" } return MediatorResult.Success(endOfPaginationReached = true) } @@ -43,7 +44,7 @@ class RecipesRemoteMediator @Inject constructor( else storage.saveRecipes(recipes) recipes.size }.getOrElse { - Timber.e(it, "load: can't load recipes") + logger.e(it) { "load: can't load recipes" } 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 pagingSourceFactory.invalidate() - Timber.d("load: expectedCount = $limit, received $count") + logger.d { "load: expectedCount = $limit, received $count" } lastRequestEnd = start + count return MediatorResult.Success(endOfPaginationReached = count < limit) } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt index 4c2c2c2..1e191f2 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt @@ -3,31 +3,32 @@ package gq.kirmanak.mealient.data.recipes.network import gq.kirmanak.mealient.data.network.ServiceFactory import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse 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.Singleton @Singleton class RecipeDataSourceImpl @Inject constructor( private val recipeServiceFactory: ServiceFactory, + private val logger: Logger, ) : RecipeDataSource { override suspend fun requestRecipes(start: Int, limit: Int): List { - Timber.v("requestRecipes() called with: start = $start, limit = $limit") + logger.v { "requestRecipes() called with: start = $start, limit = $limit" } val recipeSummary = getRecipeService().getRecipeSummary(start, limit) - Timber.v("requestRecipes() returned: $recipeSummary") + logger.v { "requestRecipes() returned: $recipeSummary" } return recipeSummary } 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) - Timber.v("requestRecipeInfo() returned: $recipeInfo") + logger.v { "requestRecipeInfo() returned: $recipeInfo" } return recipeInfo } private suspend fun getRecipeService(): RecipeService { - Timber.v("getRecipeService() called") + logger.v { "getRecipeService() called" } return recipeServiceFactory.provideService() } } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt index de71ff0..e42346c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt @@ -5,14 +5,15 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.* -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class PreferencesStorageImpl @Inject constructor( - private val dataStore: DataStore + private val dataStore: DataStore, + private val logger: Logger, ) : PreferencesStorage { override val baseUrlKey = stringPreferencesKey("baseUrl") @@ -21,7 +22,7 @@ class PreferencesStorageImpl @Inject constructor( override suspend fun getValue(key: Preferences.Key): T? { val value = dataStore.data.first()[key] - Timber.v("getValue() returned: $value for $key") + logger.v { "getValue() returned: $value for $key" } return value } @@ -29,23 +30,23 @@ class PreferencesStorageImpl @Inject constructor( checkNotNull(getValue(key)) { "Value at $key is null when it was required" } override suspend fun storeValues(vararg pairs: Pair, T>) { - Timber.v("storeValues() called with: pairs = ${pairs.contentToString()}") + logger.v { "storeValues() called with: pairs = ${pairs.contentToString()}" } dataStore.edit { preferences -> pairs.forEach { preferences += it.toPreferencesPair() } } } override fun valueUpdates(key: Preferences.Key): Flow { - Timber.v("valueUpdates() called with: key = $key") + logger.v { "valueUpdates() called with: key = $key" } return dataStore.data .map { it[key] } .distinctUntilChanged() - .onEach { Timber.d("valueUpdates: new value at $key is $it") } - .onCompletion { Timber.i(it, "valueUpdates: finished") } + .onEach { logger.d { "valueUpdates: new value at $key is $it" } } + .onCompletion { logger.i(it) { "valueUpdates: finished" } } } override suspend fun removeValues(vararg keys: Preferences.Key) { - Timber.v("removeValues() called with: key = ${keys.contentToString()}") + logger.v { "removeValues() called with: key = ${keys.contentToString()}" } dataStore.edit { preferences -> keys.forEach { preferences -= it } } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt index c0b0402..13a92ab 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt @@ -1,28 +1,22 @@ 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.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.data.add.AddRecipeDataSource 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.AddRecipeRepoImpl 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.network.RetrofitBuilder import gq.kirmanak.mealient.data.network.ServiceFactory 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 okhttp3.OkHttpClient import javax.inject.Named @@ -34,22 +28,18 @@ interface AddRecipeModule { companion object { - @Provides - @Singleton - fun provideAddRecipeInputStore( - @ApplicationContext context: Context - ): DataStore = DataStoreFactory.create(AddRecipeInputSerializer) { - context.dataStoreFile("add_recipe_input") - } - @Provides @Singleton fun provideAddRecipeServiceFactory( @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, json: Json, + logger: Logger, baseURLStorage: BaseURLStorage, ): ServiceFactory { - return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) + return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( + baseURLStorage, + logger + ) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt index cb87c5f..f7bca5f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt @@ -2,9 +2,6 @@ package gq.kirmanak.mealient.di import android.accounts.AccountManager import android.content.Context -import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKeys import dagger.Binds import dagger.Module 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.ServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory +import gq.kirmanak.mealient.logging.Logger import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import javax.inject.Named @@ -32,16 +30,19 @@ import javax.inject.Singleton interface AuthModule { companion object { - const val ENCRYPTED = "encrypted" @Provides @Singleton fun provideAuthServiceFactory( @Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient, json: Json, + logger: Logger, baseURLStorage: BaseURLStorage, ): ServiceFactory { - return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) + return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( + baseURLStorage, + logger + ) } @Provides @@ -49,23 +50,6 @@ interface AuthModule { fun provideAccountManager(@ApplicationContext context: Context): AccountManager { 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 diff --git a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt index f205096..ecb7b2b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -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.ServiceFactory import gq.kirmanak.mealient.data.network.createServiceFactory +import gq.kirmanak.mealient.logging.Logger import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import javax.inject.Named @@ -29,9 +30,13 @@ interface BaseURLModule { fun provideVersionServiceFactory( @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, json: Json, + logger: Logger, baseURLStorage: BaseURLStorage, ): ServiceFactory { - return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) + return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( + baseURLStorage, + logger + ) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt index 9558359..a6f475e 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt @@ -5,6 +5,7 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.logging.Logger import okhttp3.OkHttpClient import java.io.InputStream import javax.inject.Named @@ -13,6 +14,8 @@ import javax.inject.Named @InstallIn(SingletonComponent::class) interface GlideModuleEntryPoint { + fun provideLogger(): Logger + @Named(AUTH_OK_HTTP) fun provideOkHttp(): OkHttpClient diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index 8317f15..de4090a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -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.RecipeService import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory import kotlinx.serialization.json.Json import okhttp3.OkHttpClient @@ -61,9 +62,13 @@ interface RecipeModule { fun provideRecipeServiceFactory( @Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient, json: Json, + logger: Logger, baseURLStorage: BaseURLStorage, ): ServiceFactory { - return RetrofitBuilder(okHttpClient, json).createServiceFactory(baseURLStorage) + return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( + baseURLStorage, + logger + ) } @Provides diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt index aa27e85..6c636f8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt @@ -1,22 +1,22 @@ package gq.kirmanak.mealient.extensions import gq.kirmanak.mealient.data.network.NetworkError +import gq.kirmanak.mealient.logging.Logger import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import retrofit2.HttpException import retrofit2.Response -import timber.log.Timber import java.io.InputStream -inline fun Response.decodeErrorBodyOrNull(json: Json): R? = - errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull(it) } +inline fun Response.decodeErrorBodyOrNull(json: Json, logger: Logger): R? = + errorBody()?.byteStream()?.let { json.decodeFromStreamOrNull(it, logger) } @OptIn(ExperimentalSerializationApi::class) -inline fun Json.decodeFromStreamOrNull(stream: InputStream): T? = +inline fun Json.decodeFromStreamOrNull(stream: InputStream, logger: Logger): T? = runCatching { decodeFromStream(stream) } - .onFailure { Timber.e(it, "decodeFromStreamOrNull: can't decode") } + .onFailure { logger.e(it) { "decodeFromStreamOrNull: can't decode" } } .getOrNull() fun Throwable.mapToNetworkError(): NetworkError = when (this) { @@ -24,8 +24,12 @@ fun Throwable.mapToNetworkError(): NetworkError = when (this) { else -> NetworkError.NoServerConnection(this) } -inline fun logAndMapErrors(block: () -> T, logProvider: () -> String): T = +inline fun logAndMapErrors( + logger: Logger, + block: () -> T, + noinline logProvider: () -> String +): T = runCatchingExceptCancel(block).getOrElse { - Timber.e(it, logProvider()) + logger.e(it, messageSupplier = logProvider) throw it.mapToNetworkError() } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt index 8a1fc8c..dd4ef9c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ViewExtensions.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.material.textfield.TextInputLayout +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.ChannelResult import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.onClosed @@ -24,61 +25,60 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import timber.log.Timber -fun SwipeRefreshLayout.refreshRequestFlow(): Flow = callbackFlow { - Timber.v("refreshRequestFlow() called") +fun SwipeRefreshLayout.refreshRequestFlow(logger: Logger): Flow = callbackFlow { + logger.v { "refreshRequestFlow() called" } val listener = SwipeRefreshLayout.OnRefreshListener { - Timber.v("refreshRequestFlow: listener called") - trySend(Unit).logErrors("refreshesFlow") + logger.v { "refreshRequestFlow: listener called" } + trySend(Unit).logErrors("refreshesFlow", logger) } setOnRefreshListener(listener) awaitClose { - Timber.v("Removing refresh request listener") + logger.v { "Removing refresh request listener" } setOnRefreshListener(null) } } -fun Activity.setSystemUiVisibility(isVisible: Boolean) { - Timber.v("setSystemUiVisibility() called with: isVisible = $isVisible") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible) - else setSystemUiVisibilityV1(isVisible) +fun Activity.setSystemUiVisibility(isVisible: Boolean, logger: Logger) { + logger.v { "setSystemUiVisibility() called with: isVisible = $isVisible" } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) setSystemUiVisibilityV30(isVisible, logger) + else setSystemUiVisibilityV1(isVisible, logger) } @Suppress("DEPRECATION") -private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean) { - Timber.v("setSystemUiVisibilityV1() called with: isVisible = $isVisible") +private fun Activity.setSystemUiVisibilityV1(isVisible: Boolean, logger: Logger) { + logger.v { "setSystemUiVisibilityV1() called with: isVisible = $isVisible" } window.decorView.systemUiVisibility = if (isVisible) 0 else View.SYSTEM_UI_FLAG_FULLSCREEN } @RequiresApi(Build.VERSION_CODES.R) -private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean) { - Timber.v("setSystemUiVisibilityV30() called with: isVisible = $isVisible") +private fun Activity.setSystemUiVisibilityV30(isVisible: Boolean, logger: Logger) { + logger.v { "setSystemUiVisibilityV30() called with: isVisible = $isVisible" } val systemBars = WindowInsets.Type.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) { - Timber.v("setActionBarVisibility() called with: isVisible = $isVisible") +fun AppCompatActivity.setActionBarVisibility(isVisible: Boolean, logger: Logger) { + logger.v { "setActionBarVisibility() called with: isVisible = $isVisible" } 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 = callbackFlow { - Timber.v("textChangesFlow() called") +fun TextView.textChangesFlow(logger: Logger): Flow = callbackFlow { + logger.v { "textChangesFlow() called" } val textWatcher = doAfterTextChanged { - trySend(it).logErrors("textChangesFlow") + trySend(it).logErrors("textChangesFlow", logger) } awaitClose { - Timber.d("textChangesFlow: flow is closing") + logger.d { "textChangesFlow: flow is closing" } removeTextChangedListener(textWatcher) } } -fun ChannelResult.logErrors(methodName: String): ChannelResult { - onFailure { Timber.e(it, "$methodName: can't send event") } - onClosed { Timber.e(it, "$methodName: flow has been closed") } +fun ChannelResult.logErrors(methodName: String, logger: Logger): ChannelResult { + onFailure { logger.e(it) { "$methodName: can't send event" } } + onClosed { logger.e(it) { "$methodName: flow has been closed" } } return this } @@ -87,29 +87,31 @@ fun EditText.checkIfInputIsEmpty( lifecycleOwner: LifecycleOwner, @StringRes stringId: Int, trim: Boolean = true, + logger: Logger, ): String? { val input = if (trim) text?.trim() else text val text = input?.toString().orEmpty() - Timber.d("Input text is \"$text\"") + logger.d { "Input text is \"$text\"" } return text.ifEmpty { inputLayout.error = resources.getString(stringId) lifecycleOwner.lifecycleScope.launch { - waitUntilNotEmpty() + waitUntilNotEmpty(logger) inputLayout.error = null } null } } -suspend fun EditText.waitUntilNotEmpty() { - textChangesFlow().filterNotNull().first { it.isNotEmpty() } - Timber.v("waitUntilNotEmpty() returned") +suspend fun EditText.waitUntilNotEmpty(logger: Logger) { + textChangesFlow(logger).filterNotNull().first { it.isNotEmpty() } + logger.v { "waitUntilNotEmpty() returned" } } fun SharedPreferences.prefsChangeFlow( + logger: Logger, valueReader: SharedPreferences.() -> T, ): Flow = callbackFlow { - fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow") + fun sendValue() = trySend(valueReader()).logErrors("prefsChangeFlow", logger) val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> sendValue() } sendValue() registerOnSharedPreferenceChangeListener(listener) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt index 022cf9d..e8f1b54 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/MealieGlideModule.kt @@ -10,7 +10,7 @@ import com.bumptech.glide.module.AppGlideModule import dagger.hilt.android.EntryPointAccessors.fromApplication import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.di.GlideModuleEntryPoint -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import java.io.InputStream @GlideModule @@ -18,13 +18,13 @@ class MealieGlideModule : AppGlideModule() { override fun registerComponents(context: Context, glide: Glide, registry: 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) appendRecipeLoader(registry, 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( RecipeSummaryEntity::class.java, InputStream::class.java, @@ -33,17 +33,15 @@ class MealieGlideModule : AppGlideModule() { } 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() registry.replace( - GlideUrl::class.java, - InputStream::class.java, - OkHttpUrlLoader.Factory(okHttp) + GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(okHttp) ) } - private fun getEntryPoint(context: Context): GlideModuleEntryPoint { - Timber.v("getEntryPoint() called with: context = $context") - return fromApplication(context, GlideModuleEntryPoint::class.java) - } + private fun getEntryPoint(context: Context): GlideModuleEntryPoint = + fromApplication(context, GlideModuleEntryPoint::class.java) + + private fun getLogger(context: Context): Logger = getEntryPoint(context).provideLogger() } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt index 4c7dccb..e346641 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivity.kt @@ -13,18 +13,23 @@ import com.google.android.material.shape.MaterialShapeDrawable import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.MainActivityBinding -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { + private lateinit var binding: MainActivityBinding private val viewModel by viewModels() private val title: String by lazy { getString(R.string.app_name) } private val uiState: MainActivityUiState get() = viewModel.uiState + @Inject + lateinit var logger: Logger + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") + logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) @@ -36,7 +41,7 @@ class MainActivity : AppCompatActivity() { } private fun onNavigationItemSelected(menuItem: MenuItem): Boolean { - Timber.v("onNavigationItemSelected() called with: menuItem = $menuItem") + logger.v { "onNavigationItemSelected() called with: menuItem = $menuItem" } menuItem.isChecked = true val deepLink = when (menuItem.itemId) { R.id.add_recipe -> ADD_RECIPE_DEEP_LINK @@ -49,19 +54,19 @@ class MainActivity : AppCompatActivity() { } 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 binding.navigationView.isVisible = uiState.navigationVisible invalidateOptionsMenu() } private fun setToolbarRoundCorner() { - Timber.v("setToolbarRoundCorner() called") + logger.v { "setToolbarRoundCorner() called" } val drawables = listOf( binding.toolbarHolder.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) for (drawable in drawables) { drawable?.apply { @@ -72,7 +77,7 @@ class MainActivity : AppCompatActivity() { } 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) menu.findItem(R.id.logout).isVisible = uiState.canShowLogout menu.findItem(R.id.login).isVisible = uiState.canShowLogin @@ -80,7 +85,7 @@ class MainActivity : AppCompatActivity() { } 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) { R.id.login -> { navigateDeepLink(AUTH_DEEP_LINK) @@ -96,7 +101,7 @@ class MainActivity : AppCompatActivity() { } 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()) } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt index 7290723..0a47762 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/activity/MainActivityViewModel.kt @@ -3,15 +3,16 @@ package gq.kirmanak.mealient.ui.activity import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel class MainActivityViewModel @Inject constructor( private val authRepo: AuthRepo, + private val logger: Logger, ) : ViewModel() { private val _uiState = MutableLiveData(MainActivityUiState()) @@ -32,7 +33,7 @@ class MainActivityViewModel @Inject constructor( } fun logout() { - Timber.v("logout() called") + logger.v { "logout() called" } viewModelScope.launch { authRepo.logout() } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt index 06c20b8..858d5ce 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt @@ -20,8 +20,9 @@ import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding import gq.kirmanak.mealient.databinding.ViewSingleInputBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty import gq.kirmanak.mealient.extensions.collectWhenViewResumed +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.activity.MainActivityViewModel -import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint 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() private val activityViewModel by activityViewModels() + @Inject + lateinit var logger: Logger + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) } @@ -42,12 +46,12 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { } private fun observeAddRecipeResult() { - Timber.v("observeAddRecipeResult() called") + logger.v { "observeAddRecipeResult() called" } collectWhenViewResumed(viewModel.addRecipeResult, ::onRecipeSaveResult) } 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 } @@ -60,12 +64,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { } private fun setupViews() = with(binding) { - Timber.v("setupViews() called") + logger.v { "setupViews() called" } saveRecipeButton.setOnClickListener { recipeNameInput.checkIfInputIsEmpty( inputLayout = recipeNameInputLayout, lifecycleOwner = viewLifecycleOwner, - stringId = R.string.fragment_add_recipe_name_error + stringId = R.string.fragment_add_recipe_name_error, + logger = logger, ) ?: return@setOnClickListener 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) { - 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 inputBinding = ViewSingleInputBinding.inflate(layoutInflater, fragmentRoot, false) val root = inputBinding.root @@ -116,7 +121,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { } private fun saveValues() = with(binding) { - Timber.v("saveValues() called") + logger.v { "saveValues() called" } val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) } val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) } val settings = AddRecipeSettings( @@ -144,7 +149,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { .toList() 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) recipeDescriptionInput.setText(request.description) recipeYieldInput.setText(request.recipeYield) @@ -159,13 +164,13 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { } private fun Iterable.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() forEach { inflateInputRow(flow = flow, hintId = hintId, text = it) } } private fun Flow.removeAllViews() { - Timber.v("removeAllViews() called") + logger.v { "removeAllViews() called" } for (id in referencedIds.iterator()) { val view = binding.holder.findViewById(id) ?: continue removeView(view) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt index 6087ce4..4d61ad4 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt @@ -6,17 +6,18 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel class AddRecipeViewModel @Inject constructor( private val addRecipeRepo: AddRecipeRepo, + private val logger: Logger, ) : ViewModel() { private val _addRecipeResultChannel = Channel(Channel.UNLIMITED) @@ -27,19 +28,19 @@ class AddRecipeViewModel @Inject constructor( get() = _preservedAddRecipeRequestChannel.receiveAsFlow() fun loadPreservedRequest() { - Timber.v("loadPreservedRequest() called") + logger.v { "loadPreservedRequest() called" } viewModelScope.launch { doLoadPreservedRequest() } } private suspend fun doLoadPreservedRequest() { - Timber.v("doLoadPreservedRequest() called") + logger.v { "doLoadPreservedRequest() called" } val request = addRecipeRepo.addRecipeRequestFlow.first() - Timber.d("doLoadPreservedRequest: request = $request") + logger.d { "doLoadPreservedRequest: request = $request" } _preservedAddRecipeRequestChannel.send(request) } fun clear() { - Timber.v("clear() called") + logger.v { "clear() called" } viewModelScope.launch { addRecipeRepo.clear() doLoadPreservedRequest() @@ -47,16 +48,16 @@ class AddRecipeViewModel @Inject constructor( } fun preserve(request: AddRecipeRequest) { - Timber.v("preserve() called with: request = $request") + logger.v { "preserve() called with: request = $request" } viewModelScope.launch { addRecipeRepo.preserve(request) } } fun saveRecipe() { - Timber.v("saveRecipe() called") + logger.v { "saveRecipe() called" } viewModelScope.launch { val isSuccessful = runCatchingExceptCancel { addRecipeRepo.saveRecipe() } .fold(onSuccess = { true }, onFailure = { false }) - Timber.d("saveRecipe: isSuccessful = $isSuccessful") + logger.d { "saveRecipe: isSuccessful = $isSuccessful" } _addRecipeResultChannel.send(isSuccessful) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index 3be5eaf..043189d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -12,19 +12,24 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel -import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { + private val binding by viewBinding(FragmentAuthenticationBinding::bind) private val viewModel by viewModels() private val activityViewModel by activityViewModels() + @Inject + lateinit var logger: Logger + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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() } activityViewModel.updateUiState { 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) { - Timber.v("onLoginClicked() called") + logger.v { "onLoginClicked() called" } val email: String = emailInput.checkIfInputIsEmpty( inputLayout = emailInputLayout, lifecycleOwner = viewLifecycleOwner, stringId = R.string.fragment_authentication_email_input_empty, + logger = logger, ) ?: return val pass: String = passwordInput.checkIfInputIsEmpty( @@ -46,13 +52,14 @@ class AuthenticationFragment : Fragment(R.layout.fragment_authentication) { lifecycleOwner = viewLifecycleOwner, stringId = R.string.fragment_authentication_password_input_empty, trim = false, + logger = logger, ) ?: return viewModel.authenticate(email, pass) } private fun onUiStateChange(uiState: OperationUiState) = with(binding) { - Timber.v("onUiStateChange() called with: authUiState = $uiState") + logger.v { "onUiStateChange() called with: authUiState = $uiState" } if (uiState.isSuccess) { findNavController().popBackStack() return diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index 70b3738..55a3cdd 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -7,21 +7,22 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel class AuthenticationViewModel @Inject constructor( private val authRepo: AuthRepo, + private val logger: Logger, ) : ViewModel() { private val _uiState = MutableLiveData>(OperationUiState.Initial()) val uiState: LiveData> get() = _uiState 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() viewModelScope.launch { val result = runCatchingExceptCancel { authRepo.authenticate(email, password) } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index 079b8f9..0063929 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -12,9 +12,10 @@ import gq.kirmanak.mealient.R import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.activity.MainActivityViewModel -import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint 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() private val activityViewModel by activityViewModels() + @Inject + lateinit var logger: Logger + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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) viewModel.uiState.observe(viewLifecycleOwner, ::onUiStateChange) activityViewModel.updateUiState { @@ -34,17 +38,18 @@ class BaseURLFragment : Fragment(R.layout.fragment_base_url) { } private fun onProceedClick(view: View) { - Timber.v("onProceedClick() called with: view = $view") + logger.v { "onProceedClick() called with: view = $view" } val url = binding.urlInput.checkIfInputIsEmpty( inputLayout = binding.urlInputLayout, lifecycleOwner = viewLifecycleOwner, stringId = R.string.fragment_baseurl_url_input_empty, + logger = logger, ) ?: return viewModel.saveBaseUrl(url) } private fun onUiStateChange(uiState: OperationUiState) = with(binding) { - Timber.v("onUiStateChange() called with: uiState = $uiState") + logger.v { "onUiStateChange() called with: uiState = $uiState" } if (uiState.isSuccess) { findNavController().navigate(BaseURLFragmentDirections.actionBaseURLFragmentToRecipesFragment()) return diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index 2c717b8..1b98c87 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -8,22 +8,23 @@ import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel class BaseURLViewModel @Inject constructor( private val baseURLStorage: BaseURLStorage, private val versionDataSource: VersionDataSource, + private val logger: Logger, ) : ViewModel() { private val _uiState = MutableLiveData>(OperationUiState.Initial()) val uiState: LiveData> get() = _uiState fun saveBaseUrl(baseURL: String) { - Timber.v("saveBaseUrl() called with: baseURL = $baseURL") + logger.v { "saveBaseUrl() called with: baseURL = $baseURL" } _uiState.value = OperationUiState.Progress() val hasPrefix = ALLOWED_PREFIXES.any { baseURL.startsWith(it) } val url = baseURL.takeIf { hasPrefix } ?: WITH_PREFIX_FORMAT.format(baseURL) @@ -31,13 +32,13 @@ class BaseURLViewModel @Inject constructor( } private suspend fun checkBaseURL(baseURL: String) { - Timber.v("checkBaseURL() called with: baseURL = $baseURL") + logger.v { "checkBaseURL() called with: baseURL = $baseURL" } val result = runCatchingExceptCancel { // If it returns proper version info then it must be a Mealie versionDataSource.getVersionInfo(baseURL) baseURLStorage.storeBaseURL(baseURL) } - Timber.i("checkBaseURL: result is $result") + logger.i { "checkBaseURL: result is $result" } _uiState.value = OperationUiState.fromResult(result) } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt index deefaa9..beaebda 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerFragment.kt @@ -10,40 +10,45 @@ import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.FragmentDisclaimerBinding +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.activity.MainActivityViewModel -import timber.log.Timber +import javax.inject.Inject @AndroidEntryPoint class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer) { + private val binding by viewBinding(FragmentDisclaimerBinding::bind) private val viewModel by viewModels() private val activityViewModel by activityViewModels() + @Inject + lateinit var logger: Logger + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") + logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } viewModel.isAccepted.observe(this, ::onAcceptStateChange) } private fun onAcceptStateChange(isAccepted: Boolean) { - Timber.v("onAcceptStateChange() called with: isAccepted = $isAccepted") + logger.v { "onAcceptStateChange() called with: isAccepted = $isAccepted" } if (isAccepted) navigateNext() } private fun navigateNext() { - Timber.v("navigateNext() called") + logger.v { "navigateNext() called" } findNavController().navigate(DisclaimerFragmentDirections.actionDisclaimerFragmentToBaseURLFragment()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 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 { - Timber.v("onViewCreated: okay clicked") + logger.v { "onViewCreated: okay clicked" } viewModel.acceptDisclaimer() } viewModel.okayCountDown.observe(viewLifecycleOwner) { - Timber.d("onViewCreated: new count $it") + logger.d { "onViewCreated: new count $it" } binding.okay.text = if (it > 0) resources.getQuantityString( R.plurals.fragment_disclaimer_button_okay_timer, it, it ) else getString(R.string.fragment_disclaimer_button_okay) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt index d91796f..36ad43f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModel.kt @@ -4,19 +4,20 @@ import androidx.annotation.VisibleForTesting import androidx.lifecycle.* import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @HiltViewModel class DisclaimerViewModel @Inject constructor( - private val disclaimerStorage: DisclaimerStorage + private val disclaimerStorage: DisclaimerStorage, + private val logger: Logger, ) : ViewModel() { val isAccepted: LiveData @@ -26,12 +27,12 @@ class DisclaimerViewModel @Inject constructor( private var isCountDownStarted = false fun acceptDisclaimer() { - Timber.v("acceptDisclaimer() called") + logger.v { "acceptDisclaimer() called" } viewModelScope.launch { disclaimerStorage.acceptDisclaimer() } } fun startCountDown() { - Timber.v("startCountDown() called") + logger.v { "startCountDown() called" } if (isCountDownStarted) return isCountDownStarted = true tickerFlow(COUNT_DOWN_TICK_PERIOD_SEC.toLong(), TimeUnit.SECONDS) @@ -48,7 +49,7 @@ class DisclaimerViewModel @Inject constructor( */ @VisibleForTesting 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) var counter = 0 while (true) { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt index f64ac60..b3d5880 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipeViewHolder.kt @@ -4,25 +4,42 @@ import androidx.recyclerview.widget.RecyclerView import gq.kirmanak.mealient.R import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding +import gq.kirmanak.mealient.logging.Logger 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 recipeImageLoader: RecipeImageLoader, private val clickListener: (RecipeSummaryEntity) -> Unit, ) : 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 { binding.root.resources.getString(R.string.view_holder_recipe_text_placeholder) } fun bind(item: RecipeSummaryEntity?) { - Timber.v("bind() called with: item = $item") + logger.v { "bind() called with: item = $item" } binding.name.text = item?.name ?: loadingPlaceholder recipeImageLoader.loadRecipeImage(binding.image, item) item?.let { entity -> binding.root.setOnClickListener { - Timber.d("bind: item clicked $entity") + logger.d { "bind: item clicked $entity" } clickListener(entity) } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt index 2bf424a..d775212 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesFragment.kt @@ -13,27 +13,34 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.FragmentRecipesBinding import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.refreshRequestFlow +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.activity.MainActivityViewModel import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader import gq.kirmanak.mealient.ui.recipes.images.RecipePreloaderFactory -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint class RecipesFragment : Fragment(R.layout.fragment_recipes) { + private val binding by viewBinding(FragmentRecipesBinding::bind) private val viewModel by viewModels() private val activityViewModel by activityViewModels() + @Inject + lateinit var logger: Logger + @Inject lateinit var recipeImageLoader: RecipeImageLoader + @Inject + lateinit var recipePagingAdapterFactory: RecipesPagingAdapter.Factory + @Inject lateinit var recipePreloaderFactory: RecipePreloaderFactory override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } activityViewModel.updateUiState { it.copy(loginButtonVisible = true, titleVisible = false, navigationVisible = true) } @@ -41,7 +48,7 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { } private fun navigateToRecipeInfo(recipeSummaryEntity: RecipeSummaryEntity) { - Timber.v("navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity") + logger.v { "navigateToRecipeInfo() called with: recipeSummaryEntity = $recipeSummaryEntity" } findNavController().navigate( RecipesFragmentDirections.actionRecipesFragmentToRecipeInfoFragment( recipeSlug = recipeSummaryEntity.slug, @@ -51,29 +58,32 @@ class RecipesFragment : Fragment(R.layout.fragment_recipes) { } private fun setupRecipeAdapter() { - Timber.v("setupRecipeAdapter() called") - val recipesAdapter = RecipesPagingAdapter(recipeImageLoader, ::navigateToRecipeInfo) + logger.v { "setupRecipeAdapter() called" } + val recipesAdapter = recipePagingAdapterFactory.build( + recipeImageLoader = recipeImageLoader, + clickListener = ::navigateToRecipeInfo + ) with(binding.recipes) { adapter = recipesAdapter addOnScrollListener(recipePreloaderFactory.create(recipesAdapter)) } collectWhenViewResumed(viewModel.pagingData) { - Timber.v("setupRecipeAdapter: received data update") + logger.v { "setupRecipeAdapter: received data update" } recipesAdapter.submitData(lifecycle, it) } collectWhenViewResumed(recipesAdapter.onPagesUpdatedFlow) { - Timber.v("setupRecipeAdapter: pages updated") + logger.v { "setupRecipeAdapter: pages updated" } binding.refresher.isRefreshing = false } - collectWhenViewResumed(binding.refresher.refreshRequestFlow()) { - Timber.v("setupRecipeAdapter: received refresh request") + collectWhenViewResumed(binding.refresher.refreshRequestFlow(logger)) { + logger.v { "setupRecipeAdapter: received refresh request" } recipesAdapter.refresh() } } override fun onDestroyView() { super.onDestroyView() - Timber.v("onDestroyView() called") + logger.v { "onDestroyView() called" } // Prevent RV leaking through mObservers list in adapter binding.recipes.adapter = null } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt index 002db21..c2cb7af 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/RecipesPagingAdapter.kt @@ -6,24 +6,40 @@ import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.databinding.ViewHolderRecipeBinding +import gq.kirmanak.mealient.logging.Logger 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 recipeViewHolderFactory: RecipeViewHolder.Factory, private val clickListener: (RecipeSummaryEntity) -> Unit ) : PagingDataAdapter(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) { val item = getItem(position) holder.bind(item) } 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 binding = ViewHolderRecipeBinding.inflate(inflater, parent, false) - return RecipeViewHolder(binding, recipeImageLoader, clickListener) + return recipeViewHolderFactory.build(recipeImageLoader, binding, clickListener) } private object RecipeDiffCallback : DiffUtil.ItemCallback() { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt index f0e2692..2c70caf 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeImageLoaderImpl.kt @@ -6,17 +6,18 @@ import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import dagger.hilt.android.scopes.FragmentScoped import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject @FragmentScoped class RecipeImageLoaderImpl @Inject constructor( private val fragment: Fragment, private val requestOptions: RequestOptions, + private val logger: Logger, ) : RecipeImageLoader { 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) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt index 11b8306..e1a0487 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt @@ -7,16 +7,32 @@ import com.bumptech.glide.load.model.ModelLoader import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.runBlocking -import timber.log.Timber import java.io.InputStream +import javax.inject.Inject +import javax.inject.Singleton -class RecipeModelLoader( +class RecipeModelLoader private constructor( private val recipeImageUrlProvider: RecipeImageUrlProvider, + private val logger: Logger, concreteLoader: ModelLoader, cache: ModelCache, ) : BaseGlideUrlLoader(concreteLoader, cache) { + @Singleton + class Factory @Inject constructor( + private val recipeImageUrlProvider: RecipeImageUrlProvider, + private val logger: Logger, + ) { + + fun build( + concreteLoader: ModelLoader, + cache: ModelCache, + ) = RecipeModelLoader(recipeImageUrlProvider, logger, concreteLoader, cache) + + } + override fun handles(model: RecipeSummaryEntity): Boolean = true override fun getUrl( @@ -25,7 +41,7 @@ class RecipeModelLoader( height: Int, options: Options? ): 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) } } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt index 2dcee04..d10b309 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoaderFactory.kt @@ -1,24 +1,24 @@ package gq.kirmanak.mealient.ui.recipes.images import com.bumptech.glide.load.model.* -import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import java.io.InputStream import javax.inject.Inject import javax.inject.Singleton @Singleton class RecipeModelLoaderFactory @Inject constructor( - private val recipeImageUrlProvider: RecipeImageUrlProvider, + private val recipeModelLoaderFactory: RecipeModelLoader.Factory, + private val logger: Logger, ) : ModelLoaderFactory { private val cache = ModelCache() override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { - Timber.v("build() called with: multiFactory = $multiFactory") + logger.v { "build() called with: multiFactory = $multiFactory" } val concreteLoader = multiFactory.build(GlideUrl::class.java, InputStream::class.java) - return RecipeModelLoader(recipeImageUrlProvider, concreteLoader, cache) + return recipeModelLoaderFactory.build(concreteLoader, cache) } override fun teardown() { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt index 5bd7411..6e9ace0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipePreloadModelProvider.kt @@ -8,22 +8,23 @@ import com.bumptech.glide.RequestBuilder import com.bumptech.glide.request.RequestOptions import dagger.hilt.android.scopes.FragmentScoped import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject class RecipePreloadModelProvider( private val adapter: PagingDataAdapter, private val fragment: Fragment, private val requestOptions: RequestOptions, + private val logger: Logger, ) : ListPreloader.PreloadModelProvider { override fun getPreloadItems(position: Int): List { - Timber.v("getPreloadItems() called with: position = $position") + logger.v { "getPreloadItems() called with: position = $position" } return adapter.peek(position)?.let { listOf(it) } ?: emptyList() } 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) } @@ -31,10 +32,11 @@ class RecipePreloadModelProvider( class Factory @Inject constructor( private val fragment: Fragment, private val requestOptions: RequestOptions, + private val logger: Logger, ) { fun create( adapter: PagingDataAdapter, - ) = RecipePreloadModelProvider(adapter, fragment, requestOptions) + ) = RecipePreloadModelProvider(adapter, fragment, requestOptions, logger) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt index e52ff11..4d8c0ac 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoFragment.kt @@ -14,8 +14,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.FragmentRecipeInfoBinding +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.images.RecipeImageLoader -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -24,8 +24,17 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { private val binding by viewBinding(FragmentRecipeInfoBinding::bind) private val arguments by navArgs() private val viewModel by viewModels() - private val ingredientsAdapter = RecipeIngredientsAdapter() - private val instructionsAdapter = RecipeInstructionsAdapter() + private val ingredientsAdapter by lazy { recipeIngredientsAdapterFactory.build() } + 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 lateinit var recipeImageLoader: RecipeImageLoader @@ -35,13 +44,13 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - Timber.v("onCreateView() called") + logger.v { "onCreateView() called" } return FragmentRecipeInfoBinding.inflate(inflater, container, false).root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Timber.v("onViewCreated() called") + logger.v { "onViewCreated() called" } with(binding) { ingredientsList.adapter = ingredientsAdapter @@ -55,7 +64,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { } private fun onUiStateChange(uiState: RecipeInfoUiState) = with(binding) { - Timber.v("onUiStateChange() called") + logger.v { "onUiStateChange() called" } ingredientsHolder.isVisible = uiState.areIngredientsVisible instructionsGroup.isVisible = uiState.areInstructionsVisible uiState.recipeInfo?.let { @@ -72,7 +81,7 @@ class RecipeInfoFragment : BottomSheetDialogFragment() { override fun onDestroyView() { super.onDestroyView() - Timber.v("onDestroyView() called") + logger.v { "onDestroyView() called" } // Prevent RV leaking through mObservers list in adapter with(binding) { ingredientsList.adapter = null diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt index 08f3502..cf40aba 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt @@ -7,32 +7,33 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @HiltViewModel class RecipeInfoViewModel @Inject constructor( private val recipeRepo: RecipeRepo, + private val logger: Logger, ) : ViewModel() { private val _uiState = MutableLiveData(RecipeInfoUiState()) val uiState: LiveData get() = _uiState 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() viewModelScope.launch { runCatchingExceptCancel { recipeRepo.loadRecipeInfo(recipeId, recipeSlug) } .onSuccess { - Timber.d("loadRecipeInfo: received recipe info = $it") + logger.d { "loadRecipeInfo: received recipe info = $it" } _uiState.value = RecipeInfoUiState( areIngredientsVisible = it.recipeIngredients.isNotEmpty(), areInstructionsVisible = it.recipeInstructions.isNotEmpty(), recipeInfo = it, ) } - .onFailure { Timber.e(it, "loadRecipeInfo: can't load recipe info") } + .onFailure { logger.e(it) { "loadRecipeInfo: can't load recipe info" } } } } } diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt index b3d3a8f..fad0dc6 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeIngredientsAdapter.kt @@ -7,17 +7,40 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity import gq.kirmanak.mealient.databinding.ViewHolderIngredientBinding +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.info.RecipeIngredientsAdapter.RecipeIngredientViewHolder -import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton -class RecipeIngredientsAdapter : - ListAdapter(RecipeIngredientDiffCallback) { +class RecipeIngredientsAdapter private constructor( + private val recipeIngredientViewHolderFactory: RecipeIngredientViewHolder.Factory, + private val logger: Logger, +) : ListAdapter(RecipeIngredientDiffCallback) { - class RecipeIngredientViewHolder( - private val binding: ViewHolderIngredientBinding + @Singleton + 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) { + + @Singleton + class Factory @Inject constructor( + private val logger: Logger, + ) { + + fun build(binding: ViewHolderIngredientBinding) = + RecipeIngredientViewHolder(binding, logger) + } + fun bind(item: RecipeIngredientEntity) { - Timber.v("bind() called with: item = $item") + logger.v { "bind() called with: item = $item" } binding.checkBox.text = item.note } } @@ -35,17 +58,17 @@ class RecipeIngredientsAdapter : } 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) - return RecipeIngredientViewHolder( + return recipeIngredientViewHolderFactory.build( ViewHolderIngredientBinding.inflate(inflater, parent, false) ) } 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) - Timber.d("onBindViewHolder: item is $item") + logger.d { "onBindViewHolder: item is $item" } holder.bind(item) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt index de6d995..1ebb47a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInstructionsAdapter.kt @@ -8,11 +8,23 @@ import androidx.recyclerview.widget.RecyclerView import gq.kirmanak.mealient.R import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.databinding.ViewHolderInstructionBinding +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.recipes.info.RecipeInstructionsAdapter.RecipeInstructionViewHolder -import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton -class RecipeInstructionsAdapter : - ListAdapter(RecipeInstructionDiffCallback) { +class RecipeInstructionsAdapter private constructor( + private val logger: Logger, + private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory, +) : ListAdapter(RecipeInstructionDiffCallback) { + + @Singleton + class Factory @Inject constructor( + private val logger: Logger, + private val recipeInstructionViewHolderFactory: RecipeInstructionViewHolder.Factory, + ) { + fun build() = RecipeInstructionsAdapter(logger, recipeInstructionViewHolderFactory) + } private object RecipeInstructionDiffCallback : DiffUtil.ItemCallback() { @@ -27,11 +39,19 @@ class RecipeInstructionsAdapter : ): Boolean = oldItem == newItem } - class RecipeInstructionViewHolder( - private val binding: ViewHolderInstructionBinding + class RecipeInstructionViewHolder private constructor( + private val binding: ViewHolderInstructionBinding, + private val logger: Logger, ) : 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) { - 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( R.string.view_holder_recipe_instructions_step, position + 1 ) @@ -40,17 +60,17 @@ class RecipeInstructionsAdapter : } 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) - return RecipeInstructionViewHolder( - ViewHolderInstructionBinding.inflate(inflater, parent, false) + return recipeInstructionViewHolderFactory.build( + ViewHolderInstructionBinding.inflate(inflater, parent, false), ) } 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) - Timber.d("onBindViewHolder: item is $item") + logger.d { "onBindViewHolder: item is $item" } holder.bind(item, position) } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt index e52975f..f2978b1 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashFragment.kt @@ -11,38 +11,43 @@ import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.extensions.setActionBarVisibility import gq.kirmanak.mealient.extensions.setSystemUiVisibility -import timber.log.Timber +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject @AndroidEntryPoint class SplashFragment : Fragment(R.layout.fragment_splash) { + private val viewModel by viewModels() + @Inject + lateinit var logger: Logger + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - Timber.v("onCreate() called with: savedInstanceState = $savedInstanceState") + logger.v { "onCreate() called with: savedInstanceState = $savedInstanceState" } viewModel.nextDestination.observe(this, ::onNextDestination) } private fun onNextDestination(navDirections: NavDirections) { - Timber.v("onNextDestination() called with: navDirections = $navDirections") + logger.v { "onNextDestination() called with: navDirections = $navDirections" } findNavController().navigate(navDirections) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - Timber.v("onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState") + logger.v { "onViewCreated() called with: view = $view, savedInstanceState = $savedInstanceState" } changeFullscreenState(true) } override fun onDestroyView() { super.onDestroyView() - Timber.v("onDestroyView() called") + logger.v { "onDestroyView() called" } changeFullscreenState(false) } private fun changeFullscreenState(isFullscreen: Boolean) { - Timber.v("changeFullscreenState() called with: isFullscreen = $isFullscreen") - (activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen) - activity?.setSystemUiVisibility(!isFullscreen) + logger.v { "changeFullscreenState() called with: isFullscreen = $isFullscreen" } + (activity as? AppCompatActivity)?.setActionBarVisibility(!isFullscreen, logger) + activity?.setSystemUiVisibility(!isFullscreen, logger) } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt index c434f6e..f58334e 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.add.models.AddRecipeRequest import gq.kirmanak.mealient.data.network.NetworkError import gq.kirmanak.mealient.data.network.ServiceFactory +import gq.kirmanak.mealient.logging.Logger import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -22,13 +23,16 @@ class AddRecipeDataSourceImplTest { @MockK lateinit var service: AddRecipeService + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: AddRecipeDataSourceImpl @Before fun setUp() { MockKAnnotations.init(this) coEvery { serviceProvider.provideService(any()) } returns service - subject = AddRecipeDataSourceImpl(serviceProvider) + subject = AddRecipeDataSourceImpl(serviceProvider, logger) } @Test(expected = NetworkError.NotMealie::class) diff --git a/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt index fa31ac6..78ab001 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt @@ -1,21 +1,22 @@ package gq.kirmanak.mealient.data.add.models import com.google.common.truth.Truth.assertThat +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import org.junit.Test class AddRecipeRequestTest { @Test fun `when construct from input then fills fields correctly`() { - val input = AddRecipeInput.newBuilder() - .setRecipeName("Recipe name") - .setRecipeDescription("Recipe description") - .setRecipeYield("Recipe yield") - .addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2")) - .addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2")) - .setIsRecipePublic(false) - .setAreCommentsDisabled(true) - .build() + val input = AddRecipeDraft( + recipeName = "Recipe name", + recipeDescription = "Recipe description", + recipeYield = "Recipe yield", + recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"), + recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"), + isRecipePublic = false, + areCommentsDisabled = true, + ) val expected = AddRecipeRequest( name = "Recipe name", @@ -58,16 +59,16 @@ class AddRecipeRequestTest { ) ) - val expected = AddRecipeInput.newBuilder() - .setRecipeName("Recipe name") - .setRecipeDescription("Recipe description") - .setRecipeYield("Recipe yield") - .addAllRecipeIngredients(listOf("Recipe ingredient 1", "Recipe ingredient 2")) - .addAllRecipeInstructions(listOf("Recipe instruction 1", "Recipe instruction 2")) - .setIsRecipePublic(false) - .setAreCommentsDisabled(true) - .build() + val expected = AddRecipeDraft( + recipeName = "Recipe name", + recipeDescription = "Recipe description", + recipeYield = "Recipe yield", + recipeInstructions = listOf("Recipe instruction 1", "Recipe instruction 2"), + recipeIngredients = listOf("Recipe ingredient 1", "Recipe ingredient 2"), + isRecipePublic = false, + areCommentsDisabled = true, + ) - assertThat(request.toInput()).isEqualTo(expected) + assertThat(request.toDraft()).isEqualTo(expected) } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt index 746c11e..23b99dc 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.network.NetworkError.* import gq.kirmanak.mealient.data.network.ServiceFactory 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_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME @@ -26,12 +27,15 @@ class AuthDataSourceImplTest { @MockK lateinit var authServiceFactory: ServiceFactory + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: AuthDataSourceImpl @Before fun setUp() { MockKAnnotations.init(this) - subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson()) + subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson(), logger) coEvery { authServiceFactory.provideService(any()) } returns authService } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index 562dd64..64f828b 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -4,6 +4,7 @@ import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthStorage +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN @@ -26,12 +27,15 @@ class AuthRepoImplTest { @MockK(relaxUnitFun = true) lateinit var storage: AuthStorage + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: AuthRepo @Before fun setUp() { MockKAnnotations.init(this) - subject = AuthRepoImpl(storage, dataSource) + subject = AuthRepoImpl(storage, dataSource, logger) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt index b19f22a..5208d82 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthStorageImplTest.kt @@ -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.EMAIL_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_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME import gq.kirmanak.mealient.test.HiltRobolectricTest +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest @@ -29,14 +32,18 @@ class AuthStorageImplTest : HiltRobolectricTest() { @ApplicationContext lateinit var context: Context + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: AuthStorage lateinit var sharedPreferences: SharedPreferences @Before fun setUp() { + MockKAnnotations.init(this) sharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE) - subject = AuthStorageImpl(sharedPreferences) + subject = AuthStorageImpl(sharedPreferences, logger) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt index 7320340..54df6a5 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt @@ -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.network.NetworkError 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.toJsonResponseBody import io.mockk.MockKAnnotations @@ -28,12 +29,15 @@ class VersionDataSourceImplTest { @MockK lateinit var versionServiceFactory: ServiceFactory + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: VersionDataSource @Before fun setUp() { MockKAnnotations.init(this) - subject = VersionDataSourceImpl(versionServiceFactory) + subject = VersionDataSourceImpl(versionServiceFactory, logger) coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } returns versionService } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt index 05a7c93..59050c6 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt @@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.network import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.impl.VersionService +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import io.mockk.* import io.mockk.impl.annotations.MockK @@ -27,12 +28,15 @@ class RetrofitServiceFactoryTest { @MockK lateinit var versionService: VersionService + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: ServiceFactory @Before fun setUp() { MockKAnnotations.init(this) - subject = retrofitBuilder.createServiceFactory(baseURLStorage) + subject = retrofitBuilder.createServiceFactory(baseURLStorage, logger) coEvery { retrofitBuilder.buildRetrofit(any()) } returns retrofit every { retrofit.create(eq(VersionService::class.java)) } returns versionService coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt index 03661e4..230f70b 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt @@ -2,6 +2,7 @@ package gq.kirmanak.mealient.data.recipes.impl import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.logging.Logger import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.impl.annotations.MockK @@ -18,10 +19,13 @@ class RecipeImageUrlProviderImplTest { @MockK lateinit var baseURLStorage: BaseURLStorage + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + @Before fun setUp() { MockKAnnotations.init(this) - subject = RecipeImageUrlProviderImpl(baseURLStorage) + subject = RecipeImageUrlProviderImpl(baseURLStorage, logger) prepareBaseURL("https://google.com/") } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt index 604f9a1..2a8801f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt @@ -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.network.RecipeDataSource 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.GET_CAKE_RESPONSE import io.mockk.MockKAnnotations @@ -32,12 +33,15 @@ class RecipeRepoImplTest { @MockK lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: RecipeRepo @Before fun setUp() { MockKAnnotations.init(this) - subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource) + subject = RecipeRepoImpl(remoteMediator, storage, pagingSourceFactory, dataSource, logger) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt index 0f3c322..6e25216 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt @@ -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.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -38,10 +39,13 @@ class RecipesRemoteMediatorTest { @MockK(relaxUnitFun = true) lateinit var pagingSourceFactory: InvalidatingPagingSourceFactory + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + @Before fun setUp() { MockKAnnotations.init(this) - subject = RecipesRemoteMediator(storage, dataSource, pagingSourceFactory) + subject = RecipesRemoteMediator(storage, dataSource, pagingSourceFactory, logger) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/test/HiltRobolectricTest.kt b/app/src/test/java/gq/kirmanak/mealient/test/HiltRobolectricTest.kt index b4dc9fb..0fd38d7 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/HiltRobolectricTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/HiltRobolectricTest.kt @@ -4,7 +4,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltTestApplication import org.junit.Before -import org.junit.BeforeClass import org.junit.Rule import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -13,13 +12,6 @@ import org.robolectric.annotation.Config @Config(application = HiltTestApplication::class, manifest = Config.NONE) abstract class HiltRobolectricTest { - companion object { - - @BeforeClass - @JvmStatic - fun setupTimber() = plantPrintLn() - } - @get:Rule var hiltRule = HiltAndroidRule(this) diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt b/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt index b58b3b4..5fc79bf 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/RobolectricTest.kt @@ -2,18 +2,9 @@ package gq.kirmanak.mealient.test import android.app.Application import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.BeforeClass import org.junit.runner.RunWith import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(application = Application::class, manifest = Config.NONE) -abstract class RobolectricTest { - - companion object { - - @BeforeClass - @JvmStatic - fun setupTimber() = plantPrintLn() - } -} \ No newline at end of file +abstract class RobolectricTest \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt b/app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt index da0a67f..b9dcf8b 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt @@ -2,15 +2,6 @@ package gq.kirmanak.mealient.test import okhttp3.MediaType.Companion.toMediaType import okhttp3.ResponseBody.Companion.toResponseBody -import timber.log.Timber 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() - } - }) -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt index 97a5497..1f289bc 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt @@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.add import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.models.AddRecipeRequest +import gq.kirmanak.mealient.logging.Logger import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -27,13 +28,16 @@ class AddRecipeViewModelTest { @MockK(relaxUnitFun = true) lateinit var addRecipeRepo: AddRecipeRepo + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: AddRecipeViewModel @Before fun setUp() { MockKAnnotations.init(this) Dispatchers.setMain(UnconfinedTestDispatcher()) - subject = AddRecipeViewModel(addRecipeRepo) + subject = AddRecipeViewModel(addRecipeRepo, logger) } @After diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index d633c9f..b41b7fa 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -3,6 +3,7 @@ package gq.kirmanak.mealient.ui.baseurl import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.VersionDataSource 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.RobolectricTest import io.mockk.MockKAnnotations @@ -24,12 +25,15 @@ class BaseURLViewModelTest : RobolectricTest() { @MockK lateinit var versionDataSource: VersionDataSource + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: BaseURLViewModel @Before fun setUp() { MockKAnnotations.init(this) - subject = BaseURLViewModel(baseURLStorage, versionDataSource) + subject = BaseURLViewModel(baseURLStorage, versionDataSource, logger) } @Test diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModelTest.kt index af25311..e186f93 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/disclaimer/DisclaimerViewModelTest.kt @@ -2,6 +2,7 @@ package gq.kirmanak.mealient.ui.disclaimer import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage +import gq.kirmanak.mealient.logging.Logger import io.mockk.MockKAnnotations import io.mockk.impl.annotations.MockK import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -17,12 +18,15 @@ class DisclaimerViewModelTest { @MockK(relaxUnitFun = true) lateinit var storage: DisclaimerStorage + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + lateinit var subject: DisclaimerViewModel @Before fun setUp() { MockKAnnotations.init(this) - subject = DisclaimerViewModel(storage) + subject = DisclaimerViewModel(storage, logger) } @Test diff --git a/database/build.gradle.kts b/database/build.gradle.kts index afb1731..7f5ab6f 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -39,5 +39,4 @@ dependencies { testImplementation(libs.google.truth) testImplementation(libs.io.mockk) - } \ No newline at end of file diff --git a/datastore/.gitignore b/datastore/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/datastore/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/datastore/build.gradle.kts b/datastore/build.gradle.kts new file mode 100644 index 0000000..5b3f57e --- /dev/null +++ b/datastore/build.gradle.kts @@ -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 +} diff --git a/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/DataStoreModule.kt b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/DataStoreModule.kt new file mode 100644 index 0000000..2d0ba3c --- /dev/null +++ b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/DataStoreModule.kt @@ -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 = 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 + ) + } + } +} \ No newline at end of file diff --git a/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeDraft.kt b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeDraft.kt new file mode 100644 index 0000000..57a6063 --- /dev/null +++ b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeDraft.kt @@ -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, + val recipeIngredients: List, + val isRecipePublic: Boolean, + val areCommentsDisabled: Boolean, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeInputSerializer.kt b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeInputSerializer.kt similarity index 93% rename from app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeInputSerializer.kt rename to datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeInputSerializer.kt index d7f0199..1b2e60a 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeInputSerializer.kt +++ b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeInputSerializer.kt @@ -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.Serializer diff --git a/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorage.kt b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorage.kt new file mode 100644 index 0000000..5d10385 --- /dev/null +++ b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorage.kt @@ -0,0 +1,12 @@ +package gq.kirmanak.mealient.datastore.recipe + +import kotlinx.coroutines.flow.Flow + +interface AddRecipeStorage { + + val updates: Flow + + suspend fun save(addRecipeDraft: AddRecipeDraft) + + suspend fun clear() +} \ No newline at end of file diff --git a/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorageImpl.kt b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorageImpl.kt new file mode 100644 index 0000000..7f7ac37 --- /dev/null +++ b/datastore/src/main/kotlin/gq/kirmanak/mealient/datastore/recipe/AddRecipeStorageImpl.kt @@ -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, + private val logger: Logger, +) : AddRecipeStorage { + + override val updates: Flow + 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() } + } +} \ No newline at end of file diff --git a/app/src/main/proto/AddRecipeInput.proto b/datastore/src/main/proto/AddRecipeInput.proto similarity index 82% rename from app/src/main/proto/AddRecipeInput.proto rename to datastore/src/main/proto/AddRecipeInput.proto index 13df823..760cc0c 100644 --- a/app/src/main/proto/AddRecipeInput.proto +++ b/datastore/src/main/proto/AddRecipeInput.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "gq.kirmanak.mealient.data.add.models"; +option java_package = "gq.kirmanak.mealient.datastore.recipe"; option java_multiple_files = true; message AddRecipeInput { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c723c46..ec53334 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,8 +41,6 @@ retrofitKotlinxSerialization = "0.8.0" kotlinxSerialization = "1.3.3" # https://github.com/square/okhttp/tags okhttp = "4.10.0" -# https://github.com/JakeWharton/timber/releases -timber = "5.0.1" # https://developer.android.com/jetpack/androidx/releases/paging paging = "3.1.1" # 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" } 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" } diff --git a/logging/.gitignore b/logging/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/logging/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/logging/build.gradle.kts b/logging/build.gradle.kts new file mode 100644 index 0000000..518b450 --- /dev/null +++ b/logging/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Appender.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Appender.kt new file mode 100644 index 0000000..ea590bd --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Appender.kt @@ -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) + +} \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogLevel.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogLevel.kt new file mode 100644 index 0000000..824c69e --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogLevel.kt @@ -0,0 +1,3 @@ +package gq.kirmanak.mealient.logging + +enum class LogLevel { VERBOSE, DEBUG, INFO, WARNING, ERROR } \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogcatAppender.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogcatAppender.kt new file mode 100644 index 0000000..638e1d3 --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LogcatAppender.kt @@ -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 + } \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Logger.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Logger.kt new file mode 100644 index 0000000..66b8314 --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/Logger.kt @@ -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) +} \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggerImpl.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggerImpl.kt new file mode 100644 index 0000000..831f3a8 --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggerImpl.kt @@ -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+)+$") + } +} \ No newline at end of file diff --git a/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggingModule.kt b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggingModule.kt new file mode 100644 index 0000000..ac258d4 --- /dev/null +++ b/logging/src/main/kotlin/gq/kirmanak/mealient/logging/LoggingModule.kt @@ -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 +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4145c27..a45a879 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,3 +21,5 @@ rootProject.name = "Mealient" include(":app") include(":database") +include(":datastore") +include(":logging")