feat(android): show first, sync after

The local vault now opens by itself at launch and the GET runs behind it,
silently. A 304 shows nothing; a network failure shows nothing either -
offline is a normal state, and an alert on every launch only teaches people
to dismiss alerts. Only a new snapshot manifests, by refreshing the list.

That silence is what puts the sync password inside the Keystore-encrypted
file next to the vault: without it a new snapshot cannot be opened, so every
edit made on the desktop would raise a password prompt at launch. The file
already holds every password in plaintext; this one adds almost nothing to
its exposure. A typed password still wins over the stored one, which is how
you fix one that no longer works.

Adopting a snapshot while the app is already open rebuilds the list in place
instead of navigating - mandatory even from the detail screen, since list
rows carry indices into the old vault and a tap would open the wrong entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 11:14:44 +01:00
parent ba0d3c1d3c
commit 3bb6f6f903
4 changed files with 119 additions and 38 deletions
+72 -22
View File
@@ -75,6 +75,10 @@ type
// economiser un telechargement pour se retrouver sans donnees.
FETag: string;
FBusy: Boolean;
// Mot de passe de sync tel qu'il a ete range avec le coffre. Il rend le
// rafraichissement de fond silencieux : sans lui, chaque modification
// faite sur le poste ferait apparaitre une invite au lancement.
FSyncPwd: string;
// --- ecran 1 : deverrouillage
FLayUnlock: TLayout;
@@ -126,7 +130,11 @@ type
// procedure imbriquee (E2555), et le rappel de la biometrie en est une.
procedure OpenLocalNow;
procedure DoSync(Sender: TObject);
procedure FetchAndDecrypt(const AUrl, ATok, APwd: string);
/// ASilent : rafraichissement de fond. Le coffre local est deja affiche,
/// donc un echec reseau ne doit rien reclamer a l'utilisateur - il garde
/// ce qu'il a. Seul un instantane NOUVEAU se voit.
procedure FetchAndDecrypt(const AUrl, ATok, APwd: string;
ASilent: Boolean = False);
procedure AdoptVault(const APlainJson: string);
procedure LockVault;
@@ -202,6 +210,12 @@ begin
LEvents.SetApplicationEventHandler(HandleAppEvent);
ShowUnlock;
// Ouverture automatique du coffre local. Differee apres la construction :
// l'invite biometrique s'afficherait sinon avant la premiere peinture de la
// fenetre, sur un fond vide.
if HasVault then
TThread.ForceQueue(nil, procedure begin DoOpenLocal(nil); end);
end;
destructor TForm8.Destroy;
@@ -635,18 +649,31 @@ begin
FVault.LoadFromJson(APlainJson);
Log(' ' + FVault.LiveCount.ToString + ' entrees, ' +
FVault.TrashCount.ToString + ' en corbeille');
ShowList;
// Deja dans l'application : reconstruire la liste SANS y ramener
// l'utilisateur. Obligatoire meme s'il est dans le detail - les lignes de
// la liste portent des indices vers l'ancien coffre, et un appui ouvrirait
// la mauvaise entree.
if FLayList.Visible or FLayDetail.Visible then
OnSearchChanged(nil)
else
ShowList;
end;
procedure TForm8.OpenLocalNow;
var
LPlain, LETag: string;
LPlain, LETag, LPwd: string;
begin
if LoadVault(LPlain, LETag) then
if LoadVault(LPlain, LETag, LPwd) then
begin
FETag := LETag;
if LPwd <> '' then FSyncPwd := LPwd;
try
AdoptVault(LPlain);
// 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
FetchAndDecrypt(FUrl.Text.Trim, FToken.Text.Trim, FSyncPwd, True);
except
on E: Exception do
begin
@@ -705,7 +732,10 @@ begin
// thread de travail est un plantage differe, pas une erreur de compilation.
LUrl := FUrl.Text.Trim;
LTok := FToken.Text.Trim;
// Ce qui est tape l'emporte : c'est ainsi qu'on corrige un mot de passe
// range qui ne convient plus.
LPwd := FPwd.Text;
if LPwd = '' then LPwd := FSyncPwd;
if LPwd = '' then
begin
Status('Mot de passe de sync requis');
@@ -715,21 +745,22 @@ begin
FetchAndDecrypt(LUrl, LTok, LPwd);
end;
procedure TForm8.FetchAndDecrypt(const AUrl, ATok, APwd: string);
procedure TForm8.FetchAndDecrypt(const AUrl, ATok, APwd: string;
ASilent: Boolean);
begin
SetBusy(True);
Status('Telechargement...');
if not ASilent then Status('Telechargement...');
TThread.CreateAnonymousThread(
procedure
var
LClient: TSyncClient;
LRes: TSyncResult;
LPlain, LCached, LCachedETag: string;
LPlain, LCached, LCachedETag, LCachedPwd: string;
LSw: TStopwatch;
begin
// Le coffre local d'abord : s'il est la, on connait deja l'ETag et on
// evite les 4 secondes de derivation quand rien n'a change.
if (FETag = '') and LoadVault(LCached, LCachedETag) then
if (FETag = '') and LoadVault(LCached, LCachedETag, LCachedPwd) then
FETag := LCachedETag;
LClient := TSyncClient.Create(AUrl, ATok);
@@ -760,10 +791,11 @@ begin
procedure
begin
if FVault.Count > 0 then
begin
Log(' 304 : inchange, coffre deja ouvert');
ShowList;
end
// Rien a faire : ce qui est affiche est deja a jour. En
// rafraichissement de fond c'est le cas courant, et il
// doit passer inapercu - surtout pas de retour a la liste
// alors que l'utilisateur consulte une entree.
Log(' 304 : inchange, coffre deja ouvert')
else if LCached <> '' then
begin
Log(' 304 : inchange, coffre servi depuis le cache');
@@ -778,7 +810,8 @@ begin
// Un ETag sans clair correspondant : l'invariant est rompu.
// On le jette pour que l'appel suivant retelecharge.
FETag := '';
Status('Cache absent - appuyer a nouveau pour telecharger');
if not ASilent then
Status('Cache absent - appuyer a nouveau pour telecharger');
end;
end);
Exit;
@@ -787,21 +820,27 @@ begin
begin
// Le serveur repond pareil dans les deux cas, par conception.
Log(' 404 : pas d''instantane OU jeton invalide');
TThread.Queue(nil, procedure
begin Status('Rien a telecharger (404)'); end);
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Rien a telecharger (404)'); end);
Exit;
end;
ssRefused, ssHttpError, ssNetworkError:
begin
// En fond, un echec reseau ne se signale pas : le coffre local
// est affiche, hors ligne est un etat normal, et une alerte ici
// apprendrait a l'utilisateur a ignorer les alertes.
Log(' echec : ' + LRes.Error);
TThread.Queue(nil, procedure
begin Status('Echec reseau'); end);
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Echec reseau'); end);
Exit;
end;
end;
Log(' recu ' + Length(LRes.Container).ToString + ' octets');
TThread.Queue(nil, procedure begin Status('Dechiffrement...'); end);
if not ASilent then
TThread.Queue(nil, procedure begin Status('Dechiffrement...'); end);
LSw := TStopwatch.StartNew;
// Volontairement AVANT de retenir l'ETag : un echec ici doit laisser
@@ -817,7 +856,9 @@ begin
// True : on DEMANDE la protection par authentification. PMA.Store
// retombe tout seul sur une cle simple si l'appareil n'a ni
// empreinte ni code, plutot que de renoncer au cache.
SaveVault(LPlain, FETag, True);
// 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, True);
if VaultNeedsAuth then
Log(' cache ecrit - protege par biometrie/code')
else
@@ -832,10 +873,13 @@ begin
TThread.Queue(nil,
procedure
begin
FSyncPwd := APwd; // il vient d'ouvrir un conteneur : il est bon
try
AdoptVault(LPlain);
except
on E: Exception do Status('Instantane illisible : ' + E.Message);
on E: Exception do
if not ASilent then
Status('Instantane illisible : ' + E.Message);
end;
end);
except
@@ -844,8 +888,14 @@ begin
Log('ERREUR ' + E.ClassName + ' : ' + E.Message);
// Le cas de loin le plus frequent, et le message par defaut ne le
// dit pas : GCM echoue a la verification quand la cle est fausse.
TThread.Queue(nil, procedure
begin Status('Echec - mot de passe de sync incorrect ?'); end);
//
// En fond, cela veut dire que le mot de passe range ne convient
// plus - le poste a change de mot de passe de sync. On ne le jette
// PAS : il faudra le retaper, mais garder l'ancien laisse au moins
// le champ prerempli, et le coffre local reste consultable.
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Echec - mot de passe de sync incorrect ?'); end);
end;
end;
finally