Merge pull request 'changes' (#8) from dev into master

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-03-09 20:57:17 -04:00
11 changed files with 246 additions and 51 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-02-20T21:41:35.128842100Z"> <DropdownSelection timestamp="2026-03-10T00:31:28.813056900Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\marca\.android\avd\Medium_Phone.avd" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=58051FDCG0038S" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

2
.idea/vcs.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" /> <mapping directory="" vcs="Git" />
</component> </component>
</project> </project>

View File

@@ -18,6 +18,7 @@ android {
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
// Incrémentation automatique du versionCode basé sur le temps
versionCode = (System.currentTimeMillis() / 60000).toInt() versionCode = (System.currentTimeMillis() / 60000).toInt()
versionName = "1.0." + (System.currentTimeMillis() / 3600000).toString().takeLast(3) versionName = "1.0." + (System.currentTimeMillis() / 3600000).toString().takeLast(3)
@@ -25,6 +26,7 @@ android {
} }
signingConfigs { signingConfigs {
// Chargement des propriétés depuis local.properties
val keystoreProperties = Properties() val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("local.properties") val keystorePropertiesFile = rootProject.file("local.properties")
if (keystorePropertiesFile.exists()) { if (keystorePropertiesFile.exists()) {
@@ -33,13 +35,14 @@ android {
getByName("debug") { getByName("debug") {
storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore") storeFile = file(System.getProperty("user.home") + "/.android/debug.keystore")
storePassword = "android" getByName("debug").storePassword = "android"
keyAlias = "androiddebugkey" getByName("debug").keyAlias = "androiddebugkey"
keyPassword = "android" getByName("debug").keyPassword = "android"
} }
create("release") { create("release") {
storeFile = file("C:\\Users\\mac\\keys\\keys") // Utilisation d'un chemin relatif au dossier Utilisateur pour fonctionner sur tous les PC
storeFile = file("${System.getProperty("user.home")}/keys/keys")
storePassword = keystoreProperties.getProperty("RELEASE_STORE_PASSWORD") storePassword = keystoreProperties.getProperty("RELEASE_STORE_PASSWORD")
keyAlias = keystoreProperties.getProperty("RELEASE_KEY_ALIAS") ?: "key0" keyAlias = keystoreProperties.getProperty("RELEASE_KEY_ALIAS") ?: "key0"
keyPassword = keystoreProperties.getProperty("RELEASE_KEY_PASSWORD") keyPassword = keystoreProperties.getProperty("RELEASE_KEY_PASSWORD")
@@ -51,7 +54,7 @@ android {
buildTypes { buildTypes {
release { release {
isMinifyEnabled = true isMinifyEnabled = true // Activer l'offuscation
signingConfig = signingConfigs.getByName("release") signingConfig = signingConfigs.getByName("release")
proguardFiles( proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), getDefaultProguardFile("proguard-android-optimize.txt"),
@@ -109,7 +112,9 @@ dependencies {
implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.material.icons.extended)
// SDK Firebase App Distribution COMPLET (API + Implémentation)
implementation(libs.firebase.appdistribution) implementation(libs.firebase.appdistribution)
implementation(libs.google.generativeai) implementation(libs.google.generativeai)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
@@ -128,14 +133,22 @@ dependencies {
implementation(libs.firebase.auth) implementation(libs.firebase.auth)
implementation(libs.firebase.firestore) implementation(libs.firebase.firestore)
implementation(libs.firebase.appcheck.playintegrity) implementation(libs.firebase.appcheck.playintegrity)
// On met le debug provider en implementation pour qu'il soit disponible à la compilation en Release
// (le code MainActivity utilise un check BuildConfig.DEBUG pour ne pas l'utiliser en prod)
implementation(libs.firebase.appcheck.debug)
// Barcode Scanning & Camera
implementation(libs.mlkit.barcode.scanning) implementation(libs.mlkit.barcode.scanning)
implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view) implementation(libs.androidx.camera.view)
// PDF generation
implementation(libs.itext7.core) implementation(libs.itext7.core)
// Security
implementation(libs.androidx.security.crypto) implementation(libs.androidx.security.crypto)
testImplementation(libs.junit) testImplementation(libs.junit)
@@ -145,5 +158,4 @@ dependencies {
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.firebase.appcheck.debug)
} }

View File

@@ -29,6 +29,14 @@
"certificate_hash": "ebcc060f9a1fdeb1186536d3828574b42cefa03c" "certificate_hash": "ebcc060f9a1fdeb1186536d3828574b42cefa03c"
} }
}, },
{
"client_id": "652626507041-i928hstoseh72dta5d0lokm9c55tma2p.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.example.scanwich",
"certificate_hash": "6f363d957ca44b3ca607c29f58f575d0ae71571d"
}
},
{ {
"client_id": "652626507041-5n42q37adh1guuv9gibfcf5uvekgunbe.apps.googleusercontent.com", "client_id": "652626507041-5n42q37adh1guuv9gibfcf5uvekgunbe.apps.googleusercontent.com",
"client_type": 3 "client_type": 3

View File

@@ -4,6 +4,9 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -14,6 +17,17 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Coloricam"> android:theme="@style/Theme.Coloricam">
<receiver
android:name=".MealReminderReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
</intent-filter>
</receiver>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.scanwich.ui.theme.ReadableAmber import com.example.scanwich.ui.theme.ReadableAmber
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -329,13 +330,38 @@ fun HistoryScreen(dao: AppDao, prefs: android.content.SharedPreferences) {
val totalIn = meals.sumOf { it.totalCalories } val totalIn = meals.sumOf { it.totalCalories }
val totalOut = sports.sumOf { it.calories?.toInt() ?: 0 } val totalOut = sports.sumOf { it.calories?.toInt() ?: 0 }
val netTotal = totalIn - totalOut
val totalCarbs = meals.sumOf { it.carbs } val totalCarbs = meals.sumOf { it.carbs }
val totalProt = meals.sumOf { it.protein } val totalProt = meals.sumOf { it.protein }
val totalFat = meals.sumOf { it.fat } val totalFat = meals.sumOf { it.fat }
Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { Card(modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) {
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text("Objectifs Quotidiens", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) Text("Résumé Calorique", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Mangé", style = MaterialTheme.typography.labelMedium, color = Color.Gray)
Text("$totalIn", style = MaterialTheme.typography.titleLarge, color = calorieColor, fontWeight = FontWeight.Bold)
}
Text("-", style = MaterialTheme.typography.headlineMedium, color = Color.Gray)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Dépensé", style = MaterialTheme.typography.labelMedium, color = Color.Gray)
Text("$totalOut", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold)
}
Text("=", style = MaterialTheme.typography.headlineMedium, color = Color.Gray)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Total Net", style = MaterialTheme.typography.labelMedium, color = Color.Gray)
Text("$netTotal", style = MaterialTheme.typography.titleLarge, color = if(netTotal <= tCal) calorieColor else Color.Red, fontWeight = FontWeight.ExtraBold)
}
}
Text("kcal", style = MaterialTheme.typography.labelSmall, modifier = Modifier.align(Alignment.End), color = Color.Gray)
Spacer(Modifier.height(12.dp))
HorizontalDivider()
Spacer(Modifier.height(12.dp))
Text("Objectifs Quotidiens", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
DailyGoalChart("Calories", totalIn, tCal, calorieColor) DailyGoalChart("Calories", totalIn, tCal, calorieColor)
@@ -343,13 +369,6 @@ fun HistoryScreen(dao: AppDao, prefs: android.content.SharedPreferences) {
DailyGoalChart("Protéines", totalProt, tProt, proteinColor) DailyGoalChart("Protéines", totalProt, tProt, proteinColor)
DailyGoalChart("Lipides", totalFat, tFat, fatColor) DailyGoalChart("Lipides", totalFat, tFat, fatColor)
} }
Spacer(Modifier.height(12.dp))
HorizontalDivider()
Spacer(Modifier.height(8.dp))
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
Text("Sport (Brûlées):")
Text("$totalOut kcal", fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)
}
} }
} }

View File

@@ -1,12 +1,17 @@
package com.example.scanwich package com.example.scanwich
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import com.example.scanwich.ui.theme.ScanwichTheme import com.example.scanwich.ui.theme.ScanwichTheme
import com.google.firebase.Firebase import com.google.firebase.Firebase
import com.google.firebase.appcheck.appCheck import com.google.firebase.appcheck.appCheck
@@ -20,11 +25,26 @@ import kotlinx.coroutines.launch
import androidx.core.content.edit import androidx.core.content.edit
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
NotificationHelper.scheduleReminders(this)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
checkNotificationPermission()
NotificationHelper.scheduleReminders(this)
try { try {
Firebase.initialize(this) Firebase.initialize(this)
val appCheckFactory = if (BuildConfig.DEBUG) {
// On utilise explicitement le package name pour BuildConfig car l'import peut échouer
val appCheckFactory = if (com.example.scanwich.BuildConfig.DEBUG) {
DebugAppCheckProviderFactory.getInstance() DebugAppCheckProviderFactory.getInstance()
} else { } else {
PlayIntegrityAppCheckProviderFactory.getInstance() PlayIntegrityAppCheckProviderFactory.getInstance()
@@ -48,6 +68,16 @@ class MainActivity : ComponentActivity() {
} }
} }
private fun checkNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) !=
PackageManager.PERMISSION_GRANTED
) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleStravaCallback(intent) handleStravaCallback(intent)

View File

@@ -0,0 +1,63 @@
package com.example.scanwich
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
class MealReminderReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
NotificationHelper.scheduleReminders(context)
return
}
val mealType = intent.getStringExtra("meal_type") ?: "repas"
showNotification(context, mealType)
}
private fun showNotification(context: Context, mealType: String) {
val channelId = "meal_reminders"
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Rappels de repas",
NotificationManager.IMPORTANCE_DEFAULT
).apply {
description = "Notifications pour ne pas oublier d'entrer vos repas"
}
notificationManager.createNotificationChannel(channel)
}
val activityIntent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context, 0, activityIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info) // À remplacer par l'icône de l'app si dispo
.setContentTitle("N'oubliez pas votre $mealType !")
.setContentText("Prenez un moment pour enregistrer ce que vous avez mangé.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
val notificationId = when(mealType.lowercase()) {
"déjeuner" -> 1
"dîner" -> 2
"souper" -> 3
else -> 0
}
notificationManager.notify(notificationId, notification)
}
}

