# Client Android — relecture et scénario d'essai Réécrit le **2026-08-28**, après une séance qui a remué les deux secrets du coffre, l'écran des réglages, le code de déverrouillage et le clavier. La version précédente décrivait une application qui n'existe plus. Ce que cette séance a confirmé, et qui vaut plus que n'importe quelle liste : **les défauts se tiennent par la main.** Corriger le clavier qui volait le premier appui a laissé le clavier ouvert ; le clavier ouvert recouvrait le message ; et le message invisible cachait qu'une action avait réussi. Aucun des deux derniers n'était observable tant que le premier tenait. D'où la règle qui suit : **quand un essai échoue, lire le journal AVANT de retenter**, parce que la synchronisation le vide. --- ## Ce que la séance a corrigé, et pourquoi c'est éprouvable ### 1. Le journal ne perdait plus les causes — CORRIGÉ, et c'est le plus important Trois trous, tous du même genre : une faute arrivait, et rien ne la nommait. - **Les gestionnaires d'erreur lisaient l'exception après sa libération.** `RegisterLocal` et `LoginLocal` mettaient leur handler en file avec `TThread.Queue`, puis y lisaient `E.Message` — or Delphi libère `E` en sortant du `except`, bien avant que la file s'exécute. Résultat : un message vide, et parfois une violation d'accès. **Le vrai motif n'a jamais été affiché de toute l'histoire de ce chemin.** - **Le dispatch du pont n'était gardé par rien** : une faute dans un handler remontait à FMX, qui affichait « Access violation » avec une adresse pendant que le journal montrait la commande reçue suivie de rien. - **Le retour sur le thread principal** (`TThread.Queue`) n'était couvert ni par le `except` du thread, ni par celui du pont : il s'exécute après que les deux ont rendu la main. **Ce que ça change pour les essais** : un échec porte désormais un nom. Si un essai ci-dessous échoue sans laisser de ligne au journal, c'est un quatrième trou du même genre, et il faut le traiter avant de chercher le défaut. ### 2. Deux secrets, longtemps confondus — CORRIGÉ Le mot de passe de création scellait le conteneur local **et** prétendait sceller l'instantané du dépôt. Un coffre créé ici publiait donc sous son propre mot de passe, que le poste ne savait plus ouvrir. - `FVaultPwd` ouvre `vault-local.json`, et rien d'autre. - Le secret du dépôt vit dans les réglages, doit être identique sur tous les appareils, et lui seul ouvre et rescelle l'instantané. - **Ils ne se rencontrent qu'à un endroit**, la réconciliation : le conteneur local s'ouvre sous le sien, puis la clé est jetée avant qu'on touche au dépôt. Migration : rien à rechiffrer. Les coffres existants ont simplement les deux valeurs égales, et cessent d'être forcés de le rester. ### 3. « Coffre ouvert » se lisait sur le nombre d'entrées — CORRIGÉ Un coffre **vide** en compte zéro, comme un coffre fermé. Le garde de `DoSync` restait donc vrai après l'ouverture, réarmait la reprise, et l'on tournait en rond. Le même idiome coupait `OnIdleTimer` : un coffre vide ne s'auto-verrouillait jamais. Les trois lisent `FPlain`, le clair en main. ### 4. Ce qui reste assumé, sans correction - **`SaveAttachment`, `RegisterLocal` et `LoginLocal`** libèrent l'indicateur d'occupation dans leur `except` plutôt que dans un `finally`. Le minuteur de 25 s le lève de toute façon, et c'est son rôle. - **`ParseJSONValue(...) as TJSONObject`** fuit sur une entrée qui n'est pas un objet — le transtypage lève avant l'affectation. N'arrive que sur un conteneur corrompu, où l'on a de plus gros ennuis. - **Le nom d'utilisateur ne sert à rien dans l'APK.** Il est écrit dans l'instantané à la création, et c'est le **poste** qui s'en sert pour refuser de fondre deux coffres différents. À la connexion il ne protège rien : le mot de passe seul ouvre, et le vrai nom est lu dans le conteneur. --- ## Scénario d'essai Chaque étape dit **ce qu'il faut voir**, pas seulement ce qu'il faut faire : un test dont on ne sait pas ce qu'il doit produire ne prouve rien. **L'ordre n'est pas alphabétique** : `Préparation → C → A → B → D → E → …`. La série C réconcilie, et c'est elle qui fait du coffre né ici un coffre **publié**. Tant qu'il ne l'est pas, `CommitChange` voit `LLocalOnly`, écrit *« save: written locally »* et ne publie rien — il n'y a pas de dépôt. La série A guetterait donc un point orange qui ne peut pas paraître, et conclurait à un défaut là où il n'y a que l'ordre. **Outil de remise à zéro** : *Settings → Debug → Delete the local vault* (deux appuis). Il efface cache, conteneur local et drapeau d'attente, **et garde les réglages du dépôt** — on retélécharge sans rien retaper. Absent en release : la commande n'y est même pas compilée. ### Préparation — l'état de départ, et il compte Plusieurs séries agissent sur une entrée existante. Partir d'un coffre vide les rendrait ininterprétables : on ne sait pas distinguer « la modification n'a pas voyagé » de « il n'y avait rien à faire voyager ». 1. *Settings → Debug → Delete the local vault*, puis **Create vault**. Le coffre naît vide : zéro entrée, **et aucun dossier** — `NewEmptySnapshot` écrit un tableau `folders` vide. 2. Créer **une seule entrée**, avec un mot de passe reconnaissable. Elle se range dans **`All`**, et c'est ce premier enregistrement qui fait apparaître le dossier : `AddEntry` pose `folder = 'All'` parce que c'est le dossier par défaut du poste, pas parce que c'est joli. 3. Vérifier qu'il n'y a **qu'un dossier** dans la barre. Deux au départ voudrait dire qu'un instantané traîne encore. C'est le point de départ de tout ce qui suit. Y revenir entre deux séries qui se gênent coûte deux appuis. **Un coffre vide reste un cas à éprouver**, mais explicitement : c'est lui qui a révélé que « coffre ouvert » se lisait sur le nombre d'entrées (défaut n° 3), et que l'auto-verrouillage se coupait de lui-même. À reprendre au moins une fois sans entrée : *Sync* ne doit pas boucler, et le coffre doit finir par se verrouiller seul. ### A. Enregistrement hors connexion — le plus important **Exige un coffre PUBLIÉ** — donc la série C d'abord. Sur un coffre né ici et jamais réconcilié, il n'y a rien à publier et rien ne se met en attente. 1. Coffre ouvert, **couper les données** (mode avion). 2. Modifier un mot de passe → *« saved here, not published yet »*, et **point orange** sur le cadenas. 3. Verrouiller → le point doit être **sur le bouton Sync** de l'accueil. 4. **Fermer complètement l'application** (glisser depuis les récentes), rouvrir, ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui éprouve la persistance du drapeau ; sans elle, le reste ne prouve rien. 5. Rétablir le réseau, *Sync* → *« Offline changes published »*, point éteint. 6. Sur le poste, synchroniser → la modification doit y être. ### B. La fusion, qui est le cœur du sujet 1. Téléphone **hors ligne**, modifier l'entrée **X**. 2. Sur le poste, modifier une **autre** entrée **Y**, synchroniser. 3. Téléphone en ligne, *Sync*. 4. **Les deux modifications doivent survivre.** Si une seule survit, c'est l'arbitrage sur `updated_at` qu'il faut regarder. 5. Variante avec un **dossier créé hors connexion** : il doit apparaître dans la barre de dossiers après la fusion, et sur le poste après sa synchronisation. ### C. Les deux secrets — nouveau, et jamais éprouvé de bout en bout 1. Effacer le coffre local (bouton Debug). Les trois champs du dépôt doivent **rester remplis** — c'est la moitié de ce que ce bouton promet. 2. *Create vault* avec un mot de passe **différent** de celui du dépôt. 3. Ouvrir les réglages : le champ *sync password* doit porter **celui du dépôt**, pas celui qu'on vient de choisir. C'est toute la séparation en une capture. 4. *Sync* → la réconciliation lit le dépôt et republie **sous le secret du dépôt**. Contre-épreuve décisive : **le poste doit relire le dépôt sans se plaindre.** S'il échoue, c'est que la clé du conteneur local a servi à resceller l'instantané. 5. Après la réconciliation, `vault-local.json` doit avoir disparu — c'est ce geste qui passe l'autorité au dépôt. ### D. Ouvrir demande quelque chose — nouveau 1. Effacer le coffre, puis *Download vault*. 2. **Fermer complètement l'application**, rouvrir → le champ de code doit paraître et réclamer **le mot de passe de sync**. Le cache est scellé sous lui, parce qu'il le contient déjà : l'ouvrir sans rien livrait le coffre *et* le secret du dépôt. 3. Vérification par le fichier, si adb est à portée : `run-as com.embarcadero.pmserverApk od -An -tu1 -N1 files/vault.bin` doit rendre **4** (`VER_PIN`) et non 1. 4. Un **mauvais code** → *« Wrong unlock code »* **en rouge**, et *Open vault* reste offert : le cache n'est pas déclaré illisible. 5. Sur un appareil **avec biométrie**, rien de tout cela : `VER_WRAP` garde déjà la porte, et imposer une frappe serait un recul. C'est volontaire. ### E. Le dépôt a bougé — témoin bleu 1. Coffre ouvert sur le téléphone, **le laisser ouvert**. 2. Sur le poste, modifier une entrée et synchroniser. 3. Attendre — la vérification bat toutes les **dix minutes**. Le journal doit dire `remote has moved - sync to bring it in`, et un **point bleu** paraître sur *Sync* et sur le cadenas. 4. **Rien ne doit changer à l'écran de soi-même.** Si l'entrée qu'on regarde se met à jour toute seule, c'est que le sondage adopte au lieu de signaler. 5. *Sync* → le point bleu s'éteint. 6. Les deux ensemble : provoquer A puis E sans synchroniser → **orange et bleu côte à côte**. Ils demandent deux gestes différents. ### F. Réglages — largement réécrits, à reprendre en entier 1. **Coffre fermé**, ouvrir les réglages depuis l'accueil → la section *Unlock code* doit être **absente**. Poser comme retirer exigent le clair en main : coffre fermé, les deux ne pourraient que refuser. 2. Ouvrir le coffre, roue dentée → la section paraît. 3. **Poser un code** : taper, *Set this code* → *« Unlock code set »* **immédiatement, sans quitter l'écran**. Plus de « ouvre le coffre pour l'appliquer » : le code s'applique sur-le-champ. 4. Le message doit se voir **au-dessus du clavier resté ouvert**. C'est le défaut le plus discret de la séance : le toast vivait sous le clavier. 5. **Un seul appui** doit suffire sur *Set this code*. S'il en faut deux, le premier est mangé par le clavier et la correction du focus a régressé. 6. **Retirer le code** : *Remove the unlock code* sans rien taper → refus *« Type the current unlock code to remove it »*. Puis taper le code courant, deux appuis → *« Unlock code removed »*. Fermer l'application, rouvrir → **le code n'est plus demandé**. 7. Contre-épreuve : *Set this code* avec un champ **vide** ne doit **pas** retirer le code — c'était une porte dérobée vers le retrait sans le connaître. 8. Le champ *sync password* doit porter le mot de passe du coffre **détenu**, pas une valeur de premier lancement. 9. Touche **retour** dans les réglages → referme **sans** verrouiller ni changer de vue. ### G. Icônes 1. Ouvrir une entrée dont le *Site* est une adresse — `git.ai-agents4you.com`. 2. Trois points → **Fetch icon** → l'icône doit apparaître dans la liste. 3. Une entrée dont le site n'est **pas** une adresse (*Gitea*, ou vide) → refus *« no web address to look up »*. Sans point dans le nom, il n'y a rien à demander au relais. 4. Un domaine à trois niveaux (`chat.qwen.ai`) doit retomber sur le second niveau : c'est le deuxième essai. 5. **Remove icon** → l'entrée reprend son initiale. 6. Synchroniser depuis le poste : l'icône doit y arriver, et le poste ne doit **pas** la traiter comme une icône choisie — c'est le rôle du drapeau de provenance, mis à zéro par une recherche automatique. ### H. Anciens mots de passe **Le poste doit synchroniser d'abord** : il ne les envoie que depuis peu, et un instantané publié avant ne porte aucun historique. 1. Sur le poste, changer le mot de passe d'une entrée, puis synchroniser. 2. Téléphone : *Sync*, ouvrir l'entrée, trois points → **Previous passwords**. L'ancienne version doit être là, avec sa date. 3. Copier l'une d'elles → *« Previous password copied »*. 4. Touche **retour** → le voile se referme sans toucher au coffre. 5. Une entrée jamais modifiée → *« No previous password recorded »*, et non un écran vide. 6. Contre-épreuve : verrouiller, rouvrir, **Previous passwords** → la liste doit être reconstruite, jamais celle d'avant restée à l'écran. ### I. Clavier, mise en page, retour d'application — nouveau 1. Taper dans un champ, appuyer sur un bouton **hors de sa rangée** (*Open vault*, *Sign in*, *Set this code*) → **un seul appui**, et le clavier se replie ensuite. 2. Taper dans un champ, appuyer sur un bouton **de sa rangée** (l'œil d'un champ secret) → le clavier **reste ouvert** et le curseur en place. Les deux moitiés comptent : la première sans la seconde laisse le clavier dressé. 3. **Réglages ouverts coffre FERMÉ**, verrouiller le téléphone, déverrouiller → la page doit revenir. C'était un aplat bleu sans rien dessus : le masquage ne demandait rien, la restitution exigeait un coffre ouvert. 4. Même chose **coffre ouvert** : passé le délai de grâce, le verrouillage doit avoir lieu et ramener sur l'accueil — pas sur la liste. 5. Accueil sans coffre : l'ordre doit être **Download vault → Settings → username / password → Create vault**, espacements réguliers, et *Download vault* doit porter la couleur d'accent quand un dépôt est configuré. 6. Les boutons du bandeau (roue, +, cadenas) et ceux de la barre de sélection sont **ronds**. ### J. Sélection et actions de liste 1. Appui long → barre contextuelle, compteur. 2. Deuxième ligne → compteur à 2. 3. **Dupliquer** → deux copies, suffixées *(copy)*. 4. **Supprimer** → confirmation en deux temps, puis les lignes disparaissent. 5. Touche **retour** pendant une sélection → elle s'annule **sans verrouiller**. 6. Sur le poste, synchroniser → les suppressions doivent y être répercutées. ### K. Notes multilignes 1. **+** → *Note* → zone de texte pleine largeur, *Site* et *Username* disparus. 2. Taper plusieurs lignes — Entrée doit **insérer un saut**, pas replier le clavier. 3. Enregistrer, rouvrir → les ruptures doivent être là **en lecture**. 4. Basculer *Login* → *Note* après avoir tapé : la saisie doit **survivre**. 5. Contre-épreuve : dans un champ d'une ligne, Entrée doit toujours **replier le clavier**. ### L. Pièces jointes 1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans *Téléchargements* sous son nom, accents et arabe compris. ### M. Empaquetage 1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher `Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android Debug`, la release ne vaut rien. 2. Elle ne s'installe pas d'elle-même, et c'est voulu. 3. Vérifier que la section **Debug** des réglages est **absente** de cette version. Elle ne peut pas y être — la commande n'est pas compilée — mais c'est le genre de garantie qu'on regarde une fois plutôt que de la croire. --- ## Ce qui n'est toujours pas éprouvé - **Le chemin `-Release`** du script n'a jamais produit d'APK signé. Il compile, c'est tout ce qu'on sait. - **L'invalidation de la clé par le système** (§ D.5 pour la biométrie) : le message *« Device protection changed »* existe et son drapeau est posé là où l'invalidation se constate, mais personne ne l'a vu s'afficher. Il faudrait retirer le verrouillage d'écran d'un appareil qui en a un. - **`MergeSelfCheck`** tourne au démarrage en débogage et écrit `merge selfcheck: the four rules hold` dans le journal. Il passe ; personne ne l'a lu en cherchant à le mettre en défaut. - **Le mode `login`** (conteneur local sans cache) ne s'atteint qu'en supprimant le cache à la main. C'est là qu'a été trouvée la lecture après libération : il n'avait jamais tourné.