# WatchOsm — Proposition d'architecture Application **Wear OS 5** qui suit la navigation initiée par **OsmAnd+ 5.4.9** sous **Android 16**. | | | |---|---| | Version du document | 1.0 | | Cible | Montre Wear OS 5 (minSdk 34), autonome (standalone) | | Source de navigation | OsmAnd+ 5.4.9 sur le téléphone Android 16 | | Diagrammes | [`puml/`](puml/) — sources PlantUML versionnées | --- ## 1. Contexte et objectif OsmAnd+ assure le calcul d'itinéraire, le guidage et la voix sur le téléphone. En usage vélo, moto ou randonnée, le téléphone reste dans la poche : WatchOsm **reprend sur le poignet** l'information que la montre ne montre pas aujourd'hui — la prochaine manœuvre et les grandeurs de trajet — sans remplacer OsmAnd ni dupliquer son moteur de navigation. **Objectifs v1** 1. Détecter automatiquement le début, la pause et la fin d'une navigation OsmAnd, sans manipulation supplémentaire. 2. Afficher la prochaine manœuvre (flèche, distance, rue) en lisibilité maximale (fond noir, contraste élevé, écran maintenu allumé). 3. Afficher distance restante, temps restant, heure d'arrivée et vitesse. 4. Rester 100 % hors-ligne : aucun compte, aucun réseau, aucune collecte. **Non-objectifs v1** (volontairement exclus) - Cartographie ou calcul de route sur la montre (OsmAnd reste le cerveau). - Enregistrement de trace, tuiles, complications (phase 3). - Contrôle d'OsmAnd depuis la montre (pause/stop) : nécessite l'API AIDL OsmAnd côté téléphone (phase 2). --- ## 2. Options d'intégration étudiées OsmAnd ne publie **aucune API officielle de flux de navigation** vers une montre. Quatre ponts ont été évalués : | | Mécanisme | Avantages | Inconvénients | Décision | |---|---|---|---|---| | **A** | `NotificationListenerService` **sur la montre**, alimenté par les notifications OsmAnd relayées (bridging Wear OS) | Zéro app téléphone, zéro réseau, aucun couplet à maintenir, fonctionne avec OsmAnd+ tel quel | Dépend du format du texte de notification (non contractuel) ; permission manuelle ; bridging limité aux extras textuels | **Retenu pour v1** | | **B** | App compagnon **téléphone** : listener local + `Wearable.DataLayer` vers la montre | Source plus fiable (notifications complètes côté téléphone) | Deux modules à maintenir, installation à deux endroits | Phase 2 | | **C** | API **AIDL OsmAnd** (`net.osmand.aidlapi`) via app compagnon | Contrôle direct : `pauseNavigation`, `stopNavigation`, callbacks | Fonctionne uniquement côté téléphone, ne fournit pas le flux de consignes complet | Phase 2 (contrôle) | | **D** | Attendre l'app de montre officielle OsmAnd (PR `osmandapp/OsmAnd#26227`, *draft* en sept. 2026) | Rien à développer | Indéterminé, non publié, pas de calendrier | Suivi, hors périmètre | **Option A retenue** : elle donne une application Wear OS autonome, testable sans téléphone OsmAnd modifié, et l'heuristique de parsing est isolée dans une classe unique si le format OsmAnd évolue. ### Rappel de fonctionnement du pont OsmAnd publie une **notification permanente (ongoing)** pendant la navigation, contenant la consigne de manœuvre, l'ETA, la distance restante et la vitesse. Sur Wear OS, les notifications du téléphone sont **relayées automatiquement** (bridging Bluetooth) dans le `NotificationManager` local de la montre, où un `NotificationListenerService` peut les intercepter — y compris leurs extras textuels (`EXTRA_TITLE`, `EXTRA_TEXT`, `EXTRA_BIG_TEXT`…). La catégorie `Notification.CATEGORY_NAVIGATION`, quand OsmAnd la pose, sert de signalement fort dans le parseur. --- ## 3. Architecture retenue (v1) Application **monomodule** en trois couches strictes, sans framework de DI ni de base de données : il n'y a qu'un flux d'état à transporter. ``` ┌────────────────────────── couche présentation ──────────────────────────┐ │ MainActivity → AppNavHost (SwipeDismissableNavHost) │ │ ├─ HomeScreen (état de l'écoute, raccourci navigation) │ │ └─ NavigationScreen (manœuvre, métriques, keep-screen-on) │ │ ↑ StateFlow │ │ NavigationViewModel (relais de durée de vie) │ └──────────────────────────────────────────────────────────────────────────┘ ┌──────────────────────────────── couche données ─────────────────────────┐ │ OsmAndNotificationListener (NotificationListenerService) │ │ → OsmAndNotificationParser (texte → NavigationState, défensif) │ │ → NavigationStateRepository (source de vérité, StateFlow) │ └──────────────────────────────────────────────────────────────────────────┘ ┌────────────────────────────── couche domaine ───────────────────────────┐ │ NavigationState · TurnInstruction · Maneuver (enum) · Formatters │ │ (Kotlin pur, testable sans Android) │ └──────────────────────────────────────────────────────────────────────────┘ ``` ### Flux de données `OsmAnd (téléphone)` → *notification ongoing* → *bridging Wear OS* → `NotificationManager montre` → `OsmAndNotificationListener` → `OsmAndNotificationParser` → `NavigationStateRepository (StateFlow)` → `NavigationViewModel` → écrans Compose. Règles clés : - **Le service ne fait jamais d'UI** ; il transforme un évènement système en état. L'UI ne connaît ni les notifications ni OsmAnd. - **Le repository est unique** (contenu dans `AppContainer` de `WatchOsmApplication`) : il est partagé entre le service (production) et les ViewModels (consommation). - **La clé de notification active** (`activeNotificationKey`) empêche une notification annexe d'OsmAnd (enregistrement de trace, alerte) d'effacer l'état de navigation lorsqu'elle est retirée. - **Aucune persistance** : l'état vit en mémoire. La navigation reprend au prochain affichage de la notification, ce qui arrive à chaque poste. ### Garde-fous du parseur Le texte d'OsmAnd n'est pas un contrat. Le parseur (`OsmAndNotificationParser`) applique, dans l'ordre : 1. normalisation **caractère à caractère** (bas de casse, accents ASCII) pour conserver les indices entre chaîne normalisée et chaîne affichée ; 2. dictionnaire de motifs EN/FR **du plus spécifique au plus général** (« keep left » avant « left », rond-point avant directions) ; 3. extraction des distances avec interdiction explicite de lire « km/h » comme une distance, et distinction distance-de-manœuvre / distance-restante ; 4. **acceptation sous garantie** : un résultat n'est accepté que s'il contient un marqueur de navigation, un marqueur de pause, ou si la notification porte la catégorie `navigation` — sinon une notification d'enregistrement de trace (« 12,5 km ») serait prise pour une navigation active. ### Navigation entre écrans Deux écrans seulement, en `SwipeDismissableNavHost` (le retour latéral système fonctionne nativement) : - le passage à `isActive = true` ouvre automatiquement l'écran navigation ; - la fin de navigation laisse l'état « Navigation terminée » affiché 4 s, puis revient à l'accueil ; - l'accueil ouvre les réglages système d'accès aux notifications quand l'autorisation manque — sans lui, WatchOsm n'a aucune donnée. --- ## 4. Modèle de données ```kotlin NavigationState( isActive, isPaused, instruction: TurnInstruction?(maneuver: Maneuver, distanceMeters, streetName, rawText), remainingDistanceMeters, remainingTimeMinutes, arrivalTimeLabel, speedKmh, speedLimitKmh, destination, updatedAtMillis ) ``` - `Maneuver` : enum de 11 valeurs couvrant le vocabulaire OsmAnd (EN/FR). - Tout champ est **nullable** : OsmAnd omet régulièrement des informations selon les réglages ; l'UI a une réponse pour chaque absence (tiret, ligne masquée) et ne plante jamais sur un champ manquant. - `Formatters` produit les formes françaises (« 5,4 km », « 1 h 05 »). --- ## 5. Structure du projet ``` WatchOsm/ ├── build.gradle.kts / settings.gradle.kts / gradle/libs.versions.toml ├── documentation/ │ ├── architecture.md ← ce document │ └── puml/ │ ├── usecases.puml cas d'utilisation │ ├── composants.puml composants │ ├── classes.puml classes │ ├── sequence_navigation.puml séquence notification → écran │ ├── etats_navigation.puml machine à états │ └── deploiement.puml déploiement téléphone + montre └── wear/ module unique v1 └── src/main/java/fr/barteam/watchosm/ ├── WatchOsmApplication.kt conteneur (AppContainer) ├── domain/ NavigationState, Maneuver, Formatters ├── data/ listener, parseur, repository └── ui/ MainActivity, AppNavHost, ViewModel, écrans, ManeuverArrow, thème ``` --- ## 6. Contraintes, risques et atténuation | Risque | Impact | Atténuation | |---|---|---| | OsmAnd change le format de sa notification | Consignes non reconnues | Parseur isolé + tests unitaires sur formats connus + filet catégorie `navigation` | | **Le `NotificationListenerService` de la montre ne reçoit pas les notifications relayées** (comportement variable selon la version Wear OS et le fabricant) | Aucune donnée | Vérification hardware à faire en premier ; solution de repli = phase 2 (app compagnon téléphone + `Wearable.DataLayer`) | | Bridging Wear OS tronque les extras | Données partielles | Champs nullables, rendu dégradé, jamais de crash | | Permission d'écoute non accordée | Aucune donnée | Écran d'accueil explicite + bouton d'ouverture des réglages système | | Écran qui s'endort pendant le guidage | Inutilisable à vélo/moto | `FLAG_KEEP_SCREEN_ON` sur l'écran navigation tant que `isActive` | | Notification d'enregistrement prise pour une navigation | Faux positif | Garde-fou d'acceptation (marqueur ou catégorie) + clé de notification active | | Batterie de la montre | Autonomie | Aucun GPS, aucun réseau, aucun réveil périodique : seul un `NotificationListenerService` est lié | ## 7. Tests et vérification - **Tests unitaires JVM du parseur** (`wear/src/test/.../OsmAndNotificationParserTest`) : 11 cas — formats FR/EN, rond-point, demi-tour, vitesse ≠ distance, notification d'enregistrement rejetée, pause, catégorie navigation, arrivée, légère-gauche, durée composée. Commande : `./gradlew :wear:testDebugUnitTest`. - **Validation des diagrammes** : `java -jar plantuml.jar -checkonly documentation/puml/*.puml`. - **Lint** : `./gradlew :wear:lint`. - Vérification manuelle sur montre : démarrer une navigation OsmAnd, observer l'ouverture automatique de l'écran, la fin et le retour à l'accueil. ## 8. Évolutions planifiées | Phase | Contenu | |---|---| | **2** | Module `phone/` : notification listener local (source plus riche) + API AIDL OsmAnd (`pauseNavigation`, `stopNavigation`, `resumeNavigation`) + pont `Wearable.DataLayer` (MessageClient) vers la montre | | **3** | Tile de navigation (aperçu permanent), complication de manœuvre, support Always-On Display / mode ambiant natif, profilage batterie | | **4** | Suivi de l'app de montre officielle OsmAnd (PR `osmandapp/OsmAnd#26227`) : bascule sur le protocole natif si publié | --- ## 9. Diagrammes | Diagramme | Fichier | Objectif | |---|---|---| | Cas d'utilisation | [`puml/usecases.puml`](puml/usecases.puml) | Périmètre fonctionnel et acteurs | | Composants | [`puml/composants.puml`](puml/composants.puml) | Découpage en couches et flux | | Classes | [`puml/classes.puml`](puml/classes.puml) | Structure statique | | Séquence | [`puml/sequence_navigation.puml`](puml/sequence_navigation.puml) | Début/pause/fin d'une navigation | | États | [`puml/etats_navigation.puml`](puml/etats_navigation.puml) | Cycle de vie de `NavigationState` | | Déploiement | [`puml/deploiement.puml`](puml/deploiement.puml) | Téléphone, relais Bluetooth, montre | Les diagrammes sont versionnés en **sources PlantUML** ; la rendu PNG/SVG s'obtient avec `java -jar plantuml.jar -tsvg documentation/puml/*.puml` (PlantUML n'étant pas installé sur la machine de développement actuelle).