From af2a0dfed85886bcb25b30c13055926f7a0d2c9c Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:15:21 +0100 Subject: [PATCH] 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. --- android-client/PMA.Store.pas | 36 ++++++++++++++++++++++++++++++++++++ mainApk.pas | 19 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/android-client/PMA.Store.pas b/android-client/PMA.Store.pas index 837d88e..4d19337 100644 --- a/android-client/PMA.Store.pas +++ b/android-client/PMA.Store.pas @@ -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//files, prive a l'app. diff --git a/mainApk.pas b/mainApk.pas index dcbff20..7dc3cc2 100644 --- a/mainApk.pas +++ b/mainApk.pas @@ -390,6 +390,9 @@ begin IFMXApplicationEventService, LEvents) then LEvents.SetApplicationEventHandler(HandleAppEvent); + // AVANT TOUT RAFRAICHISSEMENT : sans cela le premier tirage remplacerait le + // cache par la version du depot, et ce qui attend ici serait perdu. + FPending := HasPendingFlag; PushConfig; ShowUnlock; {$IFNDEF DEBUG} @@ -976,6 +979,7 @@ begin end; AdoptVault(LNew); FPending := not LPublished; + SetPendingFlag(FPending); WebCall(Format('PM.setPending(%s)', [LowerCase(BoolToStr(FPending, True))])); if LPublished then @@ -1141,6 +1145,7 @@ begin if LDone then begin FPending := False; + SetPendingFlag(False); AdoptVault(LMerged); Log('pending changes published, new sha ' + FETag); WebCall('PM.setPending(false)'); @@ -1930,6 +1935,8 @@ begin end; end; + WebCall(Format('PM.setPending(%s)', + [LowerCase(BoolToStr(FPending, True))])); try LSw := TStopwatch.StartNew; AdoptVault(LPlain); @@ -2286,6 +2293,18 @@ begin FETag := LRes.ETag; Log(' decrypted in ' + LSw.ElapsedMilliseconds.ToString + ' ms'); + // CE QUI ATTEND ICI NE SE FAIT PAS ECRASER. Le rafraichissement + // remplacait le cache par la version du depot : une modification faite + // hors connexion disparaissait alors sans un mot, et c'est exactement + // ce que l'enregistrement hors ligne promet d'eviter. + // + // On garde le drapeau : fondre ne publie pas. + if FPending and (FPlain <> '') then + begin + LPlain := MergeSnapshots(FPlain, LPlain); + Log(' merged with what is waiting here - still to publish'); + end; + // Ecrit APRES un dechiffrement reussi seulement, et avec son ETag : // les deux doivent rester coherents. try