feat(android): attachments can be saved, section 3 item 1 is done

Two questions, and the code was only the second one.

WHERE THE FILE LANDS. The private directory is useless - no file manager
opens it and it leaves with the uninstall - and direct external storage has
been closed since Android 10. Of the three remaining doors, MediaStore ->
Downloads is the only one needing neither a permission nor an extra screen:
since API 29 an app may write what it inserted itself. The destination picker
would need an activity result, which FMX makes painful; sharing would need a
FileProvider in the manifest. Below API 29 it would take
WRITE_EXTERNAL_STORAGE - one of the ten permissions just removed - so it
refuses and says so.

WHERE THE CONTENT IS. TVault drops it on purpose: a single attachment took
the cache from 13 to 174 Ko. It is re-read from the raw snapshot, which has
been in memory ever since saving stopped fetching before publishing. The same
data now serves two needs that had nothing to do with each other.

Decoding and writing happen off the main thread, under the waiting veil: a
few megabytes would freeze the screen, and the veil would not even paint.

What leaves the application leaves its protection: the file lands in the
clear in Downloads. The confirmation names the place rather than saying
"saved".

Release configuration compiles too - checked, since that is what section 5
caught out.
This commit is contained in:
r-zakarya
2026-08-22 00:41:05 +01:00
parent cb2e2c7a5f
commit 6b3001666e
7 changed files with 319 additions and 6 deletions
+24 -3
View File
@@ -41,7 +41,10 @@ function DetailToJson(const AEntry: TVaultEntry): string;
/// page renvoie un rang, jamais une valeur, et Delphi relit le clair chez lui.
/// Faux si le rang ne designe rien.
function DetailRowAt(const AEntry: TVaultEntry; AIndex: Integer;
out ACaption, AValue: string): Boolean;
out ACaption, AValue: string): Boolean; overload;
/// Variante qui rend aussi le rang de la piece jointe, ou -1.
function DetailRowAt(const AEntry: TVaultEntry; AIndex: Integer;
out ACaption, AValue: string; out AAttachIndex: Integer): Boolean; overload;
implementation
@@ -160,6 +163,9 @@ type
/// Supprimer une entree n'y figure pas : cela demande des tombstones, et
/// c'est l'etape 3 de la §7.
Field: string;
/// Rang de la piece jointe dans l'entree, ou -1. Il ne se deduit pas du
/// rang de la ligne : les champs personnalises s'intercalent avant.
AttachIndex: Integer;
end;
function BuildRows(const E: TVaultEntry): TArray<TDetailRow>;
@@ -168,7 +174,7 @@ var
I: Integer;
procedure Add(const ACaption, AValue: string; ASecret: Boolean;
const AField: string = '');
const AField: string = ''; AAttach: Integer = -1);
begin
// Une ligne vide n'apprend rien - SAUF si elle se remplit. Un identifiant
// absent qu'on ne peut pas ajouter serait une impasse.
@@ -178,6 +184,7 @@ var
LRows[High(LRows)].Value := AValue;
LRows[High(LRows)].Secret := ASecret;
LRows[High(LRows)].Field := AField;
LRows[High(LRows)].AttachIndex := AAttach;
end;
begin
@@ -210,7 +217,7 @@ begin
// entree porte un document, et pas ici.
for I := 0 to High(E.Attachments) do
Add('Attachment - ' + HumanSize(E.Attachments[I].SizeBytes),
E.Attachments[I].FileName, False);
E.Attachments[I].FileName, False, '', I);
// DOSSIER ET ETIQUETTES SEPARES, parce qu'ils ne se modifient pas pareil.
// Le dossier est une chaine simple, choisie parmi celles qui existent - la
@@ -252,6 +259,10 @@ begin
LObj.AddPair('s', TJSONBool.Create(LRows[I].Secret));
// 'e' porte le nom du champ, et sa presence vaut "modifiable".
if LRows[I].Field <> '' then LObj.AddPair('e', LRows[I].Field);
// 'a' porte le rang de la piece jointe : sa presence vaut
// "enregistrable", et la page y met le bouton de telechargement.
if LRows[I].AttachIndex >= 0 then
LObj.AddPair('a', TJSONNumber.Create(LRows[I].AttachIndex));
LArr.AddElement(LObj);
end;
Result := LRoot.ToJSON;
@@ -262,14 +273,24 @@ end;
function DetailRowAt(const AEntry: TVaultEntry; AIndex: Integer;
out ACaption, AValue: string): Boolean;
var
LAttach: Integer;
begin
Result := DetailRowAt(AEntry, AIndex, ACaption, AValue, LAttach);
end;
function DetailRowAt(const AEntry: TVaultEntry; AIndex: Integer;
out ACaption, AValue: string; out AAttachIndex: Integer): Boolean;
var
LRows: TArray<TDetailRow>;
begin
AAttachIndex := -1;
LRows := BuildRows(AEntry);
Result := (AIndex >= 0) and (AIndex <= High(LRows));
if not Result then Exit;
ACaption := LRows[AIndex].Caption;
AValue := LRows[AIndex].Value;
AAttachIndex := LRows[AIndex].AttachIndex;
end;
end.