diff --git a/android-client/TESTS.md b/android-client/TESTS.md index a8edda2..23b2505 100644 --- a/android-client/TESTS.md +++ b/android-client/TESTS.md @@ -106,12 +106,26 @@ prouve rien. 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. -### E. Pièces jointes +### 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 ; les confondre dirait + « quelque chose ne va pas » sans dire quoi. + +### F. 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. -### F. Code de déverrouillage +### G. Code de déverrouillage 1. Poser un code, ouvrir le coffre, fermer, rouvrir → il est demandé. 2. Code faux → *« Wrong unlock code »*, et **Open vault reste offert** (le @@ -121,7 +135,7 @@ prouve rien. 4. Contre-épreuve : taper n'importe quoi dans le champ du code puis *Sync* → le code **ne doit pas changer**. -### G. Empaquetage +### H. Empaquetage 1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher `Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android diff --git a/android-client/web/app.html b/android-client/web/app.html index ff01265..f9470ce 100644 --- a/android-client/web/app.html +++ b/android-client/web/app.html @@ -181,12 +181,21 @@ .btn.ico-btn:active { opacity: .45; } /* Le point des modifications en attente. Sur le cadenas parce qu'il est toujours a l'ecran de la liste, et qu'il parle deja de l'etat du coffre. */ - .ico.pending, .btn.pending { position: relative; } - .ico.pending::after, .btn.pending::after { - content: ''; position: absolute; top: 6px; right: 6px; + /* DEUX TEMOINS, DEUX COULEURS, DEUX GESTES. + Orange : des choses attendent ICI et le depot les ignore. + Bleu : le depot porte des choses que nous n'avons pas. + Les confondre en un seul point dirait "quelque chose ne va pas" sans dire + quoi - or l'un se resout en publiant, l'autre en tirant. */ + .ico.pending, .btn.pending, .ico.ahead, .btn.ahead { position: relative; } + .ico.pending::after, .btn.pending::after, + .ico.ahead::before, .btn.ahead::before { + content: ''; position: absolute; top: 6px; width: 9px; height: 9px; border-radius: 50%; - background: #ffb44d; box-shadow: 0 0 0 2px var(--bg-top); + box-shadow: 0 0 0 2px var(--bg-top); } + .ico.pending::after, .btn.pending::after { right: 6px; background: #ffb44d; } + /* Decale, pour que les deux se voient ensemble quand les deux sont vrais. */ + .ico.ahead::before, .btn.ahead::before { right: 20px; background: #5ec8ff; } @keyframes spin { to { transform: rotate(360deg); } } /* L'ENREGISTREMENT DURE UNE A DEUX SECONDES, et le bouton doit le dire. Le mot "Saving..." le disait ; une coche immobile ne dirait rien, et c'est @@ -1477,6 +1486,12 @@ var PM = { document.getElementById('lock').classList.toggle('pending', !!on); document.getElementById('btnSync').classList.toggle('pending', !!on); }, + /// LE DEPOT A BOUGE. Signale, jamais adopte de soi-meme : tirer pendant que + /// quelqu'un lit une entree la ferait changer sous ses yeux. + setRemoteAhead: function (on) { + document.getElementById('lock').classList.toggle('ahead', !!on); + document.getElementById('btnSync').classList.toggle('ahead', !!on); + }, setBusy: function (on) { if (on) document.getElementById('busy').firstChild.innerHTML = ico('loader', 'Working') + 'Please wait...'; diff --git a/mainApk.pas b/mainApk.pas index 7dc3cc2..2e5e9e9 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -85,6 +85,12 @@ const // par un autre chemin et n'attend jamais. REFRESH_MIN_MINUTES = 15; + // Verification du depot : un battement du minuteur d'inactivite vaut 15 s, + // donc quarante battements font dix minutes. Assez rare pour ne rien peser + // sur une liaison mobile, assez frequent pour qu'on l'apprenne avant d'avoir + // modifie la meme entree des deux cotes. + PROBE_EVERY_TICKS = 40; + // FIRSTRUN_TOKEN et TEST_PWD viennent d'un fichier LOCAL, hors depot : le // jeton authentifie l'appareil, le mot de passe ouvre le coffre entier. Ni // l'un ni l'autre n'a sa place dans un depot, meme sans remote. Absents apres @@ -150,6 +156,10 @@ type FKeyIters: Integer; /// Des modifications sont rangees ici sans avoir atteint le depot. FPending: Boolean; + /// Le depot porte quelque chose que nous n'avons pas. + FRemoteAhead: Boolean; + /// Ticks du minuteur d'inactivite depuis la derniere verification du depot. + FTicksSinceProbe: Integer; /// L'INSTANTANE BRUT, tel qu'il a ete recu. Pas une reserialisation de /// TVault : celle-ci perdrait tout ce que le telephone ne lit pas - /// le contenu des pieces jointes, l'avatar, les tombstones - c'est @@ -291,6 +301,8 @@ type /// Publie ce qui a ete enregistre hors connexion. Sans effet s'il n'y a /// rien en attente. procedure PublishPending; + /// Demande au depot s'il a bouge, sans rien adopter. + procedure ProbeRemote; procedure ReopenEntryByUuid(const AUuid: string); procedure OnClipTimer(Sender: TObject); procedure OnDeadline(Sender: TObject); @@ -920,6 +932,7 @@ begin begin FETag := LPut.ETag; LPublished := True; + FRemoteAhead := False; Log('save: published, new sha ' + LPut.ETag); Break; end; @@ -1145,6 +1158,7 @@ begin if LDone then begin FPending := False; + FRemoteAhead := False; SetPendingFlag(False); AdoptVault(LMerged); Log('pending changes published, new sha ' + FETag); @@ -1937,6 +1951,11 @@ begin WebCall(Format('PM.setPending(%s)', [LowerCase(BoolToStr(FPending, True))])); + WebCall(Format('PM.setRemoteAhead(%s)', + [LowerCase(BoolToStr(FRemoteAhead, True))])); + // Le compteur repart de zero : on vient de regarder le depot, ou de s'en + // passer sciemment. Le sonder dans la foulee ne dirait rien de neuf. + FTicksSinceProbe := 0; try LSw := TStopwatch.StartNew; AdoptVault(LPlain); @@ -2298,6 +2317,8 @@ begin // hors connexion disparaissait alors sans un mot, et c'est exactement // ce que l'enregistrement hors ligne promet d'eviter. // + // Le depot vient d'etre lu : ce qu'il portait est ici. + FRemoteAhead := False; // On garde le drapeau : fondre ne publie pas. if FPending and (FPlain <> '') then begin @@ -2473,7 +2494,69 @@ begin begin Log('Locked after ' + AUTOLOCK_MINUTES.ToString + ' min idle'); LockVault; + Exit; end; + + // LE MEME MINUTEUR, PAS UN SECOND. Il bat deja toutes les 15 s pour + // l'inactivite ; compter ses battements coute un entier, la ou un timer de + // plus coute un objet, un cycle de vie et une occasion de l'oublier a l'arret. + Inc(FTicksSinceProbe); + if FTicksSinceProbe >= PROBE_EVERY_TICKS then + begin + FTicksSinceProbe := 0; + ProbeRemote; + end; +end; + +/// DEMANDER SI LE DEPOT A BOUGE, SANS RIEN ADOPTER. +/// +/// Adopter de soi-meme pendant que quelqu'un lit une entree la ferait changer +/// sous ses yeux. On se contente donc de comparer les empreintes et d'allumer +/// un temoin : c'est l'utilisateur qui decide quand tirer. +/// +/// LE CORPS EST TELECHARGE QUAND MEME - l'API des contenus de Gitea ne sait +/// pas repondre sans lui - mais il est jete sans etre dechiffre : pas de +/// derivation, pas d'ecriture, seulement une comparaison de chaines. Le prix +/// est en reseau, pas en temps ni en batterie. +procedure TForm8.ProbeRemote; +begin + if FBusy or (FUrl = '') or (FToken = '') then Exit; + // Un coffre ne ici n'a pas de depot a interroger. + if HasLocalContainer then Exit; + if FETag = '' then Exit; + + TThread.CreateAnonymousThread( + procedure + var + LClient: TSyncClient; + LGet: TSyncResult; + LAhead: Boolean; + begin + LAhead := False; + LClient := TSyncClient.Create(FUrl, FToken); + try + try + LGet := LClient.Fetch(FETag); + LAhead := (LGet.Status = ssOK) and (LGet.ETag <> '') + and not SameText(LGet.ETag.Trim, FETag.Trim); + except + // Hors reseau : ce n'est pas une nouvelle du depot, c'est une absence + // de nouvelle. Le temoin ne change pas. + Exit; + end; + finally + LClient.Free; + end; + if not LAhead then Exit; + TThread.Queue(nil, + procedure + begin + if FRemoteAhead then Exit; // deja dit + FRemoteAhead := True; + Log('remote has moved - sync to bring it in'); + WebCall('PM.setRemoteAhead(true)'); + end); + end).Start; end; procedure TForm8.Status(const AMsg: string);