feat(postit): keep Android SignalR chat in background without ongoing notification

Share the chat transport between UI, calls and an opt-in background service. Restore account rooms and attempt boot restart, keeping event alerts independent from connection permissions. Document Android limits and add lifecycle regressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Paul Schneider 2026-11-03 12:43:46 +00:00
commit 311d55dcc6
Signed by: notazof
GPG key ID: AD4B9B545FBD7D12
32 changed files with 2210 additions and 143 deletions

View file

@ -23,6 +23,7 @@ Chaque sujet est détaillé dans sa propre page (sous
| Dictionnaires métier (héritage en arbre) | [dictionnaires-metier.md](architecture/dictionnaires-metier.md) |
| Offre fournisseur + frontmatter (`ClasseFormulaire`, `ClasseDevis`, parsing) | [offres-frontmatter.md](architecture/offres-frontmatter.md) |
| PostIt / OIDC (client desktop, custom URI scheme, silent refresh) | [postit-oidc.md](architecture/postit-oidc.md) |
| Notifications Web/PostIt, ChatHub et service Android SignalR | [notifications.md](architecture/notifications.md) |
## Stack technique

View file

@ -20,12 +20,13 @@ clients.
| Annonce persistée | Entité `Notification` en base, API HTTP | Affichée dans le layout et masquable | Liste sur `HomePage`, chargée par HTTP et masquable |
| Message d'interface | Liste de portée requête dans `ViewBag.Notify` | Affichée comme alerte dans le layout | Non consommé |
| Événement de chat | Hub SignalR authentifié à `/chathub` | Le site Web n'en consomme pas dans ses vues MVC | `notifyUser` et `notifyRoom` alimentent les journaux du chat |
| Événement métier `push` | SignalR vers les connexions actives, plus e-mail | Pas de consommateur identifié dans les vues Web | Pas de gestionnaire `push` dans le client PostIt actuel |
| Événement métier `push` | SignalR vers les connexions actives, plus e-mail | Pas de consommateur identifié dans les vues Web | Alerte native par le service Android ; liste persistée sur l'accueil via HTTP |
| Alerte native Android | Connexion SignalR existante | Sans objet | Service de fond facultatif ; notification seulement à réception d'un événement |
Les notifications persistées sont donc différentes d'une notification système
native (notification du navigateur ou du système d'exploitation). Le client
PostIt affiche une liste dans sa page d'accueil ; il ne demande pas de
permission de notification native.
PostIt affiche une liste dans sa page d'accueil. Sur Android, le service de
messagerie peut aussi afficher des notifications natives, après autorisation.
## 1. Annonces persistées
@ -173,6 +174,7 @@ Les événements de notification émis par le hub comprennent notamment :
| `notifyRoom` | `type`, salon, utilisateur ou détail | Ajout au journal du salon ; met à jour la liste d'utilisateurs sur `UserJoin` |
| `joint` | `ChatRoomInfo` | Initialise la vue du salon rejoint |
| `ReceiveMessage` | message avec nom, salon et corps | Ajoute le message à l'historique du salon |
| `addPV` | expéditeur, texte privé | Ajoute le message privé au journal système ; alerte native Android en arrière-plan |
| `notifyUserInRoom` | `type`, salon, message | Pas de gestionnaire correspondant dans `ChatViewModel` actuellement |
Les constantes de types du chat sont déclarées dans
@ -181,23 +183,128 @@ Les messages `notifyUser` et `notifyRoom` ne sont pas enregistrés en base :
ils décrivent l'état temps réel de connexion, de salon ou les erreurs de
conversation.
Le [`ChatViewModel`](../../src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs)
Le singleton
[`ChatHubConnection`](../../src/PostIt/PostIt/Services/ChatHubConnection.cs)
ouvre le hub avec le jeton d'accès PostIt, tente un renouvellement silencieux
du jeton, et active la reconnexion automatique. Après reconnexion, il rejoint
à nouveau le salon sélectionné, car la connexion SignalR précédente et ses
groupes ne sont plus actifs. Les callbacks mettent à jour les collections sur
le thread UI d'Avalonia.
du jeton et active la reconnexion automatique. Il est partagé par le service
Android, le
[`ChatViewModel`](../../src/PostIt/PostIt/ViewModels/Chat/ChatViewModel.cs)
et la signalisation des appels : ouvrir la page ne crée pas une seconde
connexion.
Le transport conserve en mémoire les 500 dernières livraisons de messages
privés et de salon pour les présenter si l'interface s'ouvre après le
démarrage du service. Ce tampon est borné, non persisté sur disque et vidé
à l'arrêt de la connexion ou au changement de compte. Il ne permet pas de
récupérer les messages envoyés lorsque le socket était déconnecté.
Les salons sont mémorisés localement, par hôte et compte, dans
`Settings.ChatRoomsByAccount`. Le transport les rejoint à nouveau après une
reconnexion ou un redémarrage du processus, sans dépendre de l'interface
Avalonia. « Quitter le salon » supprime cette mémorisation. Cela n'ajoute pas
d'abonnement serveur hors ligne ni d'historique des messages manqués.
Les callbacks destinés aux collections de l'interface sont dispatchés sur
le thread UI et les callbacks obsolètes sont ignorés après déconnexion.
### Événement métier `push`
En plus des événements du chat, `YavscMessageSender` utilise le même contexte
SignalR pour émettre `push` avec le topic et une charge utile sérialisée. Ce
nom d'événement est distinct de `notifyUser` et `notifyRoom`. Aucun
gestionnaire `push` n'est présent dans le client PostIt actuel : les annonces
métier persistées sont visibles dans PostIt lors du prochain chargement ou
rafraîchissement HTTP.
nom d'événement est distinct de `notifyUser` et `notifyRoom`. Le service
Android peut en afficher une alerte native ; les annonces métier persistées
restent visibles dans PostIt lors du prochain chargement ou rafraîchissement
HTTP.
## 5. Points d'attention pour les évolutions
## 5. Android : connexion SignalR en arrière-plan
### Réglage utilisateur et durée de vie
`AndroidKeepChatConnected`, désactivé par défaut, est éditable dans les
paramètres Android :
« Garder la connexion au serveur active et la reprendre au démarrage du
téléphone ». Enregistrer les paramètres pour appliquer le changement.
- **Désactivé** : le chat reste connecté pendant la vie du processus PostIt,
même si l'activité est masquée, mais rien ne garantit que ce processus
survive en arrière-plan. Les messages alimentent le journal ; les alertes
natives en arrière-plan nécessitent le service activé.
- **Activé** : un service Android ordinaire héberge la même connexion
SignalR, **sans notification permanente**. Un récepteur
`BOOT_COMPLETED` tente de reprendre ce service si l'option et une session
enregistrée sont encore disponibles.
- La fermeture d'une page ou de l'activité ne ferme pas la messagerie.
La déconnexion du compte arrête le service et la connexion avant la
suppression des jetons. Les appels et captures caméra/micro restent
soumis à leur propre cycle de vie.
- Le service peut démarrer sans construire l'interface Avalonia. L'ouverture
ultérieure de l'application réutilise le même conteneur et le même
transport, sans connexion doublonnée.
Le service n'appelle ni `StartForegroundService` ni `StartForeground` et ne
publie aucune notification de connexion/reconnexion. Pour l'arrêter,
désactiver l'option et enregistrer les paramètres, ou déconnecter le compte.
Les notifications ne sont créées que lors de nouveaux messages ou événements ;
elles disparaissent à leur ouverture et ne sont pas marquées permanentes.
Android 13+ demande l'autorisation `POST_NOTIFICATIONS` pour ces alertes
visibles. Un refus empêche les alertes, **pas la connexion SignalR**.
Les échecs de démarrage sont signalés, sans basculer silencieusement vers
un service de premier plan et sans répéter une permission refusée.
### Notifications et limites
Le service écoute les messages privés, les messages des salons rejoints et
l'événement métier SignalR `push`. En arrière-plan, il affiche des alertes
locales génériques pour ne pas exposer de contenu privé sur l'écran
verrouillé. Il ne faut pas confondre cet événement SignalR nommé `push` avec
un service de push externe.
**Aucun Firebase/FCM, serveur Google de notifications, jeton d'appareil,
configuration de projet ou migration push n'est requis.** Les API métier et
le ChatHub existants restent les seuls services réseau utilisés pour la
messagerie.
Cette solution est de type *best effort*, pas une connexion immortelle :
- Android, Doze, l'absence de réseau et les politiques des constructeurs
peuvent interrompre le socket ou empêcher le redémarrage du service.
Autoriser les données en arrière-plan permet le trafic réseau ; désactiver
manuellement l'optimisation de batterie pour PostIt assouplit les limites
de Doze. Ces réglages ne garantissent pas la survie du processus ou du
service et ne suppriment pas les autres limites d'exécution Android.
- Android 8+ peut arrêter un service ordinaire lorsque l'application reste
en arrière-plan, ou refuser `StartService` depuis le récepteur de démarrage.
Dans ce cas, PostIt signale le refus et attend une nouvelle ouverture de
l'application ; aucune tentative de contournement par service de premier
plan, tâche périodique ou exemption automatique n'est ajoutée.
- Après un arrêt forcé par l'utilisateur, rouvrir PostIt ; le démarrage du
téléphone n'est pas un contournement de cet arrêt.
- Le renouvellement nécessite une session encore valide. Une session
révoquée ou sans jeton de renouvellement exige une reconnexion.
- Les messages de chat sont éphémères : les messages envoyés pendant une
interruption ne sont pas récupérés par cette fonctionnalité. Les
notifications métier persistées restent accessibles par l'API.
- Aucun type `specialUse` ni permission `FOREGROUND_SERVICE_SPECIAL_USE`
n'est déclaré pour la messagerie. Les services de premier plan des appels
et captures caméra/micro restent inchangés : ils ne sont pas utilisés pour
maintenir le chat.
### Validation sur téléphone
Les tests locaux vérifient le partage de connexion, la restauration des
salons et le nettoyage de session. Ils ne remplacent pas une validation
Android réelle : absence de notification lors de l'activation et des
reconnexions, notification seulement à réception d'un événement activité
masquée, retour dans l'interface sans second socket, désactivation dans les
paramètres, déconnexion/reconnexion, coupure et retour réseau, veille
prolongée, redémarrage du téléphone et refus de permission sans arrêt du chat.
Une activité de vérification est incluse uniquement dans l'APK Debug :
`fr.pschneider.postit.ChatServiceTestActivity`. Son résultat est écrit dans
le fichier privé de l'application `files/chat-service-test-result.txt`.
Elle vérifie la composition du conteneur partagé, pas la réception réelle
après un démarrage ou une longue veille.
## 6. Points d'attention pour les évolutions
- Conserver les règles d'audience alignées entre l'API et le site Web ; en
particulier, appliquer un filtrage `Target` côté Web avant d'exposer les