Skip to content
Closed
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
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
Application Android pour **suivre ses séances de salle** et **tracker ses calories et ses macros**.
Pas de compte, pas de serveur, pas de publicité : tout est stocké sur le téléphone.

Une seule exception au « tout local » : quand tu scannes un code-barres, ce code-barres est envoyé à
[Open Food Facts](https://world.openfoodfacts.org) pour récupérer les valeurs nutritionnelles du
produit. Rien d'autre ne sort du téléphone — ni séances, ni repas, ni poids — et le produit reçu est
mis en cache localement pour rester consultable hors ligne.
Deux exceptions au « tout local » :

- **Open Food Facts** : quand tu scannes un code-barres, ce code-barres est envoyé à
[Open Food Facts](https://world.openfoodfacts.org) pour récupérer les valeurs nutritionnelles du
produit. Le produit reçu est mis en cache localement pour rester consultable hors ligne.
- **Health Connect**, uniquement si tu l'actives : tes séances et ton journal alimentaire sont
partagés avec le magasin de santé d'Android, qui les garde sur l'appareil et ne les transmet
qu'aux applications que tu autorises explicitement.

Rien d'autre ne sort du téléphone, et les deux restent facultatifs.

## Fonctionnalités

Expand Down Expand Up @@ -49,6 +55,7 @@ corde à sauter 11,0 · rameur 7,0…). C'est une estimation, pas une mesure mé
| Catalogue | JSON embarqué dans les assets, chargé en mémoire au lancement |
| Scan | CameraX + ML Kit (barcode-scanning groupé, fonctionne sans Play Services) |
| Nutrition | API Open Food Facts v2 en lecture, `HttpURLConnection`, cache Room |
| Santé | Health Connect (écriture séances, calories et nutrition ; lecture du poids) |
| Build | Gradle 8.11 + AGP 8.7, `minSdk 26`, `targetSdk 35` |

Architecture : `data` (entités Room, DAO, catalogue, repository) → `ui` (ViewModel exposant des
Expand Down Expand Up @@ -87,6 +94,7 @@ app/src/main/
│ ├── Model.kt # Session, ExerciseSet, Meal, bilans
│ ├── Nutrition.kt # FoodProduct, portions, macros et objectifs
│ ├── OpenFoodFacts.kt # client de lecture de l'API + parsing
│ ├── HealthConnect.kt # export des séances et repas, lecture du poids
│ ├── Exercise.kt # exercice du catalogue + libellés FR
│ ├── ExerciseCatalog.kt # chargement, recherche, filtres
│ ├── Activities.kt # activités cardio (MET) et calcul des calories
Expand Down Expand Up @@ -133,4 +141,4 @@ hors ligne.
- Graphiques de progression par exercice et courbe de poids.
- Réutiliser un produit déjà scanné sans rescanner (liste des aliments récents).
- Typage des séries (échauffement, échec, drop-set) pour un volume plus juste.
- Export / import CSV et intégration Health Connect.
- Export / import CSV.
7 changes: 6 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {

android {
namespace = "com.kaz229.fittrack"
compileSdk = 35
compileSdk = 36

defaultConfig {
applicationId = "com.kaz229.fittrack"
Expand Down Expand Up @@ -64,11 +64,16 @@ dependencies {

implementation(libs.androidx.datastore.preferences)

implementation(libs.androidx.health.connect.client)

implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.mlkit.barcode.scanning)
// Fournit ListenableFuture.await() et, au passage, le vrai artefact guava :
// sans lui, Health Connect tire la version « vide » qui masque ListenableFuture.
implementation(libs.androidx.concurrent.futures.ktx)

testImplementation(libs.junit)
// org.json n'est qu'un stub dans android.jar : la vraie implémentation permet de
Expand Down
27 changes: 27 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,21 @@
android:name="android.hardware.camera.any"
android:required="false" />

<!--
Health Connect : échange des données d'entraînement et de nutrition avec les
autres applications santé du téléphone. Tout est facultatif — l'app reste
pleinement utilisable si l'utilisateur refuse, ou si Health Connect est absent.
-->
<uses-permission android:name="android.permission.health.WRITE_EXERCISE" />
<uses-permission android:name="android.permission.health.WRITE_ACTIVE_CALORIES_BURNED" />
<uses-permission android:name="android.permission.health.WRITE_NUTRITION" />
<uses-permission android:name="android.permission.health.READ_WEIGHT" />

<!-- Nécessaire pour détecter si Health Connect est installé (Android 11+). -->
<queries>
<package android:name="com.google.android.apps.healthdata" />
</queries>

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand All @@ -28,6 +43,18 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<!--
Health Connect exige que l'app explique l'usage qu'elle fait des données
de santé, et ouvre cet écran depuis ses propres réglages.
-->
<intent-filter>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
</intent-filter>
</activity>
</application>

Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/kaz229/fittrack/data/Daos.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ interface SessionDao {
@Query("SELECT * FROM sessions ORDER BY epochDay DESC, id DESC")
fun observeAll(): Flow<List<Session>>

/** Instantané des séances d'une période, pour l'export vers Health Connect. */
@Query("SELECT * FROM sessions WHERE epochDay >= :fromDay ORDER BY epochDay, id")
suspend fun since(fromDay: Long): List<Session>

@Query("SELECT COALESCE(SUM(caloriesBurned), 0) FROM sessions WHERE epochDay = :epochDay")
fun observeCaloriesForDay(epochDay: Long): Flow<Int>

Expand Down Expand Up @@ -82,6 +86,10 @@ interface MealDao {
@Query("SELECT * FROM meals WHERE epochDay = :epochDay ORDER BY id DESC")
fun observeForDay(epochDay: Long): Flow<List<Meal>>

/** Instantané des repas d'une période, pour l'export vers Health Connect. */
@Query("SELECT * FROM meals WHERE epochDay >= :fromDay ORDER BY epochDay, id")
suspend fun since(fromDay: Long): List<Meal>

@Query("SELECT COALESCE(SUM(calories), 0) FROM meals WHERE epochDay = :epochDay")
fun observeCaloriesForDay(epochDay: Long): Flow<Int>

Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/com/kaz229/fittrack/data/FitTrackRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class FitTrackRepository(
private val userPreferences: UserPreferences,
val exercises: ExerciseCatalog,
private val openFoodFacts: OpenFoodFactsClient = OpenFoodFactsClient(),
private val healthConnect: HealthConnectManager? = null,
) {

val profile: Flow<UserProfile> = userPreferences.profile
Expand Down Expand Up @@ -227,6 +228,36 @@ class FitTrackRepository(
}
}

// --- Health Connect ------------------------------------------------------

val lastHealthSync: Flow<Long?> = userPreferences.lastHealthSync

fun healthConnectAvailability(): HealthConnectAvailability =
healthConnect?.availability() ?: HealthConnectAvailability.UNSUPPORTED

suspend fun healthConnectPermissionsGranted(): Boolean =
healthConnect?.hasAllPermissions() ?: false

/**
* Exporte les séances et repas du dernier mois, et relit le poids le plus récent.
* Le poids n'est pas appliqué d'office : il est renvoyé pour que l'utilisateur
* décide de remplacer, ou non, celui de son profil.
*/
suspend fun syncHealthConnect(today: LocalDate = LocalDate.now()): SyncResult {
val manager = healthConnect ?: return SyncResult.Unavailable
val fromDay = today.minusDays((SYNC_DAYS - 1).toLong()).toEpochDay()
val result = manager.sync(
sessions = sessionDao.since(fromDay),
meals = mealDao.since(fromDay),
today = today,
days = SYNC_DAYS,
)
if (result is SyncResult.Success) {
userPreferences.setLastHealthSync(System.currentTimeMillis() / 1000)
}
return result
}

// --- Profil --------------------------------------------------------------

suspend fun setWeight(weightKg: Double) = userPreferences.setWeight(weightKg)
Expand All @@ -236,6 +267,9 @@ class FitTrackRepository(
suspend fun setMacroGoals(goals: MacroGoals) = userPreferences.setMacroGoals(goals)

companion object {
/** Fenêtre d'export : réécrire tout l'historique à chaque fois serait inutile. */
const val SYNC_DAYS = 30

@Volatile
private var instance: FitTrackRepository? = null

Expand All @@ -249,6 +283,7 @@ class FitTrackRepository(
foodProductDao = db.foodProductDao(),
userPreferences = UserPreferences(context.applicationContext),
exercises = ExerciseCatalog.get(context),
healthConnect = HealthConnectManager(context.applicationContext),
).also { instance = it }
}
}
Expand Down
Loading
Loading