diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index fb60773..f0dcc45 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") id("com.google.dagger.hilt.android") id("com.google.gms.google-services") + id("com.google.firebase.crashlytics") } android { @@ -19,6 +20,10 @@ android { versionCode = 1 versionName = "0.1.0" buildConfigField("String", "API_BASE_URL", "\"https://chat.daemonlord.ru/\"") + buildConfigField("String", "API_VERSION_HEADER", "\"2026-03\"") + buildConfigField("boolean", "FEATURE_ACCOUNT_MANAGEMENT", "true") + buildConfigField("boolean", "FEATURE_TWO_FACTOR", "true") + buildConfigField("boolean", "FEATURE_MEDIA_GALLERY", "true") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -62,6 +67,7 @@ android { } dependencies { + implementation(project(":core:common")) implementation(platform("com.google.firebase:firebase-bom:34.10.0")) implementation("androidx.core:core-ktx:1.15.0") @@ -98,6 +104,8 @@ dependencies { kapt("com.google.dagger:hilt-compiler:2.52") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") implementation("com.google.firebase:firebase-messaging") + implementation("com.google.firebase:firebase-crashlytics") + implementation("com.jakewharton.timber:timber:5.0.1") testImplementation("junit:junit:4.13.2") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") diff --git a/android/app/src/main/java/ru/daemonlord/messenger/MessengerApplication.kt b/android/app/src/main/java/ru/daemonlord/messenger/MessengerApplication.kt index 9ad958e..42d201e 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/MessengerApplication.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/MessengerApplication.kt @@ -5,14 +5,20 @@ import coil.ImageLoader import coil.ImageLoaderFactory import coil.disk.DiskCache import coil.memory.MemoryCache +import com.google.firebase.crashlytics.FirebaseCrashlytics import dagger.hilt.android.HiltAndroidApp import ru.daemonlord.messenger.core.notifications.NotificationChannels import java.io.File +import timber.log.Timber @HiltAndroidApp class MessengerApplication : Application(), ImageLoaderFactory { override fun onCreate() { super.onCreate() + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } + FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG) NotificationChannels.ensureCreated(this) } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/core/logging/TimberAppLogger.kt b/android/app/src/main/java/ru/daemonlord/messenger/core/logging/TimberAppLogger.kt new file mode 100644 index 0000000..4eec8b1 --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/core/logging/TimberAppLogger.kt @@ -0,0 +1,38 @@ +package ru.daemonlord.messenger.core.logging + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class TimberAppLogger @Inject constructor( + private val crashlytics: FirebaseCrashlytics, +) : AppLogger { + + override fun d(tag: String, message: String) { + Timber.tag(tag).d(message) + } + + override fun i(tag: String, message: String) { + Timber.tag(tag).i(message) + } + + override fun w(tag: String, message: String, throwable: Throwable?) { + if (throwable != null) { + Timber.tag(tag).w(throwable, message) + crashlytics.recordException(throwable) + } else { + Timber.tag(tag).w(message) + } + } + + override fun e(tag: String, message: String, throwable: Throwable?) { + if (throwable != null) { + Timber.tag(tag).e(throwable, message) + crashlytics.recordException(throwable) + } else { + Timber.tag(tag).e(message) + } + } +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/core/network/ApiVersionInterceptor.kt b/android/app/src/main/java/ru/daemonlord/messenger/core/network/ApiVersionInterceptor.kt new file mode 100644 index 0000000..dae63dc --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/core/network/ApiVersionInterceptor.kt @@ -0,0 +1,17 @@ +package ru.daemonlord.messenger.core.network + +import okhttp3.Interceptor +import okhttp3.Response +import ru.daemonlord.messenger.BuildConfig +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ApiVersionInterceptor @Inject constructor() : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder() + .header("X-Api-Version", BuildConfig.API_VERSION_HEADER) + .build() + return chain.proceed(request) + } +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/di/FeatureFlagsModule.kt b/android/app/src/main/java/ru/daemonlord/messenger/di/FeatureFlagsModule.kt new file mode 100644 index 0000000..db7f926 --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/di/FeatureFlagsModule.kt @@ -0,0 +1,23 @@ +package ru.daemonlord.messenger.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ru.daemonlord.messenger.BuildConfig +import ru.daemonlord.messenger.core.feature.FeatureFlags +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object FeatureFlagsModule { + @Provides + @Singleton + fun provideFeatureFlags(): FeatureFlags { + return FeatureFlags( + accountManagementEnabled = BuildConfig.FEATURE_ACCOUNT_MANAGEMENT, + twoFactorEnabled = BuildConfig.FEATURE_TWO_FACTOR, + mediaGalleryEnabled = BuildConfig.FEATURE_MEDIA_GALLERY, + ) + } +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/di/LoggingModule.kt b/android/app/src/main/java/ru/daemonlord/messenger/di/LoggingModule.kt new file mode 100644 index 0000000..4d9579d --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/di/LoggingModule.kt @@ -0,0 +1,25 @@ +package ru.daemonlord.messenger.di + +import com.google.firebase.crashlytics.FirebaseCrashlytics +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import ru.daemonlord.messenger.core.logging.AppLogger +import ru.daemonlord.messenger.core.logging.TimberAppLogger +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class LoggingModule { + @Binds + @Singleton + abstract fun bindAppLogger(logger: TimberAppLogger): AppLogger + + companion object { + @Provides + @Singleton + fun provideCrashlytics(): FirebaseCrashlytics = FirebaseCrashlytics.getInstance() + } +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/di/NetworkModule.kt b/android/app/src/main/java/ru/daemonlord/messenger/di/NetworkModule.kt index 022dabc..b7a7a67 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/di/NetworkModule.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/di/NetworkModule.kt @@ -13,11 +13,13 @@ import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import ru.daemonlord.messenger.BuildConfig import ru.daemonlord.messenger.core.network.AuthHeaderInterceptor +import ru.daemonlord.messenger.core.network.ApiVersionInterceptor import ru.daemonlord.messenger.core.network.TokenRefreshAuthenticator import ru.daemonlord.messenger.data.auth.api.AuthApiService import ru.daemonlord.messenger.data.chat.api.ChatApiService import ru.daemonlord.messenger.data.media.api.MediaApiService import ru.daemonlord.messenger.data.message.api.MessageApiService +import ru.daemonlord.messenger.data.user.api.UserApiService import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -87,11 +89,13 @@ object NetworkModule { @Singleton fun provideApiClient( loggingInterceptor: HttpLoggingInterceptor, + apiVersionInterceptor: ApiVersionInterceptor, authHeaderInterceptor: AuthHeaderInterceptor, tokenRefreshAuthenticator: TokenRefreshAuthenticator, ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(loggingInterceptor) + .addInterceptor(apiVersionInterceptor) .addInterceptor(authHeaderInterceptor) .authenticator(tokenRefreshAuthenticator) .connectTimeout(20, TimeUnit.SECONDS) @@ -137,4 +141,10 @@ object NetworkModule { fun provideMediaApiService(retrofit: Retrofit): MediaApiService { return retrofit.create(MediaApiService::class.java) } + + @Provides + @Singleton + fun provideUserApiService(retrofit: Retrofit): UserApiService { + return retrofit.create(UserApiService::class.java) + } } diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 3fdf563..f09c09c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,9 +1,12 @@ plugins { id("com.android.application") version "8.7.2" apply false + id("com.android.library") version "8.7.2" apply false id("org.jetbrains.kotlin.android") version "2.0.21" apply false + id("org.jetbrains.kotlin.jvm") version "2.0.21" apply false id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false id("com.google.dagger.hilt.android") version "2.52" apply false id("org.jetbrains.kotlin.plugin.serialization") version "2.0.21" apply false id("org.jetbrains.kotlin.kapt") version "2.0.21" apply false id("com.google.gms.google-services") version "4.4.4" apply false + id("com.google.firebase.crashlytics") version "3.0.3" apply false } diff --git a/android/core/common/build.gradle.kts b/android/core/common/build.gradle.kts new file mode 100644 index 0000000..f315f3c --- /dev/null +++ b/android/core/common/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("org.jetbrains.kotlin.jvm") +} + +kotlin { + jvmToolchain(17) +} diff --git a/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/feature/FeatureFlags.kt b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/feature/FeatureFlags.kt new file mode 100644 index 0000000..e0ddb59 --- /dev/null +++ b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/feature/FeatureFlags.kt @@ -0,0 +1,7 @@ +package ru.daemonlord.messenger.core.feature + +data class FeatureFlags( + val accountManagementEnabled: Boolean, + val twoFactorEnabled: Boolean, + val mediaGalleryEnabled: Boolean, +) diff --git a/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/logging/AppLogger.kt b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/logging/AppLogger.kt new file mode 100644 index 0000000..5e53313 --- /dev/null +++ b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/core/logging/AppLogger.kt @@ -0,0 +1,8 @@ +package ru.daemonlord.messenger.core.logging + +interface AppLogger { + fun d(tag: String, message: String) + fun i(tag: String, message: String) + fun w(tag: String, message: String, throwable: Throwable? = null) + fun e(tag: String, message: String, throwable: Throwable? = null) +} diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/common/AppError.kt b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/domain/common/AppError.kt similarity index 100% rename from android/app/src/main/java/ru/daemonlord/messenger/domain/common/AppError.kt rename to android/core/common/src/main/kotlin/ru/daemonlord/messenger/domain/common/AppError.kt diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/common/AppResult.kt b/android/core/common/src/main/kotlin/ru/daemonlord/messenger/domain/common/AppResult.kt similarity index 100% rename from android/app/src/main/java/ru/daemonlord/messenger/domain/common/AppResult.kt rename to android/core/common/src/main/kotlin/ru/daemonlord/messenger/domain/common/AppResult.kt diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 5d169bf..f730a78 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -16,3 +16,4 @@ dependencyResolutionManagement { rootProject.name = "MessengerAndroid" include(":app") +include(":core:common")