diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 71572a0..f5d8e46 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import com.technogym.android.absolute.strength.coreDomainModuleName + plugins { alias(libs.plugins.cvshowcase.application) alias(libs.plugins.cvshowcase.flavors) @@ -14,6 +16,7 @@ dependencies{ //App dependencies implementation(project(":$coreModuleName:$designSystemModuleName")) implementation(project(":$coreModuleName:$commonModuleName")) + implementation(project(":$coreModuleName:$coreDomainModuleName")) implementation(project(":$coreModuleName:$coreDataModuleName")) //Feature module dependencies implementation(project(":$featureModuleName:$profileModuleName")) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb43..8170347 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -12,10 +12,15 @@ # public *; #} -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable +# Preserve line number information for debugging stack traces sent to Crashlytics. +-keepattributes SourceFile,LineNumberTable + +# Hide the original source file name in stack traces. +-renamesourcefileattribute SourceFile + +#Firebase +-keep class com.crashlytics.** { *; } +-dontwarn com.crashlytics.** +-keep public class * extends java.lang.Exception + -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/debug/java/com/alexfin90/cvshowcase/logging/CvShowcaseDebugTree.kt b/app/src/debug/java/com/alexfin90/cvshowcase/logging/CvShowcaseDebugTree.kt new file mode 100644 index 0000000..10de83c --- /dev/null +++ b/app/src/debug/java/com/alexfin90/cvshowcase/logging/CvShowcaseDebugTree.kt @@ -0,0 +1,10 @@ +package com.alexfin90.cvshowcase.logging + +import timber.log.Timber + +object CvShowcaseDebugTree : Timber.DebugTree() { + + override fun createStackElementTag(element: StackTraceElement): String { + return "${super.createStackElementTag(element)}:${element.lineNumber}" + } +} diff --git a/app/src/debug/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt b/app/src/debug/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt new file mode 100644 index 0000000..f825998 --- /dev/null +++ b/app/src/debug/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt @@ -0,0 +1,7 @@ +package com.alexfin90.cvshowcase.logging + +import timber.log.Timber + +fun plantTimberTree() { + Timber.plant(CvShowcaseDebugTree) +} diff --git a/app/src/main/java/com/alexfin90/cvshowcase/CvShowcaseApplication.kt b/app/src/main/java/com/alexfin90/cvshowcase/CvShowcaseApplication.kt index eb8470b..1f01d7b 100644 --- a/app/src/main/java/com/alexfin90/cvshowcase/CvShowcaseApplication.kt +++ b/app/src/main/java/com/alexfin90/cvshowcase/CvShowcaseApplication.kt @@ -4,14 +4,15 @@ import android.app.Application import android.os.StrictMode import android.os.StrictMode.ThreadPolicy.Builder import android.os.StrictMode.VmPolicy +import com.alexfin90.cvshowcase.logging.plantTimberTree import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class CvApplication : Application() { override fun onCreate() { super.onCreate() + plantTimberTree() setStrictModePolicy() - //TODO Timber } } diff --git a/app/src/main/java/com/alexfin90/cvshowcase/MainActivity.kt b/app/src/main/java/com/alexfin90/cvshowcase/MainActivity.kt index 96cd3e7..8940a6d 100644 --- a/app/src/main/java/com/alexfin90/cvshowcase/MainActivity.kt +++ b/app/src/main/java/com/alexfin90/cvshowcase/MainActivity.kt @@ -19,12 +19,14 @@ import com.alexfin90.designsystem.theme.CvshowcaseTheme import com.alexfin90.detailexperience.DetailExperienceScreen import com.alexfin90.experience.ExperienceScreen import dagger.hilt.android.AndroidEntryPoint +import timber.log.Timber @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + Timber.d("onCreate") enableEdgeToEdge() setContent { CvshowcaseTheme { diff --git a/app/src/release/java/com/alexfin90/cvshowcase/logging/CvShowcaseReleaseTree.kt b/app/src/release/java/com/alexfin90/cvshowcase/logging/CvShowcaseReleaseTree.kt new file mode 100644 index 0000000..c5e6213 --- /dev/null +++ b/app/src/release/java/com/alexfin90/cvshowcase/logging/CvShowcaseReleaseTree.kt @@ -0,0 +1,57 @@ +package com.alexfin90.cvshowcase.logging + +import android.util.Log +import com.alexfin90.domain.exception.NonFatalException +import com.google.firebase.crashlytics.FirebaseCrashlytics +import timber.log.Timber + +object CvShowcaseReleaseTree : Timber.Tree() { + + private const val TAG = "CvShowcase" + private const val CRASHLYTICS_KEY_PRIORITY = "priority" + private const val CRASHLYTICS_KEY_TAG = "tag" + private const val CRASHLYTICS_KEY_MESSAGE = "message" + private const val CRASHLYTICS_KEY_ERROR_CODE = "code" + + override fun isLoggable(tag: String?, priority: Int): Boolean { + return priority >= Log.WARN + } + + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + val logTag = tag ?: TAG + + // Always output to Logcat for WARN/ERROR/ASSERT + Log.println(priority, logTag, message) + + // Send ONLY NonFatalException to Crashlytics + if (t is NonFatalException) { + recordToCrashlytics(priority, logTag, message, t) + } + } + + private fun recordToCrashlytics( + priority: Int, + tag: String, + message: String, + t: NonFatalException, + ) { + try { + val crashlytics = FirebaseCrashlytics.getInstance() + crashlytics.setCustomKey(CRASHLYTICS_KEY_PRIORITY, priorityLabel(priority)) + crashlytics.setCustomKey(CRASHLYTICS_KEY_TAG, tag) + crashlytics.setCustomKey(CRASHLYTICS_KEY_MESSAGE, message) + crashlytics.setCustomKey(CRASHLYTICS_KEY_ERROR_CODE, t.code) + crashlytics.log("$tag: $message") + crashlytics.recordException(t) + } catch (_: IllegalStateException) { + // Crashlytics not initialized (e.g., mock flavor) + } + } + + private fun priorityLabel(priority: Int): String = when (priority) { + Log.ERROR -> "ERROR" + Log.WARN -> "WARN" + Log.ASSERT -> "ASSERT" + else -> "UNKNOWN" + } +} diff --git a/app/src/release/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt b/app/src/release/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt new file mode 100644 index 0000000..295f2fa --- /dev/null +++ b/app/src/release/java/com/alexfin90/cvshowcase/logging/TimberConfiguration.kt @@ -0,0 +1,7 @@ +package com.alexfin90.cvshowcase.logging + +import timber.log.Timber + +fun plantTimberTree() { + Timber.plant(CvShowcaseReleaseTree) +} diff --git a/core/domain/consumer-rules.pro b/core/domain/consumer-rules.pro index e69de29..0fb473f 100644 --- a/core/domain/consumer-rules.pro +++ b/core/domain/consumer-rules.pro @@ -0,0 +1,2 @@ +# Optional: Keep custom exceptions. +-keep class com.alexfin90.domain.exception.** { *; } \ No newline at end of file diff --git a/core/domain/src/main/java/com/alexfin90/domain/exception/NonFatalException.kt b/core/domain/src/main/java/com/alexfin90/domain/exception/NonFatalException.kt new file mode 100644 index 0000000..c41b5de --- /dev/null +++ b/core/domain/src/main/java/com/alexfin90/domain/exception/NonFatalException.kt @@ -0,0 +1,10 @@ +package com.alexfin90.domain.exception + +open class NonFatalException( + open val code: String, + override val message: String, +) : Exception(message) { + companion object { + const val GENERIC_NON_FATAL_ERROR = "GENERIC_NON_FATAL_ERROR" + } +} diff --git a/feature/detailexperience/src/main/java/com/alexfin90/detailexperience/DetailExperienceViewModel.kt b/feature/detailexperience/src/main/java/com/alexfin90/detailexperience/DetailExperienceViewModel.kt index aa50de1..6a816a0 100644 --- a/feature/detailexperience/src/main/java/com/alexfin90/detailexperience/DetailExperienceViewModel.kt +++ b/feature/detailexperience/src/main/java/com/alexfin90/detailexperience/DetailExperienceViewModel.kt @@ -9,6 +9,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update +import timber.log.Timber import javax.inject.Inject @Stable @@ -21,6 +22,7 @@ class DetailExperienceViewModel @Inject constructor( val uiState = _uiState.asStateFlow() init { + Timber.d("init") val title: String = savedStateHandle.toRoute().title _uiState.update { it.copy(title = title) diff --git a/feature/experience/src/main/java/com/alexfin90/experience/ExperienceViewModel.kt b/feature/experience/src/main/java/com/alexfin90/experience/ExperienceViewModel.kt index ce68f62..7a7cd28 100644 --- a/feature/experience/src/main/java/com/alexfin90/experience/ExperienceViewModel.kt +++ b/feature/experience/src/main/java/com/alexfin90/experience/ExperienceViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.update +import timber.log.Timber import javax.inject.Inject @@ -33,10 +34,10 @@ class ExperienceViewModel @Inject constructor( companion object { private const val DEBOUNCE_TIME = 500L - private val TAG = ExperienceViewModel::javaClass.name } init { + Timber.d("init") observeCvUseCase() .onStart { _uiState.update { it.copy(isLoading = true) } } .onEach { cv ->