You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Delphi 11 FMX Android获取文件真实路径及内容传输求助

Delphi 11 FMX Android 文件处理解决方案

问题背景

基于Delphi 11 FMX开发Android应用,已完成表单填写、拍照上传功能,现需实现任意文件(照片/视频/文档)选择、本地查看、HTTPS传输功能。当前可获取文件Uri,但返回的是\image:123456或\document:image\img1234.jpg这类伪路径,无法直接用于文件操作,需通过Cursor获取真实路径,同时需要文件打开、转Base64传输的代码示例。


1. 获取文件真实路径(Uri转真实路径)

Android的Content Uri无法直接映射到本地文件路径,需根据Uri类型(媒体/文档)通过ContentResolver查询,或复制文件到应用私有目录规避权限问题:

function GetRealPathFromURI(Uri: Jnet_Uri): String;
var
  Cur: JCursor;
  ColumnIndex: Integer;
  Scheme, Authority: JString;
begin
  Result := '';
  Scheme := Uri.getScheme;

  // 处理content://协议的Uri
  if Scheme.equals(TJContentResolver.JavaClass.SCHEME_CONTENT) then
  begin
    Authority := Uri.getAuthority;
    // 媒体文件(图片/视频/音频)
    if Authority.equals(StringToJString('media')) then
    begin
      Cur := TAndroidHelper.Context.getContentResolver.query(Uri, 
        TJavaObjectArray<JString>.Create(StringToJString('_data')), 
        nil, nil, nil);
      try
        if (Cur <> nil) and Cur.moveToFirst then
        begin
          ColumnIndex := Cur.getColumnIndex(StringToJString('_data'));
          if ColumnIndex <> -1 then
            Result := JStringToString(Cur.getString(ColumnIndex));
        end;
      finally
        if Cur <> nil then Cur.close;
      end;
    end
    // 文档类文件(文件管理器选择的文档)
    else
    begin
      // 复制到应用私有目录,避免权限限制
      Result := CopyUriToPrivateFile(Uri);
    end;
  end
  // 处理file://协议的Uri
  else if Scheme.equals(TJContentResolver.JavaClass.SCHEME_FILE) then
  begin
    Result := JStringToString(Uri.getPath);
  end;
end;

// 辅助函数:将Uri指向的文件复制到应用私有目录,返回新路径
function CopyUriToPrivateFile(Uri: Jnet_Uri): String;
var
  Input: JInputStream;
  Output: JFileOutputStream;
  Buffer: TJavaArray<Byte>;
  TempFile: JFile;
begin
  Result := '';
  TempFile := TJFile.JavaClass.init(TAndroidHelper.Context.getFilesDir, 
    StringToJString('temp_' + GetFileName(Uri)));
  
  try
    Input := TAndroidHelper.Context.getContentResolver.openInputStream(Uri);
    Output := TJFileOutputStream.JavaClass.init(TempFile);
    Buffer := TJavaArray<Byte>.Create(1024);
    
    while (Input.read(Buffer) > 0) do
    begin
      Output.write(Buffer);
    end;
    
    Result := JStringToString(TempFile.getAbsolutePath);
  finally
    if Input <> nil then Input.close;
    if Output <> nil then Output.close;
    if Buffer <> nil then Buffer.Free;
  end;
end;

2. 修正文件选择逻辑(替换伪路径为真实路径)

修改原GetFileBrowser中的路径获取代码,使用上面的GetRealPathFromURI函数:

procedure TDM.GetFileBrowser(ReqCode: Integer);
var
  Intent: JIntent;
  FName: String;
  RealPath: String;
begin
  Intent := TJIntent.Create;

  Intent.setType(StringToJString('*/*'));
  Intent.setAction(TJIntent.JavaClass.ACTION_GET_CONTENT);
  Intent.putExtra(TJIntent.JavaClass.EXTRA_ALLOW_MULTIPLE, true);

  TMessageManager.DefaultManager.SubscribeToMessage(TMessageResultNotification,
    procedure(const Sender   : TObject;
              const aMessage : TMessage)
          var msgRES : TMessageResultNotification;
              i: Integer;
        begin
              msgRES := TMessageResultNotification(aMessage);

              if msgRES.RequestCode = ReqCode then
                 if (msgRES.ResultCode = TJActivity.JavaClass.RESULT_OK) then
                 begin

                     URIfileTrn := msgRES.Value.getData();

                     if URIfileTrn = nil then // 多选模式
                     begin
                        ClipDataF := msgRES.Value.getClipData;
                        for i := 0 to ClipDataF.getItemCount-1 do
                        begin
                          URIfileTrn := ClipDataF.getItemAt(i).getUri;
                          FName := GetFileName(URIfileTrn);
                          RealPath := GetRealPathFromURI(URIfileTrn);
                          if FName <> '' and (RealPath <> '') then
                            case ReqCode of
                              1000: SaveFile(FName, RealPath);
                              2000: SaveSFile(FName, RealPath);
                            end;
                        end;
                     end
                     else begin // 单选模式
                       FName := GetFileName(URIfileTrn);
                       RealPath := GetRealPathFromURI(URIfileTrn);
                       if FName <> '' and (RealPath <> '') then
                         case ReqCode of
                            1000: SaveFile(FName, RealPath);
                            2000: SaveSFile(FName, RealPath);
                         end;
                     end;

                     URIfileTrn := nil;
                 end;
        end);

  TAndroidHelper.Activity.startActivityForResult(Intent, ReqCode);
