fix(android): opening the vault no longer calls the server every time

Asked plainly by the user: why does open trigger a sync? Because
OpenLocalNow displays the cache and then refreshes in the background, so a
change made on the desktop shows up without anyone thinking about it. Sound
intent, applied on EVERY open - including a lock followed by a re-open three
seconds later, which cannot learn anything new. Measured cost of one such
pointless refresh: 1 s of download and 5.5 s of key derivation.

A minimum interval between two background refreshes fixes that, whatever else
is wrong: the desktop does not publish several times a minute, and looking at
most once a quarter of an hour is enough for a change to arrive unnoticed.
Pressing Sync goes through another path and never waits.

The sha comparison that should already have made an unchanged refresh cheap is
still not firing - two fingerprints print identically yet compare unequal. It
is now compared with SameText on trimmed values, which cannot confuse two
distinct snapshots since a hex digest never differs by case or whitespace
alone, and the log reports both lengths so the invisible character shows up on
the next real refresh.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-17 16:15:07 +01:00
parent f97e44768a
commit 65f5659d06
2 changed files with 43 additions and 2 deletions
+9 -1
View File
@@ -207,7 +207,15 @@ begin
// Meme sha : le contenu est celui qu'on detient deja. On rend la
// meme reponse qu'un 304 pour que l'appelant epargne la derivation,
// et on jette le conteneur - il ne servirait a rien.
else if (AKnownETag <> '') and (Result.ETag = AKnownETag) then
//
// COMPARAISON INSENSIBLE A LA CASSE ET AUX BLANCS. Une empreinte
// hexadecimale ne peut pas differer seulement par la casse ou par
// un espace : trancher ainsi ne peut donc pas confondre deux
// instantanes distincts, alors qu'une egalite stricte se laisse
// battre par un caractere invisible - un guillemet d'en-tete, une
// fin de ligne - et fait payer une derivation complete pour rien.
else if (AKnownETag <> '') and
SameText(Result.ETag.Trim, AKnownETag.Trim) then
begin
Result.Status := ssNotModified;
Result.Container := '';
+34 -1
View File
@@ -72,6 +72,19 @@ const
// qu'un telephone repose ouvert.
BACKGROUND_GRACE_SECONDS = 30;
// Duree minimale entre deux rafraichissements de fond.
//
// L'ouverture du coffre en declenchait un CHAQUE FOIS. Verrouiller puis
// rouvrir trois secondes plus tard rappelait donc le serveur, qui ne pouvait
// rien apprendre de neuf : un telechargement complet, et une derivation de
// cle entiere des que l'empreinte ne concordait pas. Le poste ne publie pas
// plusieurs fois par minute ; consulter au plus une fois par quart d'heure
// suffit a ce que la modification arrive sans qu'on y pense.
//
// Ne concerne QUE le rafraichissement automatique. Un appui sur Sync passe
// par un autre chemin et n'attend jamais.
REFRESH_MIN_MINUTES = 15;
// 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
@@ -135,6 +148,8 @@ type
FLastActivity: TDateTime;
/// Quand l'application est passee derriere. 0 = elle est au premier plan.
FBackgroundAt: TDateTime;
/// Dernier rafraichissement de fond. 0 = jamais, donc le premier a lieu.
FLastRefresh: TDateTime;
procedure Touch;
procedure OnIdleTimer(Sender: TObject);
@@ -629,8 +644,17 @@ begin
// Afficher d'abord, synchroniser ensuite : le coffre est a l'ecran, le
// reseau se debrouille derriere. Sans mot de passe range, on ne peut
// rien ouvrir de nouveau - inutile d'aller voir.
if FSyncPwd <> '' then
if FSyncPwd = '' then
Log(' no stored sync password - no background refresh')
else if (FLastRefresh > 0) and
(MinutesBetween(Now, FLastRefresh) < REFRESH_MIN_MINUTES) then
Log(Format(' refreshed %d min ago - skipping (min %d)',
[MinutesBetween(Now, FLastRefresh), REFRESH_MIN_MINUTES]))
else
begin
FLastRefresh := Now;
FetchAndDecrypt(FUrl, FToken, FSyncPwd, True);
end;
except
on E: Exception do
begin
@@ -880,6 +904,14 @@ begin
end;
Log(' received ' + Length(LRes.Container).ToString + ' bytes');
// LES DEUX EMPREINTES COTE A COTE. Sans cela on ne peut pas trancher
// entre « le distant a change » et « la comparaison ne se declenche
// pas », et les deux se ressemblent parfaitement de l'exterieur.
// LES LONGUEURS AUTANT QUE LES VALEURS. Deux empreintes s'affichaient
// identiques alors que l'egalite les separait : seul un caractere
// invisible peut faire cela, et il ne se voit qu'au compte.
Log(Format(' remote sha: %s (%d chars, known %d)',
[LRes.ETag, Length(LRes.ETag), Length(FETag)]));
// LES COORDONNEES SONT RETENUES ICI, avant le dechiffrement. Un 200
// avec un conteneur en main prouve l'adresse ET le jeton ; le mot de
@@ -915,6 +947,7 @@ begin
// Le mot de passe part avec le coffre : c'est lui qui permettra au
// prochain lancement de rafraichir sans rien demander.
SaveVault(LPlain, FETag, APwd, LCanAuth);
Log(' cache now holds sha: ' + FETag);
if VaultNeedsAuth then
Log(' cache written - protected by biometrics/PIN')
else if LCanAuth then