Als kleines „Abfallprodukt“ meines Projektes http://www.dead-drop.net hier ein wenig Code, der vielleicht anderen helfen kann.
In Delphi gibt es mit der Komponente TOpenDialog einen einfachen weg um den Benutzer Dateien auswählen zu lassen. Leider funktioniert diese Komponente unter Android nicht. das ist ärgerlich, vor allem wenn man nicht für jede Plattform anderen Code schreiben möchte und so in einer Compilerswitchhölle landet wenn man Cross Platform entwickelt.
Daher habe ich eine (genaugenommen 3) Klasse(n), die das Problem lösen und eine einheitliche zum System FilePicker bieten.
Die komplette Demo gibt es unter : https://hermes.simplecrypt,net/down/CodeCorner/Delphi/FilePicker.zip
Die Klasse TAndroidFilePicker übernimmt die eigentliche Arbeit unter Android. Da es bei Android keine Modalen Dialoge gibt muss zum einen ein Intent erzeugt werden. Und zum anderen müssen die ergebnisse in einem Listener abgerufen werden.
unit androidfilepicker;
interface
uses
System.SysUtils, System.Classes,
{$IFDEF ANDROID}
Androidapi.JNIBridge, Androidapi.JNI.Net,
{$ENDIF ANDROID}
System.SysConst, System.IOUtils, System.Messaging;
type
TGetFileNameListener = procedure (const _ok: Boolean) of object;
TAndroidFilePicker = class (TObject)
private
FLastPickedFileName : String;
FFileNameListener : TGetFileNameListener;
FMessageChooserID : Integer;
{$IFDEF ANDROID}
FLastPickedURI : JNet_URI;
function getBytesFromURI(_uri : Jnet_URI) : TBytes;
function getURIfromMsg (m : TMessage) : Jnet_Uri;
function trimExtension (_rawExt : string) : string;
function getMimeType(_extension : string) : string;
procedure InternalOpenFileDialog(_title, _extension:string);
procedure HandleActivityMessageforChooser(const Sender: TObject; const M: TMessage);
property fileNameCallBack : TGetFileNameListener read FFileNameListener;
{$ENDIF}
protected
public
constructor create(_fileNameCallBack: TGetFileNameListener);
function getBytesFromlastPick : TBytes;
{$IFDEF ANDROID}
procedure OpenFileDialog(_title, _extension:string);
property lastPickedURI : JNet_URI read FLastPickedURI;
{$ENDIF}
property lastPickedFileName : string read FLastPickedFileName;
end;
implementation
uses
{$IFDEF ANDROID}
Androidapi.Helpers, Androidapi.JNI.JavaTypes, Androidapi.JNI.GraphicsContentViewText,
Androidapi.JNI.Webkit, Androidapi.JNI.App,
{$ENDIF}
System.NetEncoding, System.Rtti, System.TypInfo, System.Math;
const
FILE_SELECT_CODE = 0;
{$IFDEF ANDROID}
function TAndroidFilePicker.getBytesFromURI(_uri : Jnet_URI) : TBytes;
var
jis: JInputStream;
b: TJavaArray<Byte>;
begin
if _uri <> nil then begin
jis := TAndroidHelper.Context.getContentResolver.openInputStream(_uri);
b := TJavaArray<Byte>.Create(jis.available);
jis.read(b);
jis.close;
SetLength(result, b.Length);
if b.Length > 0 then
Move(b.Data^, result[0], b.Length);
end else begin
result := nil;
end;
end;
procedure TAndroidFilePicker.HandleActivityMessageforChooser(const Sender: TObject; const M: TMessage);
var
FileName : string;
begin
FLastPickedURI := getURIfromMsg(m);
if FLastPickedURI = nil then begin
FLastPickedFileName := '';
exit;
end else begin
FLastPickedFileName := JStringToString(FLastPickedURI.getPath).Trim;
if Assigned(fileNameCallBack) then
fileNameCallBack(not FLastPickedFileName.IsEmpty);
end;
end;
procedure TAndroidFilePicker.InternalOpenFileDialog(_title, _extension:string);
var
Intent, IntentChooser : JIntent;
sMIMEType, extension : string;
begin
extension := trimExtension(_extension);
sMIMEType := getMimeType(extension);
Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_GET_CONTENT);
Intent.setType(StringToJString(sMIMEType));
Intent.addCategory(TJIntent.JavaClass.CATEGORY_OPENABLE);
IntentChooser := TJIntent.JavaClass.createChooser(Intent, StrToJCharSequence(_title));
if FMessageChooserID <> 0 then
TMessageManager.DefaultManager.Unsubscribe(TMessageResultNotification, FMessageChooserID);
FMessageChooserID := 0;
FMessageChooserID := TMessageManager.DefaultManager.SubscribeToMessage(
TMessageResultNotification, HandleActivityMessageforChooser);
try
TAndroidHelper.Activity.startActivityForResult(IntentChooser, FILE_SELECT_CODE);
except
raise Exception.Create('Could not Find File Manager');
end;
end;
Procedure TAndroidFilePicker.OpenFileDialog(_title, _extension:string);
begin
InternalOpenFileDialog(_title, _extension);
end;
function TAndroidFilePicker.trimExtension (_rawExt : string) : string;
begin
Result := _rawExt;
if Result.Substring(0,1) = '.' then
Result := Result.Substring(1);
if Result = '*.*' then
Result := '*/*';
end;
function TAndroidFilePicker.getMimeType(_extension : string) : string;
var
MIMEType : JString;
mime : JMimeTypeMap;
sMIMEType, extension : string;
begin
extension := AnsiLowerCase(_extension);
MIMEType := nil;
sMIMEType := '';
if extension = '' then
sMIMEType := '*/*'
else begin
if extension = 'file/*' then
sMIMEType := 'file/*';
if extension = '*' then
begin
if TOSVersion.Check(4, 4) then
sMIMEType := '*/*'
else
sMIMEType := 'file/*';
end;
if extension = '*/*' then
sMIMEType := '*/*';
if sMimeType = '' then begin
mime := TJMimeTypeMap.JavaClass.getSingleton;
if mime <> nil then
MIMEType := mime.getMimeTypeFromExtension(StringToJString(extension));
if MIMEType <> nil then
sMIMEType := JStringToString(MIMEType).Trim;
end;
end;
if sMIMEType.IsEmpty then
sMIMEType := '*/*';
result := sMIMEType;
end;
function TAndroidFilePicker.getURIfromMsg (m : TMessage) : Jnet_Uri;
begin
Result := nil;
if (m <> nil) and (M is TMessageResultNotification) then
if (TMessageResultNotification(M).RequestCode = FILE_SELECT_CODE) then
if (TMessageResultNotification(M).ResultCode = TJActivity.JavaClass.RESULT_OK) then
if TMessageResultNotification(M).Value <> nil then
result := TMessageResultNotification(M).Value.getData;
end;
{$ENDIF ANDROID}
function TAndroidFilePicker.getBytesFromlastPick : TBytes;
begin
{$IFDEF ANDROID}
result := getBytesFromURI(lastPickedURI);
{$ELSE}
result :=nil;
{$ENDIF}
end;
constructor TAndroidFilePicker.create(_fileNameCallBack: TGetFileNameListener);
begin
inherited create;
FFileNameListener := _fileNameCallBack;
FMessageChooserID := 0;
end;
end.
Um diese Aufgabe nun unabhängig vom Betriebssystem aufrufen zu können gibt es noch die Klasse TFilePicker. Diese bindedet je nach Betriebssystem die obige Klasse ein oder verwendet den Standart TOpenDialog für die Aufgabe.
Ich konnte es nicht selber testen, aber laut Doku soll der TOpenDialog unter iOS und MacOS funktionieren.
Die Klasse TFilePicker bietet somit eine einheitliche Schnittstelle für die Aufgabe.
unit filePicker;
interface
uses
{$IFDEF ANDROID}
Androidapi.Jni.Net,
{$ENDIF}
System.SysUtils, System.Types, FMX.Dialogs, androidfilepicker, mimetype;
type
TNotifyPick = procedure(const _picked: Boolean) of object;
TFilePicker = class (TObject)
private
FMimeMap : TMimeMap;
FPickedFile : boolean;
FPickedFileName : string;
FCanReadExternalStorage : boolean;
FCanWriteExternalStorage : Boolean;
FOpenDialog: TOpenDialog;
FDialogTitle : string;
FNotifyPickCallback : TNotifyPick;
Fpicker : TAndroidFilePicker;
procedure GetFileName(const _ok: Boolean);
function FileToByteArray(const _fileName: WideString): TArray<Byte>;
function getPickedFileExtension : string;
function getPickedFileMimeType : String;
function getPickedPureFileName : String;
function getPureFileName(_fileName : string) : string;
function getLastSlashPos(_inStr : string) : integer;
protected
public
constructor create(_openDLG : TOpenDialog; _title : string; _notifyPick : TNotifyPick);
destructor Destroy; override;
procedure openFileFicker(_extension : string);
procedure updatePermissions;
function getPickedFileBytes : TBytes;
procedure resetPick;
property hasPickedFile : boolean read FPickedFile;
property pickedFileName : string read FPickedFileName;
property pickedPureFileName : string read getPickedPureFileName;
property pickedFileExtension : string read getPickedFileExtension;
property pickedFileMimeType : string read getPickedFileMimeType;
property canReadExternalStorage : boolean read FCanReadExternalStorage;
property canWriteExternalStorage : boolean read FCanWriteExternalStorage;
property picker : TAndroidFilePicker read Fpicker;
property mimeMap : TMimeMap read FMimeMap;
end;
implementation
uses
{$IFDEF ANDROID}
Androidapi.Helpers, Androidapi.JNI.Os, Androidapi.Jni.JavaTypes,
{$ENDIF}
System.Permissions, System.Classes;
//private
function TFilePicker.getLastSlashPos(_inStr : string) : integer;
var
i: Integer;
begin
result := 0;
for i := length(_inStr) downto 1 do
if _inStr[i] = '/' then begin
result := i;
break;
end;
end;
function TFilePicker.getPickedPureFileName : String;
begin
result := getPureFileName(pickedFileName);
end;
function TFilePicker.getPureFileName(_fileName : string) : string;
var
start, len : integer;
begin
Result := StringReplace(_fileName, '\', '/', [rfReplaceAll, rfIgnoreCase]);
start := getLastSlashPos(result);
len := length(result) - start;
result := result.Substring(start, len);
end;
function TFilePicker.getPickedFileExtension : string;
begin
result := '';
if hasPickedFile then
result := mimeMap.getExtensionFromFileName(pickedFileName);
end;
function TFilePicker.getPickedFileMimeType : String;
begin
result := '';
if hasPickedFile then
result := mimeMap.getMimeTypeFromFileName(pickedFileName);
end;
procedure TFilePicker.GetFileName(const _ok: Boolean);
begin
FPickedFile := _ok;
FPickedFileName := picker.lastPickedFileName;
FNotifyPickCallback(_ok);
end;
function TFilePicker.FileToByteArray(const _fileName : WideString): TArray<Byte>;
var
fs : TFileStream;
begin
fs:=TFileStream.Create(_fileName,fmOpenRead);
try
fs.Seek(0,soFromBeginning);
setLength(result,fs.Size);
fs.ReadBuffer(Pointer(result)^, fs.Size);
finally
fs.Free;
end;
end;
// protected
procedure TFilePicker.updatePermissions;
{$IFDEF ANDROID}
var
FPermissionREAD_EXTERNAL_STORAGE,
FPermissionWRITE_EXTERNAL_STORAGE: string;
{$ENDIF ANDROID}
begin
FCanReadExternalStorage := True;
FCanWriteExternalStorage := True;
{$IFDEF ANDROID}
FCanReadExternalStorage := False;
FCanWriteExternalStorage := False;
FPermissionREAD_EXTERNAL_STORAGE := JStringToString(TJManifest_permission.JavaClass.READ_EXTERNAL_STORAGE);//读取文件
FPermissionWRITE_EXTERNAL_STORAGE := JStringToString(TJManifest_permission.JavaClass.WRITE_EXTERNAL_STORAGE);//写入文件
PermissionsService.RequestPermissions([FPermissionWRITE_EXTERNAL_STORAGE, FPermissionREAD_EXTERNAL_STORAGE],
procedure(const APermissions: TClassicStringDynArray; const AGrantResults: TClassicPermissionStatusDynArray)
begin
if (Length(AGrantResults) > 0 ) and (AGrantResults[0] = TPermissionStatus.Granted) then
begin
FCanReadExternalStorage := True;
FCanWriteExternalStorage := True;
end
else if (Length(AGrantResults) > 1 ) and (AGrantResults[1] = TPermissionStatus.Granted) then
begin
FCanReadExternalStorage := True;
FCanWriteExternalStorage := False;
end
else
begin
FCanReadExternalStorage := False;
FCanWriteExternalStorage := False;
end;
end);
{$ENDIF ANDROID}
end;
// public
function TFilePicker.getPickedFileBytes : TBytes;
begin
result := nil;
if hasPickedFile then begin
{$IFDEF ANDROID}
result := picker.getBytesFromlastPick;
{$ENDIF ANDROID}
{$IF DEFINED(MSWINDOWS) OR DEFINED(MACOS)}
result := FileToByteArray(FPickedFileName)
{$ENDIF}
end;
end;
constructor TFilePicker.create(_openDLG : TOpenDialog; _title : string; _notifyPick : TNotifyPick);
begin
inherited create;
FMimeMap := TMimeMap.create;;
FOpenDialog := _openDLG;
FDialogTitle := _title;
FNotifyPickCallback := _notifyPick;
Fpicker := TAndroidFilePicker.create(GetFileName);
resetPick;;
end;
destructor TFilePicker.destroy;
begin
FMimeMap.Free;
Fpicker.Free;
end;
procedure TFilePicker.resetPick;
begin
FPickedFile := false;
FPickedFileName := '';
end;
procedure TFilePicker.openFileFicker(_extension : string);
var
FileExt: string;
begin
resetPick;
FileExt := _extension.Trim;
if FileExt.Substring(0,1) = '.' then
FileExt := FileExt.Substring(1);
{$IFDEF ANDROID}
picker.OpenFileDialog('Choose File', FileExt);
{$ENDIF ANDROID}
{$IF DEFINED(MSWINDOWS) OR DEFINED(MACOS)}
{$IF NOT defined(IOS)}
FOpenDialog.Title := FDialogTitle;
FOpenDialog.DefaultExt := FileExt;
FOpenDialog.Filter := '(*.' + FileExt + ')|*.' + FileExt + '| (*.*)|*.*';
FPickedFile := FOpenDialog.Execute;
if hasPickedFile then begin
FPickedFileName := FOpenDialog.FileName;
FNotifyPickCallback(true);
end;
{$ENDIF}
{$ENDIF}
end;
end.
Die Klasse TMimeMap is eine einfache Hilfsklasse um vom Mime Type auf die Extension schliessen zu können und umgekeht. Den Code spare ich mir an dieser Stelle. Die Klasse ist aber natürlich im Demo enthalten.
Zu guter letzt noch: Wie verwendet man die Klasse:
Man benötigt eine Instanz der Klasse TFilePicker. Im Demo mache ich das mit einer privaten Variable und erzeuge die Instanz im onCreate der Form.
Um die Instanz erzeugen zu können braucht man auch noch eine Methode vom Typ TNotifyPick = procedure(const _picked: Boolean) of object; Hier kann man die Ergebnisse des abrufen.
Das relativ komplizierte Vorgehen mit der Callback Methode ist zwar nur für Android nötig. Aber auf diese Weise hat mein eine einheitliche Schnittstelle für alle OS.
Hier noch der Code der Beispielanwendung:
unit frmMain;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.Memo.Types,
FMX.StdCtrls, FMX.Controls.Presentation, FMX.ScrollBox, FMX.Memo, filepicker,
FMX.Layouts, FMX.ExtCtrls, FMX.Menus;
type
TLoadFileThread=class(TThread)
private
FFilePicker : TFilePicker;
FImage : TBitmap;
FFileContent64 : string;
protected
procedure Execute;override;
public
constructor Create(_filePicker : TFilePicker);
destructor Destroy; override;
property fileContent : string read FFileContent64;
property image : TBitmap read FImage;
end;
TfMain = class(TForm)
StyleBook1: TStyleBook;
memContent: TMemo;
btnPick: TButton;
lblFileName: TLabel;
dlgOpen: TOpenDialog;
lblMimeType: TLabel;
ImageViewer1: TImageViewer;
AniIndicator1: TAniIndicator;
procedure FormCreate(Sender: TObject);
procedure btnPickClick(Sender: TObject);
private
FLoadFileThread : TLoadFileThread;
FFilePicker : TFilePicker;
procedure listenFilePicker(const _picked : boolean);
property FilePicker : TFilePicker read FFilePicker;
procedure OnTerminate(Sender:TObject);
public
{ Public-Deklarationen }
end;
var
fMain: TfMain;
implementation
uses
System.NetEncoding, System.threading;
{$R *.fmx}
// TLoadFileThread=class(TThread)
// private
// protected
procedure TLoadFileThread.Execute;
var
ms : TMemoryStream;
imageBytes : TBytes;
mime : String;
begin
if FFilePicker.hasPickedFile then begin
imageBytes := FFilePicker.getPickedFileBytes;
FFileContent64 := TNetEncoding.Base64.EncodeBytesToString(imageBytes);
mime := FFilePicker.pickedFileMimeType;
if mime.StartsText('image', mime) then begin
ms := TMemoryStream.Create;
try
ms.Write(imageBytes[0], length(imageBytes));
FImage := TBitmap.Create;
FImage.LoadFromStream(ms);
finally
ms.free;
end;
end;
end;
end;
// public
constructor TLoadFileThread.Create(_filePicker : TFilePicker);
begin
FFilePicker := _filePicker;
inherited Create(True);
end;
destructor TLoadFileThread.destroy;
begin
if FImage <> nil then
FreeAndNil(FImage);
inherited;
end;
procedure TfMain.listenFilePicker(const _picked : boolean);
begin
if _picked then begin
AniIndicator1.Enabled := true;
AniIndicator1.Visible := true;
lblFileName.Text := FilePicker.pickedFileName;
lblMimeType.Text := FilePicker.pickedFileExtension +' -- '+ FilePicker.pickedFileMimeType;
Application.ProcessMessages;
FLoadFileThread:= TLoadFileThread.Create(FFilePicker);
FLoadFileThread.OnTerminate:=OnTerminate;
FLoadFileThread.Resume;
end;
end;
procedure TfMain.btnPickClick(Sender: TObject);
begin
FilePicker.openFileFicker('*');
end;
procedure TfMain.FormCreate(Sender: TObject);
begin
FFilePicker := TFilePicker.create(dlgOpen, '', listenFilePicker);
end;
procedure TfMain.OnTerminate(Sender:TObject);
begin
memContent.Text := FLoadFileThread.fileContent;
if FLoadFileThread.image <> nil then begin
ImageViewer1.Bitmap.Assign(FLoadFileThread.image);
ImageViewer1.BestFit;
ImageViewer1.RealignContent;
end;
FLoadFileThread := nil;
AniIndicator1.Enabled := false;
AniIndicator1.Visible := false;
end;
end.
So, ich hoffe ich kann damit dem einen oder anderen etwas Zeit sparen 😉