end;

3. 修正文件打开代码(适配真实路径)

针对真实路径,使用FileProvider生成可共享的Uri,确保其他应用有权限打开:

procedure TDM.openFile(RealPath: String);
var
  ExtFile: string;
  mime: JMimeTypeMap;
  ExtToMime: JString;
  Intent: JIntent;
  Uri: Jnet_Uri;
  LAuthority: JString;
  TargetFile: JFile;
begin
  if DM.qFiles.IsEmpty then Exit;
  if not TFile.Exists(RealPath) then
  begin
      TDialogService.MessageDialog('文件不存在(可能已删除或移动)。是否从表格中删除该文件记录?', 
        System.UITypes.TMsgDlgType.mtInformation,
        [System.UITypes.TMsgDlgBtn.mbYes, System.UITypes.TMsgDlgBtn.mbNo],
        System.UITypes.TMsgDlgBtn.mbNo, 0,
      procedure(const AResult: TModalResult)
      begin
        if AResult = mrYES then DM.qFiles.Delete;
      end);
    Exit;
  end;

  try
    ExtFile := AnsiLowerCase(StringReplace(TPath.GetExtension(RealPath), '.', '', []));
    mime := TJMimeTypeMap.JavaClass.getSingleton();
    ExtToMime := mime.getMimeTypeFromExtension(StringToJString(ExtFile));
    if ExtToMime = nil then
      ExtToMime := StringToJString('application/octet-stream'); // 未知类型默认二进制

    TargetFile := TJFile.JavaClass.init(StringToJString(RealPath));
    LAuthority := StringToJString(JStringToString(TAndroidHelper.Context.getApplicationContext.getPackageName) + '.fileprovider');
    Uri := TJcontent_FileProvider.JavaClass.getUriForFile(TAndroidHelper.Context, LAuthority, TargetFile);

    Intent := TJIntent.Create;
    Intent.setFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION);
    Intent.setAction(TJIntent.JavaClass.ACTION_VIEW);
    Intent.setDataAndType(Uri, ExtToMime);
    
    // 检查是否有应用支持打开该文件
    if Intent.resolveActivity(TAndroidHelper.Context.getPackageManager) <> nil then
      TAndroidHelper.Activity.startActivity(Intent)
    else
      ShowMessage('未找到可打开该文件的应用!');
  except
    ShowMessage('无法打开文件!');
  end;
end;

4. 文件转Base64并HTTPS传输

使用Indy组件实现文件内容转Base64,再通过HTTPS上传至服务器:

uses
  IdEncoderMIME, IdHTTP, System.Classes, System.NetEncoding;

// 文件内容转Base64字符串
function FileToBase64(const FilePath: String): String;
var
  Stream: TFileStream;
  Encoder: TIdEncoderMIME;
begin
  Result := '';
  if not TFile.Exists(FilePath) then Exit;
  
  Stream := TFileStream.Create(FilePath, fmOpenRead or fmShareDenyWrite);
  try
    Encoder := TIdEncoderMIME.Create(nil);
    try
      Result := Encoder.Encode(Stream);
    finally
      Encoder.Free;
    end;
  finally
    Stream.Free;
  end;
end;

// 上传Base64内容到服务器
procedure UploadFileToServer(const FilePath, ServerURL: String);
var
  Base64Content: String;
  HTTP: TIdHTTP;
  Params: TStringList;
begin
  Base64Content := FileToBase64(FilePath);
  if Base64Content = '' then Exit;

  HTTP := TIdHTTP.Create(nil);
  Params := TStringList.Create;
  try
    HTTP.Request.ContentType := 'application/x-www-form-urlencoded';
    // URL编码避免特殊字符问题
    Params.Add('filename=' + TNetEncoding.URL.Encode(TPath.GetFileName(FilePath)));
    Params.Add('filecontent=' + TNetEncoding.URL.Encode(Base64Content));
    
    HTTP.Post(ServerURL, Params);
    ShowMessage('文件上传成功!');
  except
    on E: Exception do
      ShowMessage('上传失败:' + E.Message);
  end;
  HTTP.Free;
  Params.Free;
end;

用户原代码参考

// 获取文件名函数
function GetFileName(Uri: Jnet_Uri): String;
var
  Cur: JCursor;
begin
  Result := '';
  Cur := TAndroidHelper.Context.getContentResolver().query(Uri, nil, nil, nil, nil, nil);
  try
    if (Cur <> nil) then
      if Cur.moveToFirst then
        Result := JStringToString(Cur.getString(cur.getColumnIndex(TJOpenableColumns.JavaClass.DISPLAY_NAME)));
  finally
    Cur.close;
  end;
end;

内容的提问来源于stack exchange,提问作者HardLink

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 00:27:33