feat(android): tell when the repository has moved, without adopting it
A probe every ten minutes compares the remote fingerprint to ours and lights an indicator. It does not pull: adopting on its own while someone is reading an entry would change it under their eyes. The user decides when. The body is downloaded anyway - Gitea's contents API cannot answer without it - but it is thrown away undecrypted: no derivation, no write, just a string comparison. The cost is network, not time or battery. It rides the idle timer rather than a second one. That timer already beats every 15 seconds for auto-lock; counting its beats costs an integer, where another timer costs an object, a lifecycle, and one more chance to forget it on shutdown. Two indicators, two colours, two gestures. Orange: things wait HERE and the repository does not know. Blue: the repository holds things we do not have. One dot for both would say "something is wrong" without saying what - and one is resolved by publishing, the other by fetching.
This commit is contained in:
+17
-3
@@ -106,12 +106,26 @@ prouve rien.
|
|||||||
5. Touche **retour** pendant une sélection → elle s'annule **sans verrouiller**.
|
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.
|
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
|
1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans
|
||||||
*Téléchargements* sous son nom, accents et arabe compris.
|
*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é.
|
1. Poser un code, ouvrir le coffre, fermer, rouvrir → il est demandé.
|
||||||
2. Code faux → *« Wrong unlock code »*, et **Open vault reste offert** (le
|
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* →
|
4. Contre-épreuve : taper n'importe quoi dans le champ du code puis *Sync* →
|
||||||
le code **ne doit pas changer**.
|
le code **ne doit pas changer**.
|
||||||
|
|
||||||
### G. Empaquetage
|
### H. Empaquetage
|
||||||
|
|
||||||
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
|
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
|
||||||
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android
|
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android
|
||||||
|
|||||||
@@ -181,12 +181,21 @@
|
|||||||
.btn.ico-btn:active { opacity: .45; }
|
.btn.ico-btn:active { opacity: .45; }
|
||||||
/* Le point des modifications en attente. Sur le cadenas parce qu'il est
|
/* 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. */
|
toujours a l'ecran de la liste, et qu'il parle deja de l'etat du coffre. */
|
||||||
.ico.pending, .btn.pending { position: relative; }
|
/* DEUX TEMOINS, DEUX COULEURS, DEUX GESTES.
|
||||||
.ico.pending::after, .btn.pending::after {
|
Orange : des choses attendent ICI et le depot les ignore.
|
||||||
content: ''; position: absolute; top: 6px; right: 6px;
|
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%;
|
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); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
/* L'ENREGISTREMENT DURE UNE A DEUX SECONDES, et le bouton doit le dire. Le
|
/* 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
|
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('lock').classList.toggle('pending', !!on);
|
||||||
document.getElementById('btnSync').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) {
|
setBusy: function (on) {
|
||||||
if (on) document.getElementById('busy').firstChild.innerHTML =
|
if (on) document.getElementById('busy').firstChild.innerHTML =
|
||||||
ico('loader', 'Working') + '<span>Please wait...</span>';
|
ico('loader', 'Working') + '<span>Please wait...</span>';
|
||||||
|
|||||||
+83
@@ -85,6 +85,12 @@ const
|
|||||||
// par un autre chemin et n'attend jamais.
|
// par un autre chemin et n'attend jamais.
|
||||||
REFRESH_MIN_MINUTES = 15;
|
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
|
// 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
|
// 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
|
// l'un ni l'autre n'a sa place dans un depot, meme sans remote. Absents apres
|
||||||
@@ -150,6 +156,10 @@ type
|
|||||||
FKeyIters: Integer;
|
FKeyIters: Integer;
|
||||||
/// Des modifications sont rangees ici sans avoir atteint le depot.
|
/// Des modifications sont rangees ici sans avoir atteint le depot.
|
||||||
FPending: Boolean;
|
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
|
/// 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 -
|
/// TVault : celle-ci perdrait tout ce que le telephone ne lit pas -
|
||||||
/// le contenu des pieces jointes, l'avatar, les tombstones - c'est
|
/// 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
|
/// Publie ce qui a ete enregistre hors connexion. Sans effet s'il n'y a
|
||||||
/// rien en attente.
|
/// rien en attente.
|
||||||
procedure PublishPending;
|
procedure PublishPending;
|
||||||
|
/// Demande au depot s'il a bouge, sans rien adopter.
|
||||||
|
procedure ProbeRemote;
|
||||||
procedure ReopenEntryByUuid(const AUuid: string);
|
procedure ReopenEntryByUuid(const AUuid: string);
|
||||||
procedure OnClipTimer(Sender: TObject);
|
procedure OnClipTimer(Sender: TObject);
|
||||||
procedure OnDeadline(Sender: TObject);
|
procedure OnDeadline(Sender: TObject);
|
||||||
@@ -920,6 +932,7 @@ begin
|
|||||||
begin
|
begin
|
||||||
FETag := LPut.ETag;
|
FETag := LPut.ETag;
|
||||||
LPublished := True;
|
LPublished := True;
|
||||||
|
FRemoteAhead := False;
|
||||||
Log('save: published, new sha ' + LPut.ETag);
|
Log('save: published, new sha ' + LPut.ETag);
|
||||||
Break;
|
Break;
|
||||||
end;
|
end;
|
||||||
@@ -1145,6 +1158,7 @@ begin
|
|||||||
if LDone then
|
if LDone then
|
||||||
begin
|
begin
|
||||||
FPending := False;
|
FPending := False;
|
||||||
|
FRemoteAhead := False;
|
||||||
SetPendingFlag(False);
|
SetPendingFlag(False);
|
||||||
AdoptVault(LMerged);
|
AdoptVault(LMerged);
|
||||||
Log('pending changes published, new sha ' + FETag);
|
Log('pending changes published, new sha ' + FETag);
|
||||||
@@ -1937,6 +1951,11 @@ begin
|
|||||||
|
|
||||||
WebCall(Format('PM.setPending(%s)',
|
WebCall(Format('PM.setPending(%s)',
|
||||||
[LowerCase(BoolToStr(FPending, True))]));
|
[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
|
try
|
||||||
LSw := TStopwatch.StartNew;
|
LSw := TStopwatch.StartNew;
|
||||||
AdoptVault(LPlain);
|
AdoptVault(LPlain);
|
||||||
@@ -2298,6 +2317,8 @@ begin
|
|||||||
// hors connexion disparaissait alors sans un mot, et c'est exactement
|
// hors connexion disparaissait alors sans un mot, et c'est exactement
|
||||||
// ce que l'enregistrement hors ligne promet d'eviter.
|
// 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.
|
// On garde le drapeau : fondre ne publie pas.
|
||||||
if FPending and (FPlain <> '') then
|
if FPending and (FPlain <> '') then
|
||||||
begin
|
begin
|
||||||
@@ -2473,7 +2494,69 @@ begin
|
|||||||
begin
|
begin
|
||||||
Log('Locked after ' + AUTOLOCK_MINUTES.ToString + ' min idle');
|
Log('Locked after ' + AUTOLOCK_MINUTES.ToString + ' min idle');
|
||||||
LockVault;
|
LockVault;
|
||||||
|
Exit;
|
||||||
end;
|
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;
|
end;
|
||||||
|
|
||||||
procedure TForm8.Status(const AMsg: string);
|
procedure TForm8.Status(const AMsg: string);
|
||||||
|
|||||||
Reference in New Issue
Block a user