Skip to content
Merged
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
16 changes: 14 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
id "com.google.devtools.ksp"
id "org.sonarqube" version "4.2.1.3168"
id 'jacoco'
id 'com.google.firebase.crashlytics'
}

def getPropertiesFile = { path ->
Expand Down Expand Up @@ -70,6 +71,10 @@ android {
getPropertiesFile('app/config/uat.properties').each { p ->
buildConfigField 'String', p.key, p.value
}
// Disable mapping file upload in debug to avoid unnecessary uploads
firebaseCrashlytics {
mappingFileUploadEnabled false
}
}
release {
minifyEnabled false
Expand All @@ -78,6 +83,11 @@ android {
getPropertiesFile('app/config/prod.properties').each { p ->
buildConfigField 'String', p.key, p.value
}
// Unable to upload mapping files without this in release
firebaseCrashlytics {
mappingFileUploadEnabled true
nativeSymbolUploadEnabled true
}
}
}
compileOptions {
Expand Down Expand Up @@ -155,7 +165,8 @@ sonar {
**/DatastorePref.kt,\
**/FirebaseService.kt,\
**/WordpressService.kt,\
**/*Interceptor.kt,"
**/*Interceptor.kt,\
**/CrashlyticsUtils.kt,"
property "sonar.coverage.jacoco.xmlReportPaths", "build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml"
}
}
Expand Down Expand Up @@ -191,14 +202,15 @@ dependencies {
debugImplementation "androidx.compose.ui:ui-test-manifest:1.9.2"

// Import the BoM for the Firebase platform
implementation platform('com.google.firebase:firebase-bom:34.3.0')
implementation platform('com.google.firebase:firebase-bom:34.5.0')

// Firebase
implementation("com.google.firebase:firebase-auth-ktx:22.3.0")
implementation 'com.google.firebase:firebase-firestore-ktx:25.1.4'
implementation 'com.google.firebase:firebase-messaging:25.0.1'
implementation 'com.google.firebase:firebase-storage-ktx:21.0.2'
implementation "com.google.firebase:firebase-analytics"
implementation 'com.google.firebase:firebase-crashlytics-ndk'

// Navigation
implementation 'androidx.navigation:navigation-compose:2.9.5'
Expand Down
26 changes: 13 additions & 13 deletions app/src/main/java/com/xpeho/xpeapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import com.xpeho.xpeapp.ui.Home
import com.xpeho.xpeapp.ui.notifications.AlarmScheduler
import com.xpeho.xpeapp.ui.theme.XpeAppTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import com.xpeho.xpeapp.utils.DefaultDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -71,30 +71,30 @@ class MainActivity : ComponentActivity() {
MutableStateFlow(if (connectedLastTime) Screens.Home else Screens.Login)

// Periodic check for token expiration (every 8 hours)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(DefaultDispatcherProvider.io).launch {
while (true) {
kotlinx.coroutines.delay(TOKEN_CHECK_INTERVAL.inWholeMilliseconds)

// Check if we are connected and if the token has expired
val authState = XpeApp.appModule.authenticationManager.authState.value
if (authState is com.xpeho.xpeapp.domain.AuthState.Authenticated) {
if (!XpeApp.appModule.authenticationManager.isAuthValid()) {
XpeApp.appModule.authenticationManager.logout()
withContext(Dispatchers.Main) {
startScreenFlow.value = Screens.Login
}
if (authState is com.xpeho.xpeapp.domain.AuthState.Authenticated &&
!XpeApp.appModule.authenticationManager.isAuthValid()
) {
XpeApp.appModule.authenticationManager.logout()
withContext(DefaultDispatcherProvider.main) {
startScreenFlow.value = Screens.Login
}
}
}
}

// If the user was connected last time, try to restore the authentication state.
if (connectedLastTime) {
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(DefaultDispatcherProvider.io).launch {
XpeApp.appModule.authenticationManager.restoreAuthStateFromStorage()
if (!XpeApp.appModule.authenticationManager.isAuthValid()) {
XpeApp.appModule.authenticationManager.logout()
withContext(Dispatchers.Main) {
withContext(DefaultDispatcherProvider.main) {
startScreenFlow.value = Screens.Login
}
}
Expand Down Expand Up @@ -142,13 +142,13 @@ class MainActivity : ComponentActivity() {
private fun checkForUpdate() {
// Check for updates only in release mode
if (!BuildConfig.DEBUG) {
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(DefaultDispatcherProvider.io).launch {
val latestVersion = getLatestReleaseTag()
val currentVersion = getCurrentAppVersion()

// If the latest version is not null and is greater than the current version,
if (latestVersion != null && isVersionLessThan(currentVersion, latestVersion)) {
withContext(Dispatchers.Main) {
withContext(DefaultDispatcherProvider.main) {
showUpdateDialog(latestVersion)
}
}
Expand Down Expand Up @@ -181,7 +181,7 @@ class MainActivity : ComponentActivity() {
}

private suspend fun getLatestReleaseTag(): String? {
return withContext(Dispatchers.IO) {
return withContext(DefaultDispatcherProvider.io) {
val client = OkHttpClient()
val request = Request.Builder()
.url("https://api.github.com/repos/XPEHO/xpeapp_android/releases/latest")
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/java/com/xpeho/xpeapp/XpeApp.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.xpeho.xpeapp

import android.app.Application
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.xpeho.xpeapp.di.AppModule
import com.xpeho.xpeapp.di.MainAppModule

Expand All @@ -11,6 +12,10 @@ class XpeApp : Application() {

override fun onCreate() {
super.onCreate()

// Initialize Firebase Crashlytics
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = true

appModule = MainAppModule(appContext = this)
}
}
33 changes: 31 additions & 2 deletions app/src/main/java/com/xpeho/xpeapp/data/service/FirebaseService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,38 @@ import com.xpeho.xpeapp.data.NEWSLETTERS_COLLECTION
import com.xpeho.xpeapp.data.model.FeatureFlipping
import com.xpeho.xpeapp.data.model.Newsletter
import com.xpeho.xpeapp.data.model.toFeatureFlipping
import com.xpeho.xpeapp.utils.CrashlyticsUtils
import kotlinx.coroutines.tasks.await
import java.time.ZoneId

class FirebaseService {
suspend fun authenticate() {
FirebaseAuth.getInstance().signInAnonymously().await()
try {
CrashlyticsUtils.logEvent("Firebase: Tentative d'authentification anonyme")
FirebaseAuth.getInstance().signInAnonymously().await()
CrashlyticsUtils.logEvent("Firebase: Authentification anonyme réussie")
} catch (e: FirebaseException) {
CrashlyticsUtils.logEvent("Firebase: Erreur d'authentification anonyme")
CrashlyticsUtils.recordException(e)
throw e
}
}

fun isAuthenticated() = FirebaseAuth.getInstance().currentUser != null

fun signOut() = FirebaseAuth.getInstance().signOut()
fun signOut() {
try {
CrashlyticsUtils.logEvent("Firebase: Déconnexion")
FirebaseAuth.getInstance().signOut()
} catch (e: FirebaseException) {
CrashlyticsUtils.recordException(e)
throw e
}
}

suspend fun fetchFeatureFlipping(): List<FeatureFlipping> {
try {
CrashlyticsUtils.logEvent("Firebase: Récupération des feature flags")
val db = FirebaseFirestore.getInstance()
val document = db.collection(FEATURE_FLIPPING_COLLECTION)
.get()
Expand All @@ -37,9 +55,14 @@ class FirebaseService {
featureFlippingList[featureFlippingList.indexOf(featureFlipping)] = featureFlipping
}
}
CrashlyticsUtils.logEvent("Firebase: Feature flags " +
"récupérés avec succès (${featureFlippingList.size} éléments)")
return featureFlippingList
} catch (firebaseException: FirebaseException) {
Log.e("fetchFeatureFlipping", "Error getting documents: $firebaseException")
CrashlyticsUtils.logEvent("Firebase: Erreur lors " +
"de la récupération des feature flags")
CrashlyticsUtils.recordException(firebaseException)
return emptyList()
}
}
Expand All @@ -50,6 +73,7 @@ class FirebaseService {
val defaultSystemOfZone = ZoneId.systemDefault()

try {
CrashlyticsUtils.logEvent("Firebase: Récupération des newsletters")
db.collection(NEWSLETTERS_COLLECTION)
.get()
.await()
Expand All @@ -73,8 +97,13 @@ class FirebaseService {
)
newslettersList.add(newsletter)
}
CrashlyticsUtils.logEvent("Firebase: Newsletters " +
"récupérées avec succès (${newslettersList.size} éléments)")
} catch (firebaseException: FirebaseException) {
Log.d("fetchNewsletters", "Error getting documents: ", firebaseException)
CrashlyticsUtils.logEvent("Firebase: Erreur lors de " +
"la récupération des newsletters")
CrashlyticsUtils.recordException(firebaseException)
}

return newslettersList.sortedByDescending { it.date }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.xpeho.xpeapp.data.model.qvst.QvstCampaign
import com.xpeho.xpeapp.data.model.qvst.QvstProgress
import com.xpeho.xpeapp.data.model.qvst.QvstQuestion
import com.xpeho.xpeapp.data.model.user.UpdatePasswordResult
import com.xpeho.xpeapp.utils.CrashlyticsUtils
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketTimeoutException
Expand Down Expand Up @@ -126,6 +127,8 @@ class WordpressRepository(
username: String,
onlyActive: Boolean = false
): List<QvstCampaignEntity>? {
CrashlyticsUtils.setCurrentFeature("qvst")
CrashlyticsUtils.setCustomKey("qvst_only_active", onlyActive.toString())
handleServiceExceptions(
tryBody = {
val campaigns = if (onlyActive) {
Expand Down Expand Up @@ -403,11 +406,25 @@ class WordpressRepository(
catchBody: (Exception) -> T
): T {
return try {
tryBody()
val result = tryBody()
// Crashlytics : Log de succès pour les appels API
CrashlyticsUtils.logEvent("API WordPress: Appel réussi")
CrashlyticsUtils.setCurrentFeature("api_wordpress")
result
} catch (e: Exception) {
if (isNetworkError(e)) {
// Crashlytics : Log des erreurs réseau
CrashlyticsUtils.logEvent("API WordPress: Erreur réseau - ${e.javaClass.simpleName}")
CrashlyticsUtils.recordException(e)
CrashlyticsUtils.setCustomKey("last_api_error", e.message ?: "Erreur inconnue")
CrashlyticsUtils.setCustomKey("last_api_error_time", System.currentTimeMillis().toString())
CrashlyticsUtils.setCurrentFeature("api_wordpress_error")
catchBody(e)
} else {
// Crashlytics : Log des erreurs non-réseau (généralement plus graves)
CrashlyticsUtils.logEvent("API WordPress: Erreur critique - ${e.javaClass.simpleName}")
CrashlyticsUtils.recordException(e)
CrashlyticsUtils.setCurrentFeature("api_wordpress_critical")
throw e
}
}
Expand Down
5 changes: 1 addition & 4 deletions app/src/main/java/com/xpeho/xpeapp/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.xpeho.xpeapp.di
import android.content.Context
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.gson.GsonBuilder
import com.google.type.DateTime
import com.xpeho.xpeapp.BuildConfig
import com.xpeho.xpeapp.data.DatastorePref
import com.xpeho.xpeapp.data.dateConverter.DateTimeTypeAdapter
Expand Down Expand Up @@ -42,7 +41,7 @@ class MainAppModule(
// Register custom date format for Date and DateTime
.registerTypeAdapter(Date::class.java, DateTypeAdapter())
.registerTypeAdapter(LocalTime::class.java, DateTimeTypeAdapter())
.setLenient().create()
.create()

private val logging = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
private val authorization by lazy {
Expand Down Expand Up @@ -111,5 +110,3 @@ class MainAppModule(
)
}
}


48 changes: 46 additions & 2 deletions app/src/main/java/com/xpeho/xpeapp/domain/AuthenticationManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.xpeho.xpeapp.data.model.WordpressToken
import com.xpeho.xpeapp.data.service.FirebaseService
import com.xpeho.xpeapp.data.service.WordpressRepository
import com.xpeho.xpeapp.di.TokenProvider
import com.xpeho.xpeapp.utils.CrashlyticsUtils
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
Expand Down Expand Up @@ -92,6 +93,15 @@ class AuthenticationManager(
}

suspend fun login(username: String, password: String): AuthResult<WordpressToken> = coroutineScope {
// Crashlytics : Contexte de connexion
CrashlyticsUtils.setCurrentScreen("login")
CrashlyticsUtils.setCurrentFeature("authentication")
CrashlyticsUtils.setUserContext(isLoggedIn = false)

// Crashlytics : Log de la tentative de connexion
CrashlyticsUtils.logEvent("Tentative de connexion pour: $username")
CrashlyticsUtils.setCustomKey("last_login_attempt", System.currentTimeMillis().toString())

val wpDefRes = async {
wordpressRepo.authenticate(AuthentificationBody(username, password))
}
Expand All @@ -103,14 +113,25 @@ class AuthenticationManager(
},
catchBody = { e ->
Log.e("AuthenticationManager: login", "Network error: ${e.message}")
// Crashlytics : Enregistrer l'erreur réseau
CrashlyticsUtils.recordException(e)
return@async AuthResult.NetworkError
}
)
}

val result = when (val wpRes = wpDefRes.await()) {
is AuthResult.NetworkError -> AuthResult.NetworkError
is AuthResult.Unauthorized -> AuthResult.Unauthorized
is AuthResult.NetworkError -> {
// Crashlytics : Log erreur réseau
CrashlyticsUtils.logEvent("Erreur réseau lors de la connexion pour: $username")
AuthResult.NetworkError
}
is AuthResult.Unauthorized -> {
// Crashlytics : Log erreur d'authentification
CrashlyticsUtils.logEvent("Erreur d'authentification pour: $username")
CrashlyticsUtils.setCustomKey("last_failed_login", username)
AuthResult.Unauthorized
}
else -> {
val fbRes = fbDefRes.await()
when (fbRes) {
Expand All @@ -121,6 +142,15 @@ class AuthenticationManager(
writeAuthentication(authData)
_authState.value = AuthState.Authenticated(authData)
tokenProvider.set("Bearer ${authData.token.token}")

// Crashlytics : Log connexion réussie
CrashlyticsUtils.logEvent("Connexion réussie pour: $username")
CrashlyticsUtils.setUserId(authData.token.userEmail)
CrashlyticsUtils.setCustomKey("user_email", authData.token.userEmail)
CrashlyticsUtils.setCustomKey("last_successful_login", System.currentTimeMillis().toString())
CrashlyticsUtils.setUserContext(isLoggedIn = true)
CrashlyticsUtils.setCurrentScreen("home")

wpRes
}
}
Expand All @@ -140,10 +170,24 @@ class AuthenticationManager(
}

suspend fun logout() {
// Crashlytics : Contexte de déconnexion
CrashlyticsUtils.setCurrentFeature("authentication")
CrashlyticsUtils.setCurrentScreen("logout")

// Crashlytics : Log de la déconnexion
val currentUser = getAuthData()?.token?.userEmail ?: "utilisateur_inconnu"
CrashlyticsUtils.logEvent("Déconnexion de l'utilisateur: $currentUser")

firebaseService.signOut()
datastorePref.clearAuthData()
datastorePref.setWasConnectedLastTime(false)
_authState.value = AuthState.Unauthenticated

// Crashlytics : Nettoyer les infos utilisateur
CrashlyticsUtils.setUserId("")
CrashlyticsUtils.setCustomKey("user_email", "")
CrashlyticsUtils.setUserContext(isLoggedIn = false)
CrashlyticsUtils.setCurrentScreen("login")
}
}

Expand Down
Loading
Loading