architecture.md 13 KB

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/ — 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<NavigationState>                                │
│  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

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 Périmètre fonctionnel et acteurs
Composants puml/composants.puml Découpage en couches et flux
Classes puml/classes.puml Structure statique
Séquence puml/sequence_navigation.puml Début/pause/fin d'une navigation
États puml/etats_navigation.puml Cycle de vie de NavigationState
Déploiement 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).