diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f874b181..56a50a7a 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,8 @@ plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.google.services)
+ alias(libs.plugins.firebase.crashlytics)
id("kotlin-parcelize")
id("jacoco")
}
@@ -48,9 +50,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
-// firebaseCrashlytics {
-// mappingFileUploadEnabled true
-// }
}
getByName("debug") {
isMinifyEnabled = false
@@ -281,6 +280,11 @@ dependencies {
implementation(libs.mp4parser.muxer)
implementation(libs.androidx.media)
+ // Firebase
+ implementation(platform(libs.firebase.bom))
+ implementation(libs.firebase.crashlytics)
+ implementation(libs.firebase.analytics)
+
// Compose
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f7521e35..3bd831b5 100755
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,7 +6,7 @@
-
+
= Log.WARN
+
+ override fun log(priority: Int, tag: String?, message: String, throwable: Throwable?) {
+ val fullMessage = if (tag != null) "[$tag] $message" else message
+ crashlytics.log(fullMessage)
+ if (throwable != null) {
+ crashlytics.recordException(throwable)
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/analytics/FirebaseAnalyticsTracker.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/analytics/FirebaseAnalyticsTracker.kt
new file mode 100644
index 00000000..d29b62c6
--- /dev/null
+++ b/app/src/main/java/com/dimowner/audiorecorder/v2/analytics/FirebaseAnalyticsTracker.kt
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2026 Dmytro Ponomarenko
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.dimowner.audiorecorder.v2.analytics
+
+import com.google.firebase.analytics.FirebaseAnalytics
+import com.google.firebase.analytics.logEvent
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * [AnalyticsTracker] implementation that forwards events to Firebase Analytics.
+ *
+ * Each method maps to a dedicated custom event with snake_case parameter names
+ * that conform to the Firebase 40-character name limit.
+ *
+ * Note: Firebase Analytics is only active in release builds (i.e. builds that
+ * include a valid `google-services.json`). In debug builds the [NoOpAnalyticsTracker]
+ * is bound instead via [com.dimowner.audiorecorder.v2.di.AnalyticsModule].
+ */
+class FirebaseAnalyticsTracker @Inject constructor(
+ private val firebaseAnalytics: FirebaseAnalytics,
+) : AnalyticsTracker {
+
+ // ── Database migration ───────────────────────────────────────────────────
+
+ override fun trackDbMigrationStarted() {
+ log(EVENT_DB_MIGRATION_STARTED)
+ firebaseAnalytics.logEvent(EVENT_DB_MIGRATION_STARTED) {}
+ }
+
+ override fun trackDbMigrationSuccess(migratedRecordCount: Int, durationMs: Long) {
+ log(EVENT_DB_MIGRATION_SUCCESS, "migrated_record_count=$migratedRecordCount, duration_ms=$durationMs")
+ firebaseAnalytics.logEvent(EVENT_DB_MIGRATION_SUCCESS) {
+ param(PARAM_MIGRATED_RECORD_COUNT, migratedRecordCount.toLong())
+ param(PARAM_DURATION_MS, durationMs)
+ }
+ }
+
+ override fun trackDbMigrationFailed(error: Throwable) {
+ log(EVENT_DB_MIGRATION_FAILED, "error=${error.message}")
+ firebaseAnalytics.logEvent(EVENT_DB_MIGRATION_FAILED) {
+ param(PARAM_ERROR_MESSAGE, error.message?.take(100) ?: "unknown")
+ }
+ }
+
+ // ── App version switching ────────────────────────────────────────────────
+
+ override fun trackSwitchToAppV2() {
+ log(EVENT_SWITCH_TO_APP_V2)
+ firebaseAnalytics.logEvent(EVENT_SWITCH_TO_APP_V2) {}
+ }
+
+ override fun trackSwitchToLegacyApp() {
+ log(EVENT_SWITCH_TO_LEGACY_APP)
+ firebaseAnalytics.logEvent(EVENT_SWITCH_TO_LEGACY_APP) {}
+ }
+
+ // ── Broken record recovery ───────────────────────────────────────────────
+
+ override fun trackBrokenRecordDetected(format: String, count: Int) {
+ log(EVENT_BROKEN_RECORD_DETECTED, "format=$format, count=$count")
+ firebaseAnalytics.logEvent(EVENT_BROKEN_RECORD_DETECTED) {
+ param(PARAM_FORMAT, format)
+ param(PARAM_COUNT, count.toLong())
+ }
+ }
+
+ override fun trackBrokenRecordRestoreSuccess(format: String) {
+ log(EVENT_BROKEN_RECORD_RESTORE_SUCCESS, "format=$format")
+ firebaseAnalytics.logEvent(EVENT_BROKEN_RECORD_RESTORE_SUCCESS) {
+ param(PARAM_FORMAT, format)
+ }
+ }
+
+ override fun trackBrokenRecordRestoreFailed(format: String) {
+ log(EVENT_BROKEN_RECORD_RESTORE_FAILED, "format=$format")
+ firebaseAnalytics.logEvent(EVENT_BROKEN_RECORD_RESTORE_FAILED) {
+ param(PARAM_FORMAT, format)
+ }
+ }
+
+ // ── Lost records ─────────────────────────────────────────────────────────
+
+ override fun trackLostRecordsDetected(count: Int) {
+ log(EVENT_LOST_RECORDS_DETECTED, "count=$count")
+ firebaseAnalytics.logEvent(EVENT_LOST_RECORDS_DETECTED) {
+ param(PARAM_COUNT, count.toLong())
+ }
+ }
+
+ // ── Helpers ──────────────────────────────────────────────────────────────
+
+ private fun log(event: String, params: String = "") {
+ if (params.isEmpty()) {
+ Timber.d("FirebaseAnalyticsTracker: $event")
+ } else {
+ Timber.d("FirebaseAnalyticsTracker: $event [$params]")
+ }
+ }
+
+ // ── Event / parameter name constants ─────────────────────────────────────
+
+ companion object {
+ const val EVENT_DB_MIGRATION_STARTED = "db_migration_started"
+ const val EVENT_DB_MIGRATION_SUCCESS = "db_migration_success"
+ const val EVENT_DB_MIGRATION_FAILED = "db_migration_failed"
+ const val EVENT_SWITCH_TO_APP_V2 = "switch_to_app_v2"
+ const val EVENT_SWITCH_TO_LEGACY_APP = "switch_to_legacy_app"
+ const val EVENT_BROKEN_RECORD_DETECTED = "broken_record_detected"
+ const val EVENT_BROKEN_RECORD_RESTORE_SUCCESS = "broken_record_restore_ok"
+ const val EVENT_BROKEN_RECORD_RESTORE_FAILED = "broken_record_restore_fail"
+ const val EVENT_LOST_RECORDS_DETECTED = "lost_records_detected"
+
+ const val PARAM_MIGRATED_RECORD_COUNT = "migrated_record_count"
+ const val PARAM_DURATION_MS = "duration_ms"
+ const val PARAM_ERROR_MESSAGE = "error_message"
+ const val PARAM_FORMAT = "format"
+ const val PARAM_COUNT = "count"
+ }
+}
+
diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/di/AnalyticsModule.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/di/AnalyticsModule.kt
index 6c723ec7..849564d2 100644
--- a/app/src/main/java/com/dimowner/audiorecorder/v2/di/AnalyticsModule.kt
+++ b/app/src/main/java/com/dimowner/audiorecorder/v2/di/AnalyticsModule.kt
@@ -1,10 +1,14 @@
package com.dimowner.audiorecorder.v2.di
+import android.content.Context
import com.dimowner.audiorecorder.v2.analytics.AnalyticsTracker
-import com.dimowner.audiorecorder.v2.analytics.NoOpAnalyticsTracker
+import com.dimowner.audiorecorder.v2.analytics.FirebaseAnalyticsTracker
+import com.google.firebase.analytics.FirebaseAnalytics
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 javax.inject.Singleton
@@ -14,5 +18,13 @@ abstract class AnalyticsModule {
@Singleton
@Binds
- abstract fun bindAnalyticsTracker(impl: NoOpAnalyticsTracker): AnalyticsTracker
+ abstract fun bindAnalyticsTracker(impl: FirebaseAnalyticsTracker): AnalyticsTracker
+
+ companion object {
+
+ @Singleton
+ @Provides
+ fun provideFirebaseAnalytics(@ApplicationContext context: Context): FirebaseAnalytics =
+ FirebaseAnalytics.getInstance(context)
+ }
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 3a4a0aa9..b2dee819 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -13,4 +13,6 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.android.test) apply false
alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.google.services) apply false
+ alias(libs.plugins.firebase.crashlytics) apply false
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 32e2c9d3..502a0483 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,8 @@
[versions]
androidGradlePlugin = "9.2.1"
+firebaseBom = "34.14.0"
+googleServices = "4.4.4"
+firebaseCrashlyticsGradle = "3.0.7"
# @keep
compose-compiler = "1.5.8"
composeBom = "2026.05.01"
@@ -89,6 +92,9 @@ mp4parser-muxer = { module = "org.mp4parser:muxer", version.ref = "mp4parser" }
androidx-media = { module = "androidx.media:media", version.ref = "media" }
#androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
#androidx-fragment = { group = "androidx.fragment:fragment-ktx:", version.ref = "fragment" }
+firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebaseBom" }
+firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
+firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -96,4 +102,6 @@ android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
\ No newline at end of file
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" }
+firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlyticsGradle" }