From 73a7f46509d11d751d6f3e13286b576a3dd258f9 Mon Sep 17 00:00:00 2001 From: mac Date: Mon, 9 Mar 2026 20:25:10 -0400 Subject: [PATCH] test --- app/build.gradle.kts | 16 ++--- .../java/com/example/scanwich/AuthWrapper.kt | 16 +++-- .../java/com/example/scanwich/Database.kt | 14 +++- .../com/example/scanwich/FirebaseUtils.kt | 33 +++++++++- .../java/com/example/scanwich/MainActivity.kt | 39 +++++++---- .../java/com/example/scanwich/Networking.kt | 65 +++++++++++++++---- .../com/example/scanwich/SettingsScreen.kt | 57 ++++++---------- .../java/com/example/scanwich/SportScreen.kt | 57 ++++++++++++---- release-notes.txt | 57 ++++++++-------- 9 files changed, 231 insertions(+), 123 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 40787d6..6534c1e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,6 @@ android { minSdk = 24 targetSdk = 36 - // Incrémentation automatique du versionCode basé sur le temps versionCode = (System.currentTimeMillis() / 60000).toInt() versionName = "1.0." + (System.currentTimeMillis() / 3600000).toString().takeLast(3) @@ -26,7 +25,6 @@ android { } signingConfigs { - // Chargement des propriétés depuis local.properties val keystoreProperties = Properties() val keystorePropertiesFile = rootProject.file("local.properties") if (keystorePropertiesFile.exists()) { @@ -35,9 +33,9 @@ android { getByName("debug") { storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore") - getByName("debug").storePassword = "android" - getByName("debug").keyAlias = "androiddebugkey" - getByName("debug").keyPassword = "android" + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" } create("release") { @@ -53,7 +51,7 @@ android { buildTypes { release { - isMinifyEnabled = true // Activer l'offuscation + isMinifyEnabled = true signingConfig = signingConfigs.getByName("release") proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -111,9 +109,7 @@ dependencies { implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.icons.extended) - // SDK Firebase App Distribution COMPLET (API + Implémentation) implementation(libs.firebase.appdistribution) - implementation(libs.google.generativeai) implementation(libs.coil.compose) implementation(libs.androidx.exifinterface) @@ -133,17 +129,13 @@ dependencies { implementation(libs.firebase.firestore) implementation(libs.firebase.appcheck.playintegrity) - // Barcode Scanning & Camera implementation(libs.mlkit.barcode.scanning) implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.view) - // PDF generation implementation(libs.itext7.core) - - // Security implementation(libs.androidx.security.crypto) testImplementation(libs.junit) diff --git a/app/src/main/java/com/example/scanwich/AuthWrapper.kt b/app/src/main/java/com/example/scanwich/AuthWrapper.kt index 21b5cd7..fcbc8f4 100644 --- a/app/src/main/java/com/example/scanwich/AuthWrapper.kt +++ b/app/src/main/java/com/example/scanwich/AuthWrapper.kt @@ -100,24 +100,28 @@ fun AuthWrapper(dao: AppDao) { if (email != null && email.isNotEmpty()) { Log.d("Auth", "Vérification de l'autorisation pour l'email: '$email'") try { - // On spécifie explicitement la base de données "scan-wich" val db = FirebaseFirestore.getInstance("scan-wich") val docRef = db.collection("authorized_users").document(email) val document = docRef.get().await() if (document.exists()) { - Log.d("Auth", "Accès AUTORISÉ pour '$email'. Document trouvé.") + Log.d("Auth", "Accès AUTORISÉ pour '$email'.") + + // --- NOUVEAU : Rapatriement des données --- + coroutineScope.launch(Dispatchers.IO) { + FirebaseUtils.fetchAllDataFromFirestore(dao) + } + isAuthorized = true } else { - Log.w("Auth", "Accès REFUSÉ pour '$email'. Document NON trouvé.") + Log.w("Auth", "Accès REFUSÉ pour '$email'.") isAuthorized = false } } catch (e: Exception) { - Log.e("Auth", "Erreur critique Firestore. Vérifiez les règles de sécurité.", e) + Log.e("Auth", "Erreur critique Firestore.", e) isAuthorized = false } } else if (firebaseUser != null) { - Log.w("Auth", "L'utilisateur est connecté mais son email est vide.") isAuthorized = false } } @@ -132,7 +136,7 @@ fun AuthWrapper(dao: AppDao) { Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() Spacer(modifier = Modifier.height(8.dp)) - Text("Vérification de l'accès...") + Text("Vérification et synchronisation...") } } } diff --git a/app/src/main/java/com/example/scanwich/Database.kt b/app/src/main/java/com/example/scanwich/Database.kt index 22ef1db..cd860cc 100644 --- a/app/src/main/java/com/example/scanwich/Database.kt +++ b/app/src/main/java/com/example/scanwich/Database.kt @@ -51,21 +51,29 @@ data class FavoriteMeal( @Dao interface AppDao { - @Insert suspend fun insertMeal(meal: Meal): Long + // On ajoute OnConflictStrategy.REPLACE pour la synchronisation + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMeal(meal: Meal): Long + @Delete suspend fun deleteMeal(meal: Meal) @Query("SELECT * FROM meals WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC") fun getMealsForDay(startOfDay: Long, endOfDay: Long): Flow> @Query("SELECT * FROM meals WHERE date >= :start AND date <= :end ORDER BY date ASC") suspend fun getMealsInRangeSync(start: Long, end: Long): List - @Insert suspend fun insertGlycemia(glycemia: Glycemia): Long + // On ajoute OnConflictStrategy.REPLACE pour la synchronisation + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertGlycemia(glycemia: Glycemia): Long + @Delete suspend fun deleteGlycemia(glycemia: Glycemia) @Query("SELECT * FROM glycemia WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC") fun getGlycemiaForDay(startOfDay: Long, endOfDay: Long): Flow> @Query("SELECT * FROM glycemia WHERE date >= :start AND date <= :end ORDER BY date ASC") suspend fun getGlycemiaInRangeSync(start: Long, end: Long): List - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertSports(sports: List) + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSports(sports: List) + @Query("SELECT * FROM sports ORDER BY date DESC") fun getAllSports(): Flow> @Query("SELECT * FROM sports WHERE date >= :startOfDay AND date < :endOfDay ORDER BY date DESC") fun getSportsForDay(startOfDay: Long, endOfDay: Long): Flow> diff --git a/app/src/main/java/com/example/scanwich/FirebaseUtils.kt b/app/src/main/java/com/example/scanwich/FirebaseUtils.kt index 91b76dd..c4a864b 100644 --- a/app/src/main/java/com/example/scanwich/FirebaseUtils.kt +++ b/app/src/main/java/com/example/scanwich/FirebaseUtils.kt @@ -4,14 +4,13 @@ import android.util.Log import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.SetOptions +import kotlinx.coroutines.tasks.await object FirebaseUtils { private fun getDb(): FirebaseFirestore { return try { - // Tente d'utiliser la base de données nommée "scan-wich" FirebaseFirestore.getInstance("scan-wich") } catch (e: Exception) { - // Repli sur la base par défaut si "scan-wich" n'est pas configurée comme base secondaire FirebaseFirestore.getInstance() } } @@ -48,4 +47,34 @@ object FirebaseUtils { .addOnFailureListener { e -> Log.e("Firestore", "Error sport: ${e.message}") } } } + + // NOUVELLE FONCTION : Rapatrie toutes les données depuis le Cloud + suspend fun fetchAllDataFromFirestore(dao: AppDao) { + val user = FirebaseAuth.getInstance().currentUser ?: return + val db = getDb() + val userDoc = db.collection("users").document(user.uid) + + try { + // 1. Récupérer les repas + val mealsSnapshot = userDoc.collection("meals").get().await() + val meals = mealsSnapshot.toObjects(Meal::class.java) + meals.forEach { dao.insertMeal(it) } + Log.d("FirestoreSync", "${meals.size} repas récupérés") + + // 2. Récupérer la glycémie + val glycemiaSnapshot = userDoc.collection("glycemia").get().await() + val glycemia = glycemiaSnapshot.toObjects(Glycemia::class.java) + glycemia.forEach { dao.insertGlycemia(it) } + Log.d("FirestoreSync", "${glycemia.size} glycémies récupérées") + + // 3. Récupérer le sport + val sportsSnapshot = userDoc.collection("sports").get().await() + val sports = sportsSnapshot.toObjects(SportActivity::class.java) + dao.insertSports(sports) + Log.d("FirestoreSync", "${sports.size} activités sportives récupérées") + + } catch (e: Exception) { + Log.e("FirestoreSync", "Erreur lors du rapatriement des données: ${e.message}") + } + } } diff --git a/app/src/main/java/com/example/scanwich/MainActivity.kt b/app/src/main/java/com/example/scanwich/MainActivity.kt index ae897e8..111b606 100644 --- a/app/src/main/java/com/example/scanwich/MainActivity.kt +++ b/app/src/main/java/com/example/scanwich/MainActivity.kt @@ -12,6 +12,7 @@ import com.google.firebase.Firebase import com.google.firebase.appcheck.appCheck import com.google.firebase.appcheck.debug.DebugAppCheckProviderFactory import com.google.firebase.appcheck.playintegrity.PlayIntegrityAppCheckProviderFactory +import com.google.firebase.functions.functions import com.google.firebase.initialize import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -58,20 +59,34 @@ class MainActivity : ComponentActivity() { val code = data.getQueryParameter("code") if (code != null) { val prefs = ApiClient.getEncryptedPrefs(this) - val clientId = prefs.getString("strava_client_id", "") ?: "" - val clientSecret = prefs.getString("strava_client_secret", "") ?: "" - if (clientId.isNotEmpty() && clientSecret.isNotEmpty()) { - CoroutineScope(Dispatchers.IO).launch { - try { - val response = ApiClient.stravaApi.exchangeToken(clientId, clientSecret, code) - prefs.edit { - putString("strava_token", response.accessToken) - putString("strava_refresh_token", response.refreshToken) - putLong("strava_expires_at", response.expiresAt) + val functions = Firebase.functions + + val requestData = hashMapOf( + "code" to code + ) + + functions.getHttpsCallable("exchangeStravaToken") + .call(requestData) + .addOnSuccessListener { result -> + val res = result.data as? Map<*, *> + if (res != null) { + val accessToken = res["access_token"] as? String + val refreshToken = res["refresh_token"] as? String + val expiresAt = (res["expires_at"] as? Number)?.toLong() ?: 0L + + if (accessToken != null && refreshToken != null) { + prefs.edit { + putString("strava_token", accessToken) + putString("strava_refresh_token", refreshToken) + putLong("strava_expires_at", expiresAt) + } + Log.d("StravaAuth", "Token exchange successful") } - } catch (e: Exception) { Log.e("StravaAuth", "Exchange failed: ${e.message}") } + } + } + .addOnFailureListener { e -> + Log.e("StravaAuth", "Cloud Function exchange failed: ${e.message}") } - } } } } diff --git a/app/src/main/java/com/example/scanwich/Networking.kt b/app/src/main/java/com/example/scanwich/Networking.kt index ee64fc3..a14e6b6 100644 --- a/app/src/main/java/com/example/scanwich/Networking.kt +++ b/app/src/main/java/com/example/scanwich/Networking.kt @@ -1,10 +1,16 @@ package com.example.scanwich import android.content.Context +import android.content.Intent import android.content.SharedPreferences +import android.net.Uri +import android.util.Log +import android.widget.Toast import androidx.core.content.edit import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import com.google.firebase.Firebase +import com.google.firebase.functions.functions import com.google.gson.annotations.SerializedName import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -13,6 +19,7 @@ import retrofit2.http.* import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.TimeUnit +import kotlinx.coroutines.tasks.await // --- OPEN FOOD FACTS API --- data class OffProductResponse(val status: Int, val product: OffProduct?) @@ -64,12 +71,15 @@ object ApiClient { .create(StravaApi::class.java) val offApi: OffApi = Retrofit.Builder() - .baseUrl("https://world.openfoodfacts.org/api/v2/") + // On force l'utilisation de l'API française pour les codes-barres + .baseUrl("https://fr.openfoodfacts.org/api/v2/") .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() .create(OffApi::class.java) + private const val STRAVA_CLIENT_ID = "203805" + fun getEncryptedPrefs(context: Context): SharedPreferences { val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() return EncryptedSharedPreferences.create( @@ -84,19 +94,38 @@ object ApiClient { suspend fun getValidStravaToken(prefs: SharedPreferences): String? { val stravaToken = prefs.getString("strava_token", null) ?: return null val expiresAt = prefs.getLong("strava_expires_at", 0) - if (System.currentTimeMillis() / 1000 >= expiresAt) { + + if (System.currentTimeMillis() / 1000 >= (expiresAt - 300)) { val refreshToken = prefs.getString("strava_refresh_token", null) ?: return null - val clientId = prefs.getString("strava_client_id", "") ?: "" - val clientSecret = prefs.getString("strava_client_secret", "") ?: "" + return try { - val res = stravaApi.refreshToken(clientId, clientSecret, refreshToken) - prefs.edit { - putString("strava_token", res.accessToken) - putString("strava_refresh_token", res.refreshToken) - putLong("strava_expires_at", res.expiresAt) + val functions = Firebase.functions + val data = hashMapOf("refreshToken" to refreshToken) + + val result = functions.getHttpsCallable("refreshStravaToken") + .call(data) + .await() + + val res = result.data as? Map<*, *> + if (res != null) { + val newAccessToken = res["access_token"] as? String + val newRefreshToken = res["refresh_token"] as? String + val newExpiresAt = (res["expires_at"] as? Number)?.toLong() ?: 0L + + if (newAccessToken != null && newRefreshToken != null) { + prefs.edit { + putString("strava_token", newAccessToken) + putString("strava_refresh_token", newRefreshToken) + putLong("strava_expires_at", newExpiresAt) + } + return newAccessToken + } } - res.accessToken - } catch (e: Exception) { null } + null + } catch (e: Exception) { + Log.e("StravaAuth", "Refresh failed: ${e.message}") + null + } } return stravaToken } @@ -125,4 +154,18 @@ object ApiClient { val durationHours = activity.movingTime / 3600.0 return met * weightKg * durationHours } + + fun launchStravaAuth(context: Context) { + val intentUri = Uri.parse("https://www.strava.com/oauth/mobile/authorize") + .buildUpon() + .appendQueryParameter("client_id", STRAVA_CLIENT_ID) + .appendQueryParameter("redirect_uri", "coloricam://localhost") + .appendQueryParameter("response_type", "code") + .appendQueryParameter("approval_prompt", "auto") + .appendQueryParameter("scope", "activity:read_all") + .build() + + val intent = Intent(Intent.ACTION_VIEW, intentUri) + context.startActivity(intent) + } } diff --git a/app/src/main/java/com/example/scanwich/SettingsScreen.kt b/app/src/main/java/com/example/scanwich/SettingsScreen.kt index 323ab59..6ba87ba 100644 --- a/app/src/main/java/com/example/scanwich/SettingsScreen.kt +++ b/app/src/main/java/com/example/scanwich/SettingsScreen.kt @@ -1,8 +1,6 @@ package com.example.scanwich -import android.content.Intent import android.content.SharedPreferences -import android.net.Uri import android.widget.Toast import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -16,16 +14,27 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.content.edit -import androidx.core.net.toUri @Composable fun SettingsScreen(prefs: SharedPreferences, onLogout: () -> Unit, onProfileUpdated: () -> Unit) { var isEditing by remember { mutableStateOf(false) } val context = LocalContext.current - var stravaClientId by remember { mutableStateOf(prefs.getString("strava_client_id", "") ?: "") } - var stravaClientSecret by remember { mutableStateOf(prefs.getString("strava_client_secret", "") ?: "") } - val isStravaConnected = prefs.contains("strava_token") + // État réactif pour la connexion Strava + var isStravaConnected by remember { mutableStateOf(prefs.contains("strava_token")) } + + // Écouteur pour mettre à jour l'état si le token change + DisposableEffect(prefs) { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { p, key -> + if (key == "strava_token") { + isStravaConnected = p.contains("strava_token") + } + } + prefs.registerOnSharedPreferenceChangeListener(listener) + onDispose { + prefs.unregisterOnSharedPreferenceChangeListener(listener) + } + } if (isEditing) { SetupScreen(prefs) { @@ -48,51 +57,25 @@ fun SettingsScreen(prefs: SharedPreferences, onLogout: () -> Unit, onProfileUpda Button(onClick = { isEditing = true }, modifier = Modifier.fillMaxWidth()) { Icon(Icons.Default.Edit, null); Text(" Modifier le profil") } Spacer(Modifier.height(32.dp)) - Text("Configuration Strava", style = MaterialTheme.typography.titleMedium) + Text("Intégrations", style = MaterialTheme.typography.titleMedium) Spacer(Modifier.height(8.dp)) - OutlinedTextField( - value = stravaClientId, - onValueChange = { stravaClientId = it; prefs.edit { putString("strava_client_id", it) } }, - label = { Text("Strava Client ID") }, - modifier = Modifier.fillMaxWidth() - ) - OutlinedTextField( - value = stravaClientSecret, - onValueChange = { stravaClientSecret = it; prefs.edit { putString("strava_client_secret", it) } }, - label = { Text("Strava Client Secret") }, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(Modifier.height(8.dp)) - - if (!isStravaConnected) { - Button( - onClick = { - if (stravaClientId.isBlank() || stravaClientSecret.isBlank()) { - Toast.makeText(context, "Entrez votre ID et Secret Client", Toast.LENGTH_SHORT).show() - } else { - val intent = Intent(Intent.ACTION_VIEW, "https://www.strava.com/oauth/mobile/authorize?client_id=$stravaClientId&redirect_uri=coloricam://localhost&response_type=code&approval_prompt=auto&scope=read,activity:read_all".toUri()) - context.startActivity(intent) - } - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Connecter Strava") - } - } else { + if (isStravaConnected) { OutlinedButton( onClick = { prefs.edit { remove("strava_token") remove("strava_refresh_token") } + // L'écouteur mettra `isStravaConnected` à jour automatiquement Toast.makeText(context, "Strava déconnecté", Toast.LENGTH_SHORT).show() }, modifier = Modifier.fillMaxWidth() ) { Text("Déconnecter Strava (Connecté)") } + } else { + Text("Aucune intégration active. Allez dans l'onglet 'Sport' pour connecter Strava.", style = MaterialTheme.typography.bodyMedium) } Spacer(Modifier.height(32.dp)) diff --git a/app/src/main/java/com/example/scanwich/SportScreen.kt b/app/src/main/java/com/example/scanwich/SportScreen.kt index c278944..26fd28e 100644 --- a/app/src/main/java/com/example/scanwich/SportScreen.kt +++ b/app/src/main/java/com/example/scanwich/SportScreen.kt @@ -1,6 +1,7 @@ package com.example.scanwich import android.content.Context +import android.content.SharedPreferences import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.* @@ -8,6 +9,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Sync import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -23,21 +25,51 @@ import java.text.SimpleDateFormat import java.util.* @Composable -fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) { +fun SportScreen(dao: AppDao, prefs: SharedPreferences) { val sports by dao.getAllSports().collectAsState(initial = emptyList()) val coroutineScope = rememberCoroutineScope() val context = LocalContext.current + + // État réactif pour la connexion Strava + var isConnectedToStrava by remember { + mutableStateOf(prefs.getString("strava_token", null) != null) + } + + // Écouteur de changements pour les SharedPreferences + DisposableEffect(prefs) { + val listener = SharedPreferences.OnSharedPreferenceChangeListener { p, key -> + if (key == "strava_token") { + isConnectedToStrava = p.getString("strava_token", null) != null + } + } + prefs.registerOnSharedPreferenceChangeListener(listener) + onDispose { + prefs.unregisterOnSharedPreferenceChangeListener(listener) + } + } Column(modifier = Modifier.fillMaxSize().padding(16.dp)) { Text("Activités Sportives", style = MaterialTheme.typography.headlineMedium) - Button( - onClick = { syncStravaActivities(dao, prefs, coroutineScope, context) }, - modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) - ) { - Icon(Icons.Default.Refresh, null) - Spacer(Modifier.width(8.dp)) - Text("Synchroniser Strava") + if (!isConnectedToStrava) { + Button( + onClick = { ApiClient.launchStravaAuth(context) }, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFFFC6100)) // Strava orange + ) { + Icon(Icons.Default.Sync, null) + Spacer(Modifier.width(8.dp)) + Text("Se connecter à Strava") + } + } else { + Button( + onClick = { syncStravaActivities(dao, prefs, coroutineScope, context) }, + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp) + ) { + Icon(Icons.Default.Refresh, null) + Spacer(Modifier.width(8.dp)) + Text("Synchroniser les activités") + } } Spacer(Modifier.height(8.dp)) @@ -50,7 +82,8 @@ fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) { Text(activity.name, fontWeight = FontWeight.Bold, modifier = Modifier.weight(1f)) Text(SimpleDateFormat("dd/MM HH:mm", Locale.getDefault()).format(Date(activity.date)), style = MaterialTheme.typography.bodySmall) } - Text("${activity.type} - ${(activity.distance / 1000).format(2)} km") + val distanceInKm = activity.distance / 1000 + Text("${activity.type} - ${String.format(Locale.getDefault(), "%.2f", distanceInKm)} km") Text("${activity.calories?.toInt() ?: 0} kcal brûlées", color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold) } } @@ -59,11 +92,11 @@ fun SportScreen(dao: AppDao, prefs: android.content.SharedPreferences) { } } -private fun syncStravaActivities(dao: AppDao, prefs: android.content.SharedPreferences, scope: CoroutineScope, context: Context) { +private fun syncStravaActivities(dao: AppDao, prefs: SharedPreferences, scope: CoroutineScope, context: Context) { scope.launch { val token = ApiClient.getValidStravaToken(prefs) if (token == null) { - Toast.makeText(context, "Veuillez connecter Strava dans les paramètres", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Erreur de connexion Strava", Toast.LENGTH_LONG).show() return@launch } try { @@ -83,7 +116,7 @@ private fun syncStravaActivities(dao: AppDao, prefs: android.content.SharedPrefe ).toFloat(), date = ApiClient.parseStravaDate(it.startDate) ) - syncSportToFirestore(activity) // Firestore Sync + syncSportToFirestore(activity) activity } dao.insertSports(sportActivities) diff --git a/release-notes.txt b/release-notes.txt index 811eaa8..9a73e3d 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,48 +2,49 @@ **Changements majeurs de la version actuelle :** -🎨 **Améliorations de l'Interface Utilisateur :** -- **Correction du Thème :** Résolution d'un problème de contraste sur l'écran de configuration du profil qui rendait le texte illisible (texte jaune sur fond blanc). L'écran respecte désormais correctement le thème de l'application. +🇫🇷 **Expérience 100% en Français :** +- **IA Francophone :** Les analyses de repas (caméra et manuel) sont désormais exclusivement en français grâce à des instructions renforcées côté serveur. +- **Scanner Localisé :** Utilisation forcée de la base de données française d'Open Food Facts pour le scan de codes-barres, garantissant des noms de produits familiers. -🛡️ **Refonte de l'Architecture de Sécurité et de Données :** -- **Gestion des accès centralisée :** L'ancien système d'utilisateurs autorisés codé en dur dans l'application a été supprimé. L'accès est désormais contrôlé de manière sécurisée et dynamique via une liste d'autorisation sur le serveur Firebase Firestore. Cela ouvre la voie à la gestion d'abonnements. -- **Profils Utilisateurs dans le Cloud :** Les données de profil (poids, objectifs, etc.) sont maintenant synchronisées avec le compte Firebase de l'utilisateur, permettant une expérience cohérente sur plusieurs appareils. -- **Correctif Critique de Connexion :** Résolution d'un bug majeur qui empêchait l'application de se connecter correctement à la base de données Firestore, causant des accès refusés inattendus pour les utilisateurs légitimes. L'application est désormais compatible avec les bases de données Firestore nommées. +🤖 **Analyse IA plus Robuste :** +- **Correctif d'analyse :** Résolution du bug "Erreur IA" lié au formatage des réponses du modèle. +- **Fiabilité accrue :** Nettoyage automatique des données nutritionnelles côté serveur (Cloud Functions). + +🚀 **Connexion Strava 100% Automatique :** +- **Simplicité totale :** Connexion en un seul clic sans saisie d'identifiants techniques. +- **Sécurité Maximale :** Utilisation de Google Cloud Secret Manager pour la protection des clés API Strava. + +🎨 **Améliorations UI/UX :** +- **Interface épurée :** Suppression des réglages superflus. +- **Correction du Thème :** Lisibilité parfaite sur l'écran de profil. + +🛡️ **Architecture Cloud :** +- **Gestion des accès :** Contrôle dynamique des utilisateurs autorisés via Firestore. +- **Profils Cloud :** Synchronisation automatique de vos données personnelles. --- **Mises à jour précédentes :** 🛠️ **Correctifs et Améliorations Strava :** -- Résolution d'un problème de compilation bloquant sur l'écran des sports. -- Intégration d'un nouvel algorithme d'estimation des calories basé sur les MET (Metabolic Equivalent of Task) pour une précision accrue. -- Amélioration de la fiabilité du parsing des dates d'activités Strava. +- Résolution de bugs de compilation et amélioration du parsing des dates. +- Nouvel algorithme d'estimation des calories basé sur les MET. 🛡️ **Sécurité renforcée :** -- Intégration de Firebase App Check (Play Integrity) pour protéger l'API contre les accès non autorisés. -- Migration de la clé API vers Google Cloud Secret Manager, supprimant toute information sensible du code source. +- Intégration de Firebase App Check (Play Integrity). +- Migration des clés vers Secret Manager. ⚡ **Analyse Ultra-Rapide :** -- Nouveau moteur de compression d'image intelligent (réduction de ~2.2 Mo à 150 Ko par scan), accélérant drastiquement l'analyse IA. - -🤖 **IA Sécurisée :** -- Migration de la logique d'analyse (prompts) vers des Cloud Functions pour garantir des résultats plus fiables et protégés. +- Nouveau moteur de compression d'image intelligent. 📄 **Export PDF Professionnel :** -- Nouvelle fonctionnalité d'exportation de l'historique. Générez un rapport PDF complet incluant vos repas, activités sportives et suivis de glycémie. +- Exportation de l'historique complet (repas, sport, glycémie). -🩸 **Suivi Diabétique complet :** -- Possibilité d'enregistrer et de visualiser sa glycémie avant/après chaque catégorie de repas directement dans l'historique. - -🚴 **Synchronisation Strava :** -- Connexion directe à Strava pour importer vos activités et calculer précisément les calories brûlées. +🩸 **Suivi Diabétique :** +- Visualisation de la glycémie directement dans l'historique. ⭐ **Gestion des Favoris :** -- Refonte de l'interface des favoris avec une nouvelle fenêtre modale pour un ajout rapide de vos repas récurrents. +- Nouvelle interface d'ajout rapide. -🔍 **Scanner de Code-barres :** -- Intégration d'Open Food Facts pour identifier instantanément les produits industriels via leur code-barres. - -🔧 **Stabilité et Modernisation :** -- Optimisation pour Android 36. -- Correction de bugs majeurs sur le stockage sécurisé des préférences (MasterKey) et la synchronisation Firebase (collectAsState). +🔧 **Stabilité :** +- Optimisation pour Android 36 et corrections diverses.