При разработке Android приложений на Delphi, использующих Intents для выбора файлов и последующей их загрузки на сервер (например, PHP), можно столкнуться с проблемой, когда функция GetRealPathFromURI перестает работать на Android 15 (и, возможно, на более новых версиях). В частности, она может возвращать пустую строку, что делает невозможным получение физического пути к выбранному файлу.
Причина проблемы:
Начиная с Android API 33 (Android 13), Google ввела более строгие ограничения на доступ к внешнему хранилищу. Разрешение READ_EXTERNAL_STORAGE больше не предоставляет доступ ко всем файлам на внешнем хранилище. Это изменение направлено на повышение конфиденциальности пользователей. В Android 15 эти ограничения, вероятно, еще больше ужесточены. Функция GetRealPathFromURI, полагающаяся на получение прямого пути, может не работать из-за отсутствия необходимых разрешений или изменений в структуре хранения файлов.
Решение, предложенное автором оригинального поста:
Автор оригинального поста решил проблему, скопировав выбранный файл из URI во внутреннюю или кэш-директорию приложения, а затем загрузив копию на сервер.
Пример кода на Object Pascal (Delphi):
uses
Androidapi.JNI.GraphicsContentViewText,
Androidapi.JNI.Net,
Androidapi.JNI.Os,
Androidapi.JNIBridge,
System.IOUtils,
System.Net.HttpClient,
System.Net.URLClient,
IOUtils;
function CopyFileFromUri(const URI: string; const DestDir: string): Boolean;
var
InputStream: JInputStream;
OutputStream: TJavaOutputStream;
Buffer: array[0..4095] of Byte;
BytesRead: Integer;
FileName: string;
DestPath: string;
Context: JContext;
begin
Result := False;
try
Context := SharedActivityContext; // Получаем контекст Android
InputStream := (Context.getContentResolver.openInputStream(StringToJURI(URI)) as JInputStream);
if InputStream <> nil then
begin
FileName := GetFileNameByUri(URI); // Функция для получения имени файла из URI (см. ниже)
DestPath := IncludeTrailingPathDelimiter(DestDir) + FileName;
OutputStream := TJavaOutputStream.Create(TFileStream.Create(DestPath, fmCreate));
try
repeat
BytesRead := InputStream.read(Buffer, 0, SizeOf(Buffer));
if BytesRead > 0 then
OutputStream.write(Buffer, 0, BytesRead);
until BytesRead = -1;
Result := True;
finally
OutputStream.Free;
end;
InputStream.close;
end;
except
on E: Exception do
// Обработка ошибок, например, запись в лог
ShowMessage('Ошибка при копировании файла: ' + E.Message);
end;
end;
function GetFileNameByUri(const URI: string): string;
var
Cursor: JCursor;
ColumnIndex: Integer;
FileName: string;
Context: JContext;
begin
Result := '';
try
Context := SharedActivityContext;
Cursor := Context.getContentResolver.query(StringToJURI(URI), nil, nil, nil, nil);
if Cursor <> nil then
begin
try
ColumnIndex := Cursor.getColumnIndex(StringToJString('_display_name'));
if ColumnIndex > -1 then
begin
Cursor.moveToFirst;
FileName := JStringToString(Cursor.getString(ColumnIndex));
Result := FileName;
end;
finally
Cursor.close;
end;
end;
except
on E: Exception do
// Обработка ошибок
ShowMessage('Ошибка при получении имени файла: ' + E.Message);
end;
end;
procedure TForm1.ButtonUploadClick(Sender: TObject);
var
FilePath: string;
IntentData: string;
begin
// Предполагается, что IntentManager1.GetDataUri(intentData) возвращает URI выбранного файла
IntentData := IntentManager1.GetDataUri(intentData);
// Получаем путь к директории загрузок (можно использовать другую директорию, например, кэш)
CopyFileFromUri(IntentData, GetEnvironmentDirectoryPath(dirDownloads));
FilePath := GetEnvironmentDirectoryPath(dirDownloads) + '/' + GetFileNameByUri(IntentData);
if FileExists(FilePath) then
begin
// Загрузка файла на сервер (пример с использованием HttpClient)
HttpClient1.UploadFile('http://192.xx.xx.xx/webapi/uploadfile.php', FilePath, 'fileupload');
end
else
ShowMessage('Файл не существует по адресу: ' + FilePath);
end;
Альтернативное решение (более предпочтительное):
Вместо копирования файла, более эффективным и безопасным подходом является работа с InputStream напрямую. Это позволяет избежать ненужного копирования и экономит место на устройстве.
Пример кода на Object Pascal (Delphi):
uses
Androidapi.JNI.GraphicsContentViewText,
Androidapi.JNI.Net,
Androidapi.JNI.Os,
Androidapi.JNIBridge,
System.IOUtils,
System.Net.HttpClient,
System.Net.URLClient,
IOUtils;
procedure UploadFileFromUri(const URI: string; const UploadURL: string; const FieldName: string);
var
InputStream: JInputStream;
HttpClient: THTTPClient;
Content: TMultipartFormDataContent;
StreamContent: TStreamContent;
Context: JContext;
FileName: string;
Response: IHTTPResponse;
begin
try
Context := SharedActivityContext;
InputStream := (Context.getContentResolver.openInputStream(StringToJURI(URI)) as JInputStream);
if InputStream <> nil then
begin
FileName := GetFileNameByUri(URI);
HttpClient := THTTPClient.Create;
Content := TMultipartFormDataContent.Create;
StreamContent := TStreamContent.Create(TJavaInputStreamAdapter.Create(InputStream)); // Адаптер для работы с JInputStream как с TStream
StreamContent.Headers.ContentType := 'application/octet-stream'; // Укажите правильный ContentType, если знаете
Content.Add(StreamContent, FieldName, FileName);
try
Response := HttpClient.Post(UploadURL, Content);
// Обработка ответа сервера
if Response.StatusCode = 200 then
ShowMessage('Файл успешно загружен')
else
ShowMessage('Ошибка при загрузке файла. Код ответа: ' + IntToStr(Response.StatusCode));
finally
Response := nil;
Content.Free;
HttpClient.Free;
end;
InputStream.close;
end;
except
on E: Exception do
ShowMessage('Ошибка при загрузке файла: ' + E.Message);
end;
end;
// Адаптер для преобразования JInputStream в TStream
type
TJavaInputStreamAdapter = class(TStream)
private
FInputStream: JInputStream;
FPosition: Int64;
public
constructor Create(AInputStream: JInputStream);
destructor Destroy; override;
function Read(var Buffer; Count: Integer): Integer; override;
function Seek(Offset: Int64; Origin: TSeekOrigin): Int64; override;
property Position: Int64 read FPosition write FPosition;
property Size: Int64 read GetSize;
private
function GetSize: Int64;
end;
constructor TJavaInputStreamAdapter.Create(AInputStream: JInputStream);
begin
inherited Create;
FInputStream := AInputStream;
FPosition := 0;
end;
destructor TJavaInputStreamAdapter.Destroy;
begin
FInputStream := nil;
inherited Destroy;
end;
function TJavaInputStreamAdapter.Read(var Buffer; Count: Integer): Integer;
var
BytesRead: Integer;
begin
BytesRead := FInputStream.read(Buffer, 0, Count);
if BytesRead > 0 then
FPosition := FPosition + BytesRead;
Result := BytesRead;
end;
function TJavaInputStreamAdapter.Seek(Offset: Int64; Origin: TSeekOrigin): Int64;
begin
// Seeking в JInputStream не поддерживается, возвращаем текущую позицию
Result := FPosition;
end;
function TJavaInputStreamAdapter.GetSize: Int64;
begin
// Размер JInputStream получить сложно, возвращаем -1
Result := -1;
end;
procedure TForm1.ButtonUploadClick(Sender: TObject);
var
IntentData: string;
begin
IntentData := IntentManager1.GetDataUri(intentData);
UploadFileFromUri(IntentData, 'http://192.xx.xx.xx/webapi/uploadfile.php', 'fileupload');
end;
Ключевые моменты альтернативного решения:
Использование TJavaInputStreamAdapter: Этот класс адаптирует JInputStream (Java InputStream) к TStream (Delphi Stream), что позволяет работать с данными, полученными из URI, как с обычным потоком данных.
Загрузка потока данных напрямую:TStreamContent.Create(TJavaInputStreamAdapter.Create(InputStream)) создает контент для загрузки из потока, что позволяет избежать промежуточного сохранения файла.
Использование THTTPClient и TMultipartFormDataContent: Эти классы упрощают процесс отправки данных на сервер в формате multipart/form-data, который обычно используется для загрузки файлов.
Обработка ContentType: Важно правильно указать ContentType для загружаемого файла. Если тип файла известен, укажите его явно (например, image/jpeg). Если тип файла неизвестен, можно использовать application/octet-stream.
Преимущества альтернативного решения:
Экономия места: Не требуется временное хранение файла на устройстве.
Производительность: Избегается операция копирования файла, что может быть особенно важно для больших файлов.
Безопасность: Меньше риска утечки данных, так как файл не сохраняется во временной директории.
Важно:
Убедитесь, что на сервере (PHP) правильно обрабатывается загрузка файлов в формате multipart/form-data.
Не забудьте добавить необходимые uses в ваш проект Delphi.
Протестируйте оба решения на разных версиях Android, чтобы убедиться в их корректной работе.
Обрабатывайте исключения, чтобы обеспечить стабильную работу приложения.
В заключение, проблема с GetRealPathFromURI в Android 15 требует обхода, связанного с ограничениями доступа к файловой системе. Использование InputStream напрямую для загрузки файлов на сервер является более эффективным и предпочтительным решением, чем копирование файла во временную директорию. Приведенные примеры кода на Object Pascal (Delphi) помогут вам реализовать эти решения в ваших Android приложениях.
Контекст описывает проблему с `GetRealPathFromURI` в Android 15 при загрузке файлов на сервер PHP и предлагает решения, включая копирование файла и использование `InputStream` напрямую.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS