Files
r-zakarya 5bcb969552 docs(android): la serie M est close, et raconte ses quatre obstacles
L'APK signe existe. Le chemin -Release n'avait jamais tourne et a butte
quatre fois - dossier de mise en scene absent, volume plein, cinq
doublons refuses par aapt, ressources generees jamais rangees. Toutes des
choses que seule une release partant d'un terrain vierge pouvait
rencontrer.

La verification de la section Debug est corrigee : chercher wipelocal
dans le .so ne prouve rien, la page HTML embarquee la contient dans les
deux configurations. Seule la chaine du gestionnaire Pascal tranche, et
en UTF-16LE. La garantie est "inatteignable et sans effet", pas
"absente".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:00:46 +01:00

357 lines
19 KiB
Markdown

# 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
**Cette série était impossible telle qu'elle était écrite**, et la mesure le dit :
`AUTOLOCK_MINUTES = 3` verrouille après trois minutes d'inactivité, tandis que
`PROBE_EVERY_TICKS = 40` — à quinze secondes le battement — demande **dix minutes
de coffre ouvert**. Le compteur n'avance que tant que le coffre l'est, et
`LockVault` sort avant de l'incrémenter. « Laisser ouvert dix minutes sans y
toucher » verrouillait donc à la troisième, et le sondage ne partait jamais.
Ce n'est pas un défaut de l'application : le témoin sert **pendant qu'on se sert
du coffre**, et quelqu'un qui s'en sert le touche. Mais il faut le dire, sinon on
cherche un point qui ne peut pas paraître.
1. Coffre ouvert sur le téléphone. **Le toucher toutes les deux minutes** —
faire défiler la liste suffit, il ne faut surtout pas synchroniser : un
*Sync*, comme une ouverture, remet le compteur à zéro.
2. Sur le poste, modifier une entrée et synchroniser.
3. Continuer à toucher jusqu'à **dix minutes de coffre ouvert**. 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.** Cette condition compte autant
que le témoin : si l'entrée qu'on regarde se met à jour toute seule, le
sondage adopte au lieu de signaler, ce qui est plus grave qu'un témoin absent.
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.
**Si tu préfères ne pas veiller dix minutes**, la contre-épreuve courte : mets
`PROBE_EVERY_TICKS` à 8 — deux minutes — recompile, éprouve les points 3 à 6,
puis remets 40. C'est la seule série du plan où le réglage de production rend
l'observation coûteuse.
### 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 — CLOSE le 2026-08-29, apres quatre obstacles
L'APK signe existe : `CN=R Zakarya, ..., C=DZ`, 12,1 Mo. Ce chemin n'avait
jamais tourne, et il a butte quatre fois — toutes sur des choses que **seule une
release partant d'un terrain vierge pouvait rencontrer**. Le debogage vit sur un
arbre constitue au fil des mois, ou tout cela existait deja.
1. **Le dossier de mise en scene n'existait pas.** Les trois copies du script s'y
deversent avant l'etape Deploy qui le creuse. Corrige : on le cree d'abord.
2. **Le volume etait plein.** Debug et Release cote a cote ne tiennent pas sur
ce disque ; le `strip` echouait a mi-fichier et empoisonnait tout ce qui
suivait. A surveiller avant chaque release.
3. **aapt refusait cinq doublons a la racine de `res\`**, deposes par le
deploiement de Delphi alors que les vrais sont dans `drawable\`. Corrige :
on les ecarte avant l'appel.
4. **Les ressources generees n'etaient pas mises en place.** Le Build les depose
a plat dans `Android64\<Config>\` et le Deploy ne les range dans `res\`
qu'en DEBUG — d'ou `@style/AppTheme` introuvable. Corrige : le script les
range lui-meme, avant les notres pour que notre `colors.xml` garde le dernier
mot.
**Ce qu'il faut verifier a chaque release :**
1. La sortie doit afficher `Signer #1 certificate DN: CN=R Zakarya`. Si elle dit
`CN=Android Debug`, la release ne vaut rien.
2. Elle ne s'installe pas d'elle-meme, et c'est voulu : une signature differente
obligerait a desinstaller d'abord, ce qui effacerait le coffre local.
3. La section **Debug** ne doit pas etre atteignable. **Mesure exacte** : la
chaine `DEBUG: local vault dropped` doit etre absente du `.so` — en UTF-16LE,
comme toutes les chaines Delphi ; la chercher en ASCII ne prouve rien.
Attention, `wipelocal` et `showDebug` s'y trouvent dans les DEUX
configurations : ils viennent de la page HTML embarquee, qui porte toujours
le bouton. Ce qui manque en release, c'est l'appel `PM.showDebug()` et le
gestionnaire du pont — la section est donc inatteignable et sans effet, pas
absente.
4. **Le mot de passe du magasin ne se retrouve pas.** Un JKS ne s'ouvre pas sans
lui. Aujourd'hui le recreer ne coute rien ; une fois une version distribuee,
le perdre voudra dire ne plus jamais pouvoir mettre a jour l'application sur
ces appareils - Android refuse une mise a jour dont la signature a change.
---
## 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é.