View File

@@ -0,0 +1,48 @@
package com.example.scanwich
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import java.util.*
object NotificationHelper {
fun scheduleReminders(context: Context) {
scheduleMealAlarm(context, 8, 30, "Déjeuner", 101)
scheduleMealAlarm(context, 12, 30, "Dîner", 102)
scheduleMealAlarm(context, 19, 30, "Souper", 103)
}
private fun scheduleMealAlarm(context: Context, hour: Int, minute: Int, mealType: String, requestCode: Int) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, MealReminderReceiver::class.java).apply {
putExtra("meal_type", mealType)
}
val pendingIntent = PendingIntent.getBroadcast(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val calendar = Calendar.getInstance().apply {
timeInMillis = System.currentTimeMillis()
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
// Si l'heure est déjà passée, on programme pour demain
if (before(Calendar.getInstance())) {
add(Calendar.DATE, 1)
}
}
alarmManager.setInexactRepeating(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
AlarmManager.INTERVAL_DAY,
pendingIntent
)
}
}

View File

@@ -72,7 +72,15 @@ fun analyzeImage(
bitmap?.let { getOptimizedImageBase64(it) } bitmap?.let { getOptimizedImageBase64(it) }
} }
val data = hashMapOf("imageBase64" to base64, "mealName" to textDescription) // Instruction pour que l'IA se concentre uniquement sur la nourriture (sans qualificatifs ni environnement)
val aiInstruction = "Focus seulement sur la nourriture, pas de qualificatif, pas son environnement, seulement la nourriture."
val mealDescriptionForAI = if (textDescription.isNullOrBlank()) {
aiInstruction
} else {
"$textDescription. $aiInstruction"
}
val data = hashMapOf("imageBase64" to base64, "mealName" to mealDescriptionForAI)
Firebase.functions("us-central1") Firebase.functions("us-central1")
.getHttpsCallable("analyzeMealProxy") .getHttpsCallable("analyzeMealProxy")

