Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -48,9 +50,6 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
// firebaseCrashlytics {
// mappingFileUploadEnabled true
// }
}
getByName("debug") {
isMinifyEnabled = false
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<!-- <uses-permission android:name="android.permission.INTERNET" />-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/java/com/dimowner/audiorecorder/ARApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import android.telephony.TelephonyCallback
import android.telephony.TelephonyManager
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.dimowner.audiorecorder.app.migration.DatabaseMigrationService
import com.dimowner.audiorecorder.audio.player.PlayerContractNew
import com.dimowner.audiorecorder.util.AndroidUtils
import com.dimowner.audiorecorder.v2.audio.AudioRecorderDelegate
Expand All @@ -40,8 +41,6 @@ import timber.log.Timber
import timber.log.Timber.DebugTree
import javax.inject.Inject

//import com.google.firebase.FirebaseApp;

@HiltAndroidApp
class ARApplication : Application() {

Expand All @@ -58,6 +57,9 @@ class ARApplication : Application() {
if (BuildConfig.DEBUG) {
//Timber initialization
Timber.plant(DebugTree())
// Firebase is not configured for the debug flavor — skip initialization.
} else {
Timber.plant(CrashlyticsTree())
}
super.onCreate()
PACKAGE_NAME = applicationContext.packageName
Expand Down
42 changes: 42 additions & 0 deletions app/src/main/java/com/dimowner/audiorecorder/CrashlyticsTree.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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

import android.util.Log
import com.google.firebase.crashlytics.FirebaseCrashlytics
import timber.log.Timber

/**
* A Timber tree that forwards WARNING / ERROR / WTF messages to Firebase Crashlytics.
* Throwables are recorded as non-fatal exceptions; plain messages are added as log entries.
*/
class CrashlyticsTree : Timber.Tree() {

private val crashlytics = FirebaseCrashlytics.getInstance()

override fun isLoggable(tag: String?, priority: Int): Boolean =
priority >= 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)
}
}
}

Original file line number Diff line number Diff line change
@@ -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"
}
}

Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
}
}
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
10 changes: 9 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -89,11 +92,16 @@ 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" }
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" }
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" }