6b3001666e
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.
137 lines
4.5 KiB
ObjectPascal
137 lines
4.5 KiB
ObjectPascal
unit PMA.SaveFile;
|
|
{
|
|
Deposer un fichier la ou l'utilisateur le retrouvera.
|
|
|
|
LA QUESTION N'ETAIT PAS LE CODE, C'ETAIT L'ENDROIT. Le repertoire prive de
|
|
l'application ne sert a rien : aucun gestionnaire de fichiers ne l'ouvre, et
|
|
il part avec la desinstallation. Le stockage externe direct est ferme depuis
|
|
Android 10. Restent trois portes, et une seule ne demande ni permission ni
|
|
ecran supplementaire :
|
|
|
|
- MediaStore -> Telechargements : le systeme cree l'entree, nous ecrivons
|
|
dedans, le fichier apparait dans "Telechargements" avec les autres. AUCUNE
|
|
permission depuis l'API 29, parce que l'application n'ecrit que ce qu'elle
|
|
a elle-meme insere.
|
|
- Le selecteur de destination (ACTION_CREATE_DOCUMENT) donnerait le choix,
|
|
mais il exige un retour d'activite, que FMX rend penible.
|
|
- Le partage (ACTION_SEND) exigerait un FileProvider declare au manifeste.
|
|
|
|
Donc MediaStore. En dessous de l'API 29 il faudrait WRITE_EXTERNAL_STORAGE,
|
|
une permission qu'on vient justement de retirer des dix defauts de Delphi :
|
|
on refuse et on le dit, plutot que de la reprendre pour des versions
|
|
d'Android que personne ici n'utilise.
|
|
|
|
CE QUI SORT DE L'APPLICATION SORT DE SA PROTECTION. Le fichier depose est
|
|
en clair, lisible par tout ce qui lit les telechargements. C'est ce que
|
|
l'utilisateur demande en appuyant sur "enregistrer", et c'est pour cela que
|
|
l'appel le dit dans son nom plutot que de s'appeler "export".
|
|
}
|
|
|
|
interface
|
|
|
|
uses
|
|
System.SysUtils;
|
|
|
|
/// Ecrit ADonnees dans Telechargements sous AFileName.
|
|
/// Rend le nom retenu par le systeme, ou '' en cas d'echec - AError dit alors
|
|
/// pourquoi.
|
|
function SaveToDownloads(const AFileName, AMime: string; const AData: TBytes;
|
|
out AError: string): string;
|
|
|
|
implementation
|
|
|
|
{$IF DEFINED(ANDROID)}
|
|
// UNE SEULE CLAUSE uses PAR SECTION : deux d'affilee, meme separees par une
|
|
// directive, sont une erreur de syntaxe et non un ajout.
|
|
uses
|
|
Androidapi.Helpers, Androidapi.JNI.JavaTypes, Androidapi.JNI.Os,
|
|
Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Provider,
|
|
// Androidapi.JNI.Net pour Jnet_Uri : le resolveur rend une URI, pas un
|
|
// chemin - c'est elle qui designe la ligne que le systeme vient de creer.
|
|
Androidapi.JNI.Net, Androidapi.JNIBridge;
|
|
|
|
const
|
|
// MediaStore.MediaColumns, ecrites en dur comme EXTRA_IS_SENSITIVE ailleurs :
|
|
// les noms de colonnes sont stables depuis toujours, et les chercher dans les
|
|
// en-tetes coute plus que de les lire ici.
|
|
COL_NAME = '_display_name';
|
|
COL_MIME = 'mime_type';
|
|
COL_PATH = 'relative_path';
|
|
|
|
function SaveToDownloads(const AFileName, AMime: string; const AData: TBytes;
|
|
out AError: string): string;
|
|
var
|
|
LValues: JContentValues;
|
|
LUri: Jnet_Uri;
|
|
LOut: JOutputStream;
|
|
LArr: TJavaArray<Byte>;
|
|
LMime: string;
|
|
begin
|
|
Result := '';
|
|
AError := '';
|
|
if Length(AData) = 0 then
|
|
begin
|
|
AError := 'nothing to write';
|
|
Exit;
|
|
end;
|
|
if TJBuild_VERSION.JavaClass.SDK_INT < 29 then
|
|
begin
|
|
AError := 'saving files needs Android 10 or later';
|
|
Exit;
|
|
end;
|
|
|
|
LMime := AMime;
|
|
// Un type vide ferait ranger le fichier sans que rien ne sache l'ouvrir.
|
|
if LMime = '' then LMime := 'application/octet-stream';
|
|
|
|
try
|
|
LValues := TJContentValues.JavaClass.init;
|
|
LValues.put(StringToJString(COL_NAME), StringToJString(AFileName));
|
|
LValues.put(StringToJString(COL_MIME), StringToJString(LMime));
|
|
// Le systeme se charge des doublons : deux fichiers du meme nom deviennent
|
|
// "nom" et "nom (1)". Rien a gerer de notre cote.
|
|
LValues.put(StringToJString(COL_PATH), StringToJString('Download'));
|
|
|
|
LUri := TAndroidHelper.Context.getContentResolver.insert(
|
|
TJMediaStore_Downloads.JavaClass.EXTERNAL_CONTENT_URI, LValues);
|
|
if LUri = nil then
|
|
begin
|
|
AError := 'the system refused to create the file';
|
|
Exit;
|
|
end;
|
|
|
|
LOut := TAndroidHelper.Context.getContentResolver.openOutputStream(LUri);
|
|
if LOut = nil then
|
|
begin
|
|
AError := 'the system gave no way to write';
|
|
Exit;
|
|
end;
|
|
LArr := TJavaArray<Byte>.Create(Length(AData));
|
|
try
|
|
Move(AData[0], LArr.Data^, Length(AData));
|
|
LOut.write(LArr);
|
|
LOut.flush;
|
|
finally
|
|
LOut.close;
|
|
LArr.Free;
|
|
end;
|
|
Result := AFileName;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
AError := E.ClassName + ': ' + E.Message;
|
|
Result := '';
|
|
end;
|
|
end;
|
|
end;
|
|
{$ELSE}
|
|
function SaveToDownloads(const AFileName, AMime: string; const AData: TBytes;
|
|
out AError: string): string;
|
|
begin
|
|
Result := '';
|
|
AError := 'Downloads are Android-only';
|
|
end;
|
|
{$ENDIF}
|
|
|
|
end.
|