View File

@@ -1,50 +1,43 @@
📝 Notes de version - Scan-Wich 📝 Notes de version - Scan-Wich
**Changements majeurs de la version actuelle :** **Nouveautés de la version actuelle :**
🍲 **Focus Nourriture Pur (IA) :**
- **Analyse sélective :** L'intelligence artificielle se concentre désormais exclusivement sur la nourriture. Les qualificatifs, les descriptions de l'environnement ou les éléments de décor sont ignorés pour ne garder que l'essentiel nutritionnel.
🔔 **Rappels de Repas :**
- **Notifications intelligentes :** Ne manquez plus un enregistrement ! Des rappels automatiques ont été ajoutés pour le déjeuner (08h30), le dîner (12h30) et le souper (19h30).
- **Relance automatique :** Les notifications se réactivent automatiquement même après un redémarrage du téléphone.
📊 **Résumé Calorique Avancé :**
- **Bilan Net :** L'historique affiche maintenant le calcul "Mangé - Dépensé (Sport) = Total Net" pour une vision précise de votre équilibre journalier.
- **Alertes visuelles :** Le total net passe en rouge si vous dépassez vos objectifs quotidiens.
---
**Changements majeurs précédents :**
🇫🇷 **Expérience 100% en Français :** 🇫🇷 **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. - **IA Francophone :** Les analyses de repas (caméra et manuel) sont désormais exclusivement en français.
- **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. - **Scanner Localisé :** Utilisation de la base de données française d'Open Food Facts.
🤖 **Analyse IA plus Robuste :** 🤖 **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.
- **Fiabilité accrue :** Nettoyage automatique des données nutritionnelles côté serveur (Cloud Functions).
🚀 **Connexion Strava 100% Automatique :** 🚀 **Connexion Strava 100% Automatique :**
- **Simplicité totale :** Connexion en un seul clic sans saisie d'identifiants techniques. - **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 :** 🎨 **Améliorations UI/UX :**
- **Interface épurée :** Suppression des réglages superflus.
- **Correction du Thème :** Lisibilité parfaite sur l'écran de profil. - **Correction du Thème :** Lisibilité parfaite sur l'écran de profil.
🛡️ **Architecture Cloud :** 🛡️ **Architecture Cloud :**
- **Gestion des accès :** Contrôle dynamique des utilisateurs autorisés via Firestore.
- **Profils Cloud :** Synchronisation automatique de vos données personnelles. - **Profils Cloud :** Synchronisation automatique de vos données personnelles.
--- ---
**Mises à jour précédentes :** 🛠️ **Historique technique :**
- Export PDF Professionnel (Repas, sport, glycémie).
🛠️ **Correctifs et Améliorations Strava :** - Suivi Diabétique complet.
- Résolution de bugs de compilation et amélioration du parsing des dates. - Intégration Firebase App Check (Play Integrity).
- Nouvel algorithme d'estimation des calories basé sur les MET. - Algorithme d'estimation MET pour le sport.
- Optimisation pour Android 36.
🛡️ **Sécurité renforcée :**
- 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.
📄 **Export PDF Professionnel :**
- Exportation de l'historique complet (repas, sport, glycémie).
🩸 **Suivi Diabétique :**
- Visualisation de la glycémie directement dans l'historique.
⭐ **Gestion des Favoris :**
- Nouvelle interface d'ajout rapide.
🔧 **Stabilité :**
- Optimisation pour Android 36 et corrections diverses.