diff --git a/README.md b/README.md index ac113e4..3d0713e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ sauvegarde JSON. **100 % local, aucun compte, aucun serveur.** > à titre informatif — ce ne sont pas des mesures. Fie-toi toujours à tes analyses de > sang et aux consignes de ton endocrinologue. -- **Statut** : v1.3.4 — build Android ✅, **lint vert** ✅, **90 tests unitaires** ✅ (72 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ +- **Statut** : v1.3.5 — build Android ✅, **lint vert** ✅, **90 tests unitaires** ✅ (72 sans les données de test locales ; 3 régressions épinglées sur données réelles **non versionnées**), intégration montre = notifications ✅, **dépôt Gitea privé + releases avec APK** ✅ - **Journal des versions** : [docs/CHANGELOG.md](docs/CHANGELOG.md) - **Guide utilisateur** : [docs/GUIDE_INSTALLATION.md](docs/GUIDE_INSTALLATION.md) - **Doc de développement** (architecture, maths, décisions, bugs) : [docs/DEVELOPPEMENT.md](docs/DEVELOPPEMENT.md) @@ -85,7 +85,7 @@ Pas de Play Store : l'app est sideloadée. Détails pas-à-pas : [docs/GUIDE_INS Dépôts : **gitea.cloudyfy.fr** et **gitea.farewell.dev** (miroir) — `Siphonight/HormoneTrack` sur les deux (privé), avec -**releases taguées** (`v1.1.0` → `v1.3.4`) et **deux APK par release** (depuis v1.2.5) +**releases taguées** (`v1.1.0` → `v1.3.5`) et **deux APK par release** (depuis v1.2.5) (téléchargeables sans compiler, cf [docs/DEVELOPPEMENT.md §16.bis](docs/DEVELOPPEMENT.md)) : `-release.apk` (**recommandé**, optimisé R8, 2,4 Mo) et `-debug.apk` (20 Mo) : diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d65d79d..dc27c1a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -17,8 +17,8 @@ android { // tag : v1.3.0→v1.3.2 contenaient tous versionCode 14 / "1.3.0" // (bump jamais commité) → BuildConfig.VERSION_NAME était faux dans // les APK publiés (Paramètres + titre du dialog « Nouveautés »). - versionCode = 18 - versionName = "1.3.4" + versionCode = 19 + versionName = "1.3.5" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/hormonetrack/reminder/CalendarEvents.kt b/app/src/main/java/com/hormonetrack/reminder/CalendarEvents.kt index 175beb7..5d89096 100644 --- a/app/src/main/java/com/hormonetrack/reminder/CalendarEvents.kt +++ b/app/src/main/java/com/hormonetrack/reminder/CalendarEvents.kt @@ -64,11 +64,25 @@ object CalendarEvents { * Retourne l'id du calendrier local « HormoneTrack » (le crée s'il manque). * Nécessite WRITE_CALENDAR (l'insertion d'un calendrier local se fait avec * CALLER_IS_SYNCADAPTER=true). + * + * ⚠️ BUG fixé (v1.3.5, #50) : un URI avec CALLER_IS_SYNCADAPTER DOIT aussi + * embarquer ACCOUNT_NAME + ACCOUNT_TYPE — sinon le CalendarProvider lève + * `IllegalArgumentException: Sync adapters must specify an account and + * account type` sur la requête ou l'insertion. Vu dans les AppLog exportés + * du Pixel 9 /e/OS v1.3.4 : la permission agenda était accordée (fix + * manifest v1.3.3 ✓) et ensureCalendar plantait juste après. */ fun ensureCalendar(context: Context): Long { val calUri = CalendarContract.Calendars.CONTENT_URI .buildUpon() .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_NAME, CALENDAR_NAME + ) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ) .build() context.contentResolver.query( @@ -140,11 +154,26 @@ object CalendarEvents { return ContentUris.parseId(eventUriInserted) } - /** Supprime l'événement d'agenda du traitement (si présent). */ + /** + * Supprime l'événement d'agenda du traitement (si présent). + * + * ⚠️ #50 bis (v1.3.5) : CALLER_IS_SYNCADAPTER exige AUSSI les paramètres + * ACCOUNT_NAME/ACCOUNT_TYPE ici — sans eux le provider lève + * IllegalArgumentException (avalée par le catch), et l'événement restait + * dans le provider après « switch OFF » + save. Validé émulateur : la + * suppression échouait silencieusement avant ce fix. + */ fun deleteEvent(context: Context, eventId: Long) { val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId) .buildUpon() .appendQueryParameter(CalendarContract.CALLER_IS_SYNCADAPTER, "true") + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_NAME, CALENDAR_NAME + ) + .appendQueryParameter( + CalendarContract.Calendars.ACCOUNT_TYPE, + CalendarContract.ACCOUNT_TYPE_LOCAL + ) .build() try { context.contentResolver.delete(uri, null, null) diff --git a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt index 7da916e..e6e0efc 100644 --- a/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt +++ b/app/src/main/java/com/hormonetrack/ui/screens/TreatmentEditorScreen.kt @@ -110,6 +110,12 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { var showTimePicker by remember { mutableStateOf(false) } var loading by remember { mutableStateOf(treatmentId > 0) } var loadedCreatedAt by remember { mutableStateOf(System.currentTimeMillis()) } + // ⚠️ BUG #51 (v1.3.5) : buildTreatment() ne transmettait PAS + // calendarEventId → l'id revenait à null à CHAQUE save : l'événement + // d'agenda devenait ORPHELIN (plus jamais supprimable/désactivable par + // l'app, il restait dans le provider). L'id existant est chargé ici, + // reporté par buildTreatment(), puis recalcité par le save (upsert). + var loadedCalendarEventId by remember { mutableStateOf(null) } LaunchedEffect(treatmentId) { if (treatmentId > 0) { @@ -129,6 +135,7 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { reminderTime = LocalTime.of(tr.reminderHour ?: 12, tr.reminderMinute ?: 0) active = tr.isActive loadedCreatedAt = tr.createdAt + loadedCalendarEventId = tr.calendarEventId // Fréquence de prévision (si configurée) tr.forecastIntervalDays?.let { fi -> forecastEnabled = true @@ -172,6 +179,9 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { reminderEnabled = reminderEnabled, reminderHour = if (reminderEnabled) reminderTime.hour else null, reminderMinute = if (reminderEnabled) reminderTime.minute else null, + // ⚠️ #51 : conserver l'id de l'événement d'agenda existant (cf + // déclaration de loadedCalendarEventId) — sinon orphelin au save. + calendarEventId = loadedCalendarEventId, createdAt = loadedCreatedAt ) } @@ -535,8 +545,18 @@ fun TreatmentEditorScreen(treatmentId: Long, onDone: () -> Unit) { repo.updateTreatment(saved.copy(calendarEventId = eventToStore)) } AppLog.log("agenda", - "traitement #${saved.id} : événement agenda " + - if (eventToStore != null) "créé/mis à jour (id=$eventToStore)" else "supprimé") + // Log VÉRIDIQUE (v1.3.5) : « supprimé » était + // imprimé aussi quand rien n'a été supprimé + // (switch désactivé, posologie absente, + // permission absente) → messages distincts. + "traitement #${saved.id} : " + when { + eventToStore != null -> + "événement agenda créé/mis à jour (id=$eventToStore)" + saved.calendarEventId != null -> + "événement agenda supprimé" + else -> + "agenda non activé (switch/posologie/permission)" + }) } catch (e: Exception) { AppLog.log("agenda", "ERREUR événement agenda : ${e.message}") } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3d54b53..9e446a2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,7 +1,42 @@ # Changelog — HormoneTrack Format : [Keep a Changelog](https://keepachangelog.com/fr-FR/1.1.0/). -Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.4`, …). +Versionnage : [SemVer](https://semver.org/). Releases taguées dans git (`v1.3.5`, …). + +## [1.3.5] — 2026-09-06 (versionCode 19) + +### Corrigé +- **Création du calendrier d'agenda impossible** (remontée via les logs de + diagnostic — le mécanisme v1.3.1 a tout de suite nourri le diagnostic + à distance : `Sync adapters must specify an account and account type`) : + l'URI d'insertion `CALLER_IS_SYNCADAPTER=true` du calendrier local + « HormoneTrack » n'embarquait pas `ACCOUNT_NAME` + `ACCOUNT_TYPE` — + exigé par le CalendarProvider pour toute opération sync-adapter + (`CalendarEvents.ensureCalendar`, §14 #50). Ces mêmes logs prouvent aussi + que **la permission agenda est désormais accordée** (fix manifest v1.3.3 ✓) + et que **l'export des logs fonctionne sur le téléphone** (fix #48 ✓) — + la chaîne de diagnostic est opérationnelle. +- **Suppression d'événement d'agenda silencieusement manquée** (#50 bis) : + l'URI de DELETE avait le même défaut (`CALLER_IS_SYNCADAPTER` sans params + account) → « switch OFF » + save loguait « supprimé » mais l'événement + restait dans le provider. Fix : mêmes paramètres sur l'URI de delete. + Validé émulateur : cycle complet ON→save→OFF→save = événement créé puis + réellement supprimé (`content query` vide). +- **Événement d'agenda orphelin après tout re-save** (#51) : + `TreatmentEditorScreen.buildTreatment()` ne transmettait pas + `calendarEventId` → l'id revenait à `null` à chaque sauvegarde : après le + premier save, l'événement devenait définitivement orphelin (plus jamais + supprimable/désactivable par l'app). L'id existant est désormais chargé à + l'ouverture et conservé par le save. +- **Log « événement agenda supprimé » mensonger** : imprimé aussi quand rien + n'a été supprimé (switch désactivé, posologie absente, permission absente). + Désormais trois messages distincts : « créé/mis à jour (id=…) », + « supprimé », « non activé (switch/posologie/permission) ». + +### Ajouté +- Validation émulateur de l'agenda end-to-end (recette §16.ter, permissions + accordées via `pm grant`) : calendrier + événement récurrent créés et + visibles dans le provider (`content query`). ## [1.3.4] — 2026-09-06 (versionCode 18) diff --git a/docs/DEVELOPPEMENT.md b/docs/DEVELOPPEMENT.md index 59faa55..8143f12 100644 --- a/docs/DEVELOPPEMENT.md +++ b/docs/DEVELOPPEMENT.md @@ -81,6 +81,7 @@ Fonctionnalités v1 : | 6 sept. 2026 (session farewell) | Repo créé côté farewell → **push SSH (alias `farewell` : giteassh:2222) + 12 releases publiées avec APK vérifiés par téléchargement** ; piège lexicographique `v1.2.10 < v1.2.5` en comparaison de chaînes épinglé (§14 #38) ; les deux instances Gitea sont synchrones. | | 6 sept. 2026 (session v1.3.3) | **Audit de reprise de maintenance** (nouvelle session IA) : 3 bugs racines trouvés — permissions agenda ABSENTES du manifest (jamais déclarées, §14 #44), export logs plantait TOUJOURS (le fix v1.3.2 réimplémentait l'IO au lieu de réutiliser BackupManager, §14 #45), bump de version JAMAIS commité (tags v1.3.0–1.3.2 tous `versionCode 14 / "1.3.0"`, §14 #46). Corrigés + doc rafraîchie (DB v3, targetSdk 36, régression 3 = export v1.3.1, §19/§20 staleness) ; **téléphone de test documenté : Google Pixel 9 /e/OS** (AOSP ; SAF DocumentsUI standard). | | 6 sept. 2026 (session v1.3.4) | **Deux crashs v1.3.3 remontés (Doses + export logs)** → reproduits sur ÉMULATEUR avec les vraies données (recette §16.ter, désormais standard) : (a) Doses = string `hrt_duration` 3 placeholders vs 2 args (**né en v1.3.1**, §14 #47) ; (b) export logs = `LocalDate.format("…-HHmm")` levant `UnsupportedTemporalTypeException` au TAP (§14 #48) — les fixes IO v1.3.2/v1.3.3 étaient à côté du vrai problème. Lint mis en filet bloquant (aurait attrapé les deux) + #49 notif ; helper `ExportFileNames` + 3 tests (90 verts) ; fixes VÉRIFIÉS sur émulateur avec l'APK release + données réelles (flux SAF complet : SAVE → fichier + message). Publication v1.3.4. | +| 6 sept. 2026 (session v1.3.5) | **Premier diagnostic à distance RÉUSSI via les logs exportés** : les AppLog de la v1.3.4 fournis par l'utilisatrice prouvent export logs `ok=true` (fix #48 ✓ au téléphone), permission agenda accordée (fix manifest ✓) et révèlent le bug racine EXPLICITE : `ensureCalendar` plantait car l'URI sync-adapter n'embarquait pas ACCOUNT_NAME/ACCOUNT_TYPE (§14 #50). En validant sur émulateur, DEUX bugs découverts : delete d'event sans account (#50 bis — « supprimé » logué mais l'event restait) et `buildTreatment` sans `calendarEventId` (#51 — événements orphelins). Cycle ON→save→OFF→save validé end-to-end (`content query`). Publication v1.3.5. | | 6 sept. 2026 (session v1.3.0) | Dialog « Nouveautés » après mise à jour (CHANGELOG embarqué en asset, tâche `copyChangelog`, version vue en DataStore) ; **événements d'agenda récurrents** (calendrier local HormoneTrack, RRULE posologie, permission runtime, Room v3 `calendarEventId`) ; Paramètres : version + lien releases ; **78 tests verts**, APK v1.3.0 + releases. | | 5 sept. 2026 (session v1.2.10) | Sens des boutons de zoom inversé (+ = zoom avant, convention carte — retour utilisateur) ; 67 tests verts, APK v1.2.10 + releases. | | 5 sept. 2026 (session v1.2.9) | **Zoom du graphique** (pinch + boutons, 6 h → 300 j, focal stable, échantillonnage adaptatif `stepForRange`, labels X 1 h/3 h) ; **README : disclaimer IA-assisté** en en-tête ; 3 tests ; **67 tests verts**, APK v1.2.9 + releases. | @@ -879,6 +880,48 @@ aucun contexte, dans l'esprit de cette doc) :** théoriques `NonObservableLocale`/`LocalContextGetResourceValueCall` sont tombés en warning via `app/lint.xml`, cf §19). +**Session v1.3.5 (diagnostic à distance via les logs exportés — la boucle par +l'exemple) :** + +50. **Création du calendrier d'agenda impossible** (`IllegalArgumentException: + Sync adapters must specify an account and account type`, remontée par les + AppLog exportés du Pixel 9 v1.3.4) : `CalendarEvents.ensureCalendar` + construisait l'URI calendars avec `CALLER_IS_SYNCADAPTER=true` SANS + `ACCOUNT_NAME`+`ACCOUNT_TYPE` — requis par le CalendarProvider pour TOUTE + opération sync-adapter (requête ET insertion). De plus : + - les mêmes logs CONFIRMAIENT au passage les fixes précédents : export + logs `ok=true` (#48 ✓ depuis v1.3.4 au téléphone), permission agenda + accordée (manifest v1.3.3 ✓) — AppLog fait son job de diagnostic à + distance ; + - le log « événement agenda supprimé » était imprimé aussi quand le + switch était inéligible (posologie absente, permission absente, + switch off) → trois messages distincts désormais (créé-mis à jour / + supprimé / non activé) ; + - contexte : les « événement agenda supprimé » répétés dans les logs = + des saves où le switch ne remplissait pas les conditions d'activation + (l'utilisatrice avait retourné le switch après les échecs) — les trois + messages distincts rendront les prochains logs univoques. + FIX : append ACCOUNT_NAME/ACCOUNT_TYPE sur l'URI ; le pattern complet : + `syncadapter=true` + account + account_type sur les URIs de CRÉATION + (calendriers) ; l'insertion d'EVENTS l'avait déjà (upsertEvent). Validé + émulateur : `content query --uri content://com.android.calendar/calendars` + montre le calendrier, events avec RRULE corrects. +50.bis. **Delete d'événement silencieusement manqué** (cité au #50) : le même + défaut (CALLER_IS_SYNCADAPTER sans account) sur l'URI de DELETE de + `deleteEvent` — l'exception est avalée par le catch, le log dit + « supprimé » mais l'event reste dans le provider. FIX : mêmes params + sur l'URI. Règle : TOUT URI avec CALLER_IS_SYNCADAPTER doit embarquer + account+account_type — pas seulement les insertions. +51. **Événement orphelin après re-save** (découvert en validant #50 sur + émulateur — le provider montrait un event que la base ne référençait + plus) : `buildTreatment()` reconstruisait le Treatment SANS le champ + `calendarEventId` → null à chaque save → l'event existant devenait + orphelin dès la deuxième sauvegarde, plus jamais désactivable. FIX : + `loadedCalendarEventId` chargé dans le LaunchedEffect + renvoyé par + buildTreatment(). Leçon : la reconstruction complete d'une entité pour + un save doit repartir des champs NON ÉDITABLES (voir `loadedCreatedAt`, + déjà protégé, et désormais `loadedCalendarEventId`). + **Leçons** : (a) ne jamais croire un build « probablement bon » sans l'avoir lancé ; (b) les tests sémantiques attrapent ce que la compilation ne voit pas ; (c) se méfier des constantes stdlib « de mémoire » (`ln2`), des mélanges Float/Double, et des APIs M3 @@ -1274,14 +1317,22 @@ Sur le téléphone de test (à compléter par l'utilisatrice) : (lint #49, ex format « 4.0 ») - [ ] **v1.3.4** : `./gradlew lint` vert — intégré à la vérification de release (step 2 de la checklist §16 désormais : tests + lint) +- [ ] **v1.3.5** : **agenda end-to-end** : sur un traitement actif avec + Posologie, activer « Événement d'agenda récurrent » → permission + accordée → sauvegarder → l'événement existe dans le calendrier local + « HormoneTrack » (« créé/mis à jour (id=…) » dans les logs exportés) ; + puis **désactiver le switch → re-save → l'événement disparaît + RÉELLEMENT** de l'app d'agenda (bugs #50 / #50 bis / #51 : en v1.3.4 + ni création ni suppression ne fonctionnaient, et les re-saves + orphelinaient les événements — cf §14) --- -*Doc mise à jour le 6 sept. 2026 (v1.3.4) — build OK, lint vert, 90/90 tests verts (72 sans +*Doc mise à jour le 6 sept. 2026 (v1.3.5) — build OK, lint vert, 90/90 tests verts (72 sans les données locales), dépôts Gitea (cloudyfy + farewell) avec releases APK, aucune donnée -de santé dans le dépôt ni l'historique. v1.3.3 = session de reprise de maintenance : -3 bugs racines corrigés (permissions agenda, export logs, bump de version). v1.3.4 : -2 crashs remontés reproduits sur émulateur avec les données réelles (recette §16.ter, -scripts/seed-emulator.py versionné) — bugs #47 (stringResource arity, né v1.3.1) et #48 -(LocalDate + pattern horaire au tap), lint mis en filet bloquant (#49, app/lint.xml), -ExportFileNames + 3 tests. Dette connue : fragments de labs dans l'historique git +de santé dans le dépôt ni l'historique. Fil des corrections : v1.3.3 = reprise de +maintenance (permissions agenda, export logs IO, bump de version) ; v1.3.4 = 2 crashs +reproduits sur émulateur avec les données réelles (#47 stringResource arity, #48 +LocalDate+pattern horaire), lint filet bloquant ; v1.3.5 = premier diagnostic à distance +RéUSSI via les logs exportés de l'utilisatrice (#50 CalendarProvider sync-adapter account, +log agenda véridique). Dette connue : fragments de labs dans l'historique git (v1.1.0→v1.2.3) — cf §8.bis.* diff --git a/docs/GUIDE_INSTALLATION.md b/docs/GUIDE_INSTALLATION.md index 7dedc81..e574c2b 100644 --- a/docs/GUIDE_INSTALLATION.md +++ b/docs/GUIDE_INSTALLATION.md @@ -86,9 +86,12 @@ Les notifications de l'app remontent automatiquement sur la montre via **Huawei passe-le **inactif** (switch « Actif ») : il disparaît de la saisie et de tes rappels, mais **son historique reste simulé et calibré** sur les graphiques — parfait pour une transition valerate → enanthate - - **Agenda** : active « Événement d'agenda récurrent » → un événement se + - **Agenda** : active « Événement d'agenda récurrent » (sous Rappels) → une + fois la permission accordée et le traitement sauvegardé, un événement se répétant tous les N jours (Posologie) apparaît dans le calendrier - « HormoneTrack » de ton téléphone + « HormoneTrack » de ton téléphone (≥ v1.3.5 ; en v1.3.4 la création du + calendrier échouait — retour dans le guide si besoin : re-sauvegarde avec + le switch activé) - Ester (EV / EU / EEn) + modèle (Estrannaise / Transfem Science) = les courbes du `.ods` - Pour gel/patch/oral : paramètres Bateman (temps au pic, demi-vie, biodispo) 2. **Doses → +** → logue tes injections passées