fix(android): offline changes survive a refresh, and a restart

Two defects in what offline saving had just introduced, both losing data
silently - found by re-reading rather than by testing, since the device was
away.

The background refresh REPLACED the cache with the repository's version. An
edit made offline vanished at the next fetch, without a word, which is
exactly what offline saving promises to prevent. It merges now, and keeps the
flag: merging is not publishing.

And the flag lived only in memory. An application killed between the offline
save and the publish forgot it had anything to send - and the next refresh
then clobbered the cache. It is a file now, read before any refresh can run.

A file rather than a field of the sealed cache: the flag carries no secret,
its whole content is yes or no, and the cache's signature is already nine
parameters long.
This commit is contained in:
r-zakarya
2026-08-22 15:15:21 +01:00
parent a24b18e90f
commit af2a0dfed8
2 changed files with 55 additions and 0 deletions
+36
View File
@@ -151,6 +151,20 @@ procedure SaveLocalContainer(const AContainer: string);
/// c'est le geste qui fait passer l'autorite au depot, et il est sans retour.
procedure DropLocalContainer;
/// DES MODIFICATIONS ATTENDENT D'ETRE PUBLIEES.
///
/// UN FICHIER A PART, ET NON UN CHAMP DU CACHE : ce drapeau ne porte aucun
/// secret - sa seule information est "oui" ou "non" -, il n'a donc rien a
/// faire dans un blob chiffre dont l'ecriture coute une derivation et dont la
/// signature est deja longue de neuf parametres.
///
/// IL DOIT SURVIVRE A UN REDEMARRAGE. En memoire seulement, une application
/// tuee entre l'enregistrement hors ligne et la publication oubliait qu'elle
/// avait quelque chose a envoyer - et le rafraichissement suivant remplacait
/// le cache par la version du depot, effacant la modification sans un mot.
function HasPendingFlag: Boolean;
procedure SetPendingFlag(AOn: Boolean);
/// Chemin du fichier, pour le journal et les tests.
function VaultPath: string;
@@ -184,6 +198,7 @@ uses
const
VAULT_FILE = 'vault.bin';
LOCAL_FILE = 'vault-local.json';
PENDING_FILE = 'pending.flag';
SETTINGS_FILE = 'settings.bin';
// Un alias par schema : la contrainte d'authentification est figee a la
// creation de la cle et ne peut pas etre ajoutee apres coup.
@@ -259,6 +274,27 @@ begin
TFile.Delete(LocalContainerPath);
end;
function PendingPath: string;
begin
Result := TPath.Combine(TPath.GetHomePath, PENDING_FILE);
end;
function HasPendingFlag: Boolean;
begin
Result := TFile.Exists(PendingPath);
end;
procedure SetPendingFlag(AOn: Boolean);
begin
try
if AOn then TFile.WriteAllText(PendingPath, '1')
else if TFile.Exists(PendingPath) then TFile.Delete(PendingPath);
except
// Un drapeau qu'on n'a pas pu ecrire ne doit pas faire echouer un
// enregistrement qui, lui, a reussi.
end;
end;
function VaultPath: string;
begin
// GetHomePath sur Android = /data/data/<paquet>/files, prive a l'app.