News & Events
Como visualizar arquivos PDF em Android 7.+ com Delphi?
- 14 de fevereiro de 2021
- Posted by: Adriano Santos
- Category: Notícias
Deixe-me ler sua mente: seu aplicativo mobile Android precisa acessar e/ou receber um arquivo PDF e em seguida possibilitar o usuário visualizá-lo, uma DANFE ou um Relatório por exemplo, e então vários erros ocorrem? Eu consigo te ajudar nessa etapa. Vamos lá?
O desenvolvimento mobile é uma tarefa árdua e cheia de desafios. O Delphi nos facilita muito o trabalho no dia a dia e muitas dessas tarefas se tornam brinquedo de criança na mão de desenvolvedor. Mas nem tudo são flores e em alguns momentos há mais pedras e espinhos do que imagina. Por isso minha principal missão é orientá-lo e apontar o caminho certo.
O que o Google está fazendo?
Há quanto tempo você desenvolve para desktop? 5 anos, 10 anos, mais de 20 anos como Eu? Independente disso, o desenvolvimento para desktop é muito mais fácil pois estamos falando de um sistema operacional com mais anos de estrada e, por isso, mais consolidado e sem tantos “grandes lançamentos”. Normalmente o que se desenvolve hoje com uma IDE/Linguagem nova, funciona facilmente em versões antigas e novas do Windows. Porém, no mobile o desafio é maior.
Android e iOS são sistemas relativamente novos em comparação aos demais e tanto Google quanto Apple têm modificado bastante seus sistemas operacionais a cada nova versão.
Segurança, privacidade, novos recursos de interface, uso facilitado, enfim, são inúmeras as alterações nos SO’s.
Falando especificamente sobre Android, as políticas de segurança e privacidade estão sendo modificadas fortemente desde a versão 8.0, então coisas que “podíamos” fazer antes…hoje não é possível sem as devidas permissões.
E por falar em permissões, elas já eram necessárias desde algumas versões antes do 8.0, a diferença é que antes bastávamos acrescentar algumas TAG’s no AndroidManifest.xml e tudo perfeito, o próprio Android solicitava a permissão ao usuário final, tais como Câmera, acesso a biblioteca de fotos, leitura e gravação nas pastas do app e compartilhadas, enfim, tudo que o app precisava usar, o Android avisava ao usuário.
Isso continua. Marque o que precisa usar no seu app em Project > Options > Uses Permissions e salve o projeto. O Delphi irá automaticamente “informar” ao sistema operacional que precisa usar tais recursos. A questão é que agora (a partir da versão 8.1+), além de marcarmos o que vamos usar, temos que necessariamente “invocar via programação” a solicitação de permissão ao usuário. Foi exatamente pensando nisso que criei o componente MobilePermissions gratuito que pode ser baixado em nosso GitHub.
Esse é o primeiro passo para prosseguir nesse artigo, a menos que já faça esse trabalho de permissões sem uso de componentes e/ou com suas próprias funções.
As permissões que precisará para esse artigo são READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE e marcar a opção Secure File Sharing presente em Project > Options > Application > Entitlement List > Secure File Sharing. Não esqueça de escolher Android como Target, caso contrário não aparecerá a última opção informada.
Baixe meu componente, instale no Delphi e no evento de sua preferência (eu prefiro no OnCreate do form principal), adicione as linhas:
MobilePermissions1.Dangerous.ReadExternalStorage := True;
MobilePermissions1.Dangerous.WriteExternalStorage := True;
MobilePermissions1.Apply;
Pronto, assim que o app for executado no Android, o usuário será perguntado se deseja dar acesso ao app. #Ficadica
O problema do PDF no Android
Bem, visto tudo isso que falei antes, eis o problema. Até a API 26 do Android, bastava chamar o intent de visualização do arquivo passando o caminho dele usando a classe TPath presente em System.IOUtils, meia dúzia de linhas e pronto! Arquivo PDF no ar. Eis um exemplo de código:
var
FName : String;
{$IFDEF ANDROID}
Intent : JIntent;
URI : Jnet_Uri;
{$ENDIF}
begin
FName := TPath.GetDocumentsPath + PathDelim + 'Motorola_One.pdf';
if TFile.Exists(FName) then
ShowMessage('Arquivo presente na pasta');
{$IFDEF ANDROID}
URI := TJnet_Uri.JavaClass.parse(StringToJString('file:/' + fName));
intent := TJIntent.Create;
intent.setAction(TJIntent.JavaClass.ACTION_VIEW);
intent.setDataAndType(URI,StringToJString('application/pdf'));
SharedActivity.startActivity(intent);
{$ENDIF}
end;
No trecho de código acima, estamos levando em consideração que o arquivo PDF com nome “Motorola_One.pdf” foi incorporado ao APK do aplicativo através do Project > Deployment na pasta “.\assets\internal”, que depois é representada dentro do dispositivo (depois de instalado) como algo começando com “\data\emulate\…”, etc (não precisamos entrar no detalhe).
Esse caminho é obtido por TPath.GetDocumentsPath, dessa forma estamos concatenamento com PathDelim (A barra intervita “/”) e o nome do arquivo. Depois instanciamos um intent, “setamos” como ACTION_VIEW para visualização no Android e ajustamos o “Data” e “Type” do arquivo que será aberto “application/pdf“. Por fim invocamos o método para “levantar” a Activity para mostrar o arquivo.
Essa última etapa faz com que o Android entenda que queremos visualizar o arquivo então procura por um visualizador padrão. Caso exista mais de um visualizador, o usuário é indagado sobre ONDE deseja visualizar o PDF podendo ou não marcar um deles como padrão.
Até aqui, moleza. Sem nenhum problema. Já escrevi artigos, gravei vídeos e todos entenderam.
Por que não funciona mais essa abordagem?
Simples: porque o Google mudou. 😉
Como mencionei anteriormente, o Google tem modificado diversas políticas e solicitado cada vez mais informações e permissões para evitar constrangimentos e violações de segurança e privacidade, por isso muita coisa vai parando de funcionar conforme vamos avançando no desenvolvimento. Eu desenvolvi uma Unit chamada xPlat.OpenPDF.pas disponível gratuitamente para quem desejar facilitar esse trabalho de abertura do PDF, inclusive no iOS.
Acontece que a classe parou de funcionar a partir da versão 26 da API por conta de mudanças do Android. Fiz uma atualização nessa classe, invoquei métodos de uma outra suíte muito conhecida chamada KastriFree da galera do DelphiWords (top demais inclusive) e que dá suporte a um novo recurso (se não quiser chamar de regra) do Android, o File Provider, na verdade uma classe pública.
Todos os detalhes estão na referência em develper.android.com/reference. Para API acima de 26, atualmente até o fechamento desse artigo obrigatório mínimo 28, é necessário fazer uso do File Provider que facilita a leitura e compartilhamento de arquivos. É exatamente isso que precisamos fazer para visualizar os PDFs no Android 8.1 (API 27) em diante.
Android 7, 8, 9…10…11? Como gerenciar tudo isso de versão?
Pois é. Já entendeu qual é da parada né? Para Android 8.1 em diante, precisamos do File Provider. Antes disso, não é necessário. Como fazer nosso app funcionar em todos…ou pelo menos na maioria dos aparelhos?
Bom, se seu caso enquadra-se apenas com versões anteriores a 8.1 (o que acho difícil) basta copiar a função lá em cima, declarar as uses corretas do Delphi referentes ao uso de intents, e já era. Mas se chegou até esse artigo, é praticamente certeza que precisa de compatibilidade com File Provider. Vamos lá?
Incluindo o File Provider no seu app Android
Primeiro do que tudo, aqui vão meus créditos ao amigo Jacques Nascimento do canal Imperium Delphi que ajudou a esclarecer todos os “Qs” desse problema. Muito obrigado!
A resolução do problema consiste em incluir o File Provider no projeto como já mencionei. Vamos começar adicionando as Units necessárias para uso. Vou considerar que está desenvolvendo (compilando para Android) mas vou usar diretivas de compilação caso queira compilar em Windows.
{$IFDEF ANDROID}
Androidapi.JNI.GraphicsContentViewText,
Androidapi.JNI.provider,
Androidapi.JNI.JavaTypes,
Androidapi.JNI.Net,
Androidapi.JNI.App,
AndroidAPI.jNI.OS,
Androidapi.JNIBridge,
FMX.Helpers.Android,
IdUri,
Androidapi.Helpers,
FMX.Platform.Android,
{$ENDIF}
Acima declaramos tudo que precisaremos, não esqueça de nenhuma Unit. Além delas, se estiver usando nosso componente de permissões, o Delphi adicionará automaticamente as units necessárias. Caso não esteja usando, saiba que para visualizar o arquivo PDF, precisamos de permissão do usuário.
Agora vamos declarar o FileProvider através do trecho de código abaixo:
type
JFileProvider = interface;
JFileProviderClass = interface(JContentProviderClass)
['{33A87969-5731-4791-90F6-3AD22F2BB822}']
{class} function getUriForFile(context: JContext; authority: JString; _file: JFile): Jnet_Uri; cdecl;
{class} function init: JFileProvider; cdecl;
end;
[JavaSignature('android/support/v4/content/FileProvider')]
JFileProvider = interface(JContentProvider)
['{12F5DD38-A3CE-4D2E-9F68-24933C9D221B}']
procedure attachInfo(context: JContext; info: JProviderInfo); cdecl;
function delete(uri: Jnet_Uri; selection: JString; selectionArgs: TJavaObjectArray<JString>): Integer; cdecl;
function getType(uri: Jnet_Uri): JString; cdecl;
function insert(uri: Jnet_Uri; values: JContentValues): Jnet_Uri; cdecl;
function onCreate: Boolean; cdecl;
function openFile(uri: Jnet_Uri; mode: JString): JParcelFileDescriptor; cdecl;
function query(uri: Jnet_Uri; projection: TJavaObjectArray<JString>; selection: JString; selectionArgs: TJavaObjectArray<JString>;
sortOrder: JString): JCursor; cdecl;
function update(uri: Jnet_Uri; values: JContentValues; selection: JString; selectionArgs: TJavaObjectArray<JString>): Integer; cdecl;
end;
TJFileProvider = class(TJavaGenericImport<JFileProviderClass, JFileProvider>) end;
Apenas faça a declaração das classes e interfaces acima citadas. Nenhuma outra ação é necessária nesse ponto.
Agora vamos fazer a criação do método GetFileUri que vai pegar o caminho exato da URI que será usada para abrir nossos arquivos. Não vou entrar em detalhes, para entender melhor basta ler a documentação sobre o assunto nesse link de referência do Android.
Crie o método como na linha abaixo na seção private do seu projeto e pressione Ctrl + Shift + C para criar o escopo do método em Implementation.
function GetFileUri(aFile: String): JNet_Uri;
Por fim implemente a função como abaixo:
function TForm1.GetFileUri(aFile: String): JNet_Uri;
var
FileAtt : JFile;
Auth : JString;
PackageName : String;
begin
PackageName := JStringToString(SharedActivityContext.getPackageName);
FileAtt := TJFile.JavaClass.init(StringToJString(aFile));
Auth := StringToJString(Packagename+'.fileprovider');
Result := TJFileProvider.JavaClass.getUriForFile(TAndroidHelper.Context, Auth, FileAtt);
end;
Agora desenhe uma interface mobile simples, apenas com um Botão, um Label, um Switch e um TNetTHttpClient (Name = http). No meu caso estou usando meu componente de permissões. Veja um exemplo de tela:
A ideia do Switch na tela é podermos abrir um arquivo PDF incorporado ao APK quando desmarcado e quando marcado carregar um arquivo através de uma URL, baixar e então exibir localmente no mobile.
Clique duas vezes sobre o botão para inserirmos a codificação. No passo seguinte vamos entender exatamente o que estamos fazendo.
procedure TForm1.Button2Click(Sender: TObject);
var
Str : TStringStream;
path : String;
Intent : JIntent;
URI : JNet_Uri;
SPathDocs : string;
SFile : string;
begin
SPathDocs := System.IOUtils.TPath.GetDocumentsPath + PathDelim;
Path := System.IOUtils.TPath.GetSharedDocumentsPath + PathDelim + 'tmp' + PathDelim;
if Not TDirectory.Exists(Path) then
TDirectory.CreateDirectory(Path);
if Switch1.IsChecked then
begin
SFile := 'Motorola_One.pdf';
TFile.Copy(SPathDocs + SFile, Path + SFile, True);
end
else
begin
SFile := 'printid.pdf';
Str := TStringStream.Create;
Http.Get('https://www.controlid.com.br/userguide/printid.pdf', Str);
Str.Position := 0;
Str.SaveToFile(Path + SFile);
Str.DisposeOf;
end;
Intent := TJIntent.JavaClass.init(TJintent.JavaClass.ACTION_VIEW);
Uri := GetFileURI(Path + SFile);
Intent.setDataAndType(Uri, StringToJString('application/pdf'));
Intent.setFlags(TJintent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION);
TAndroidHelper.Activity.startActivity(Intent);
end;
Em primeiro lugar declaramos algumas variáveis para nos auxiliar no trabalho. Criamos uma variável para Stream (Str) onde faremos a gravação localmente do arquivo baixado da url. Em seguida Intent e URI para que possamos instanciar e executar nosso Activity.
Também criamos duas variáveis string para identificarmos o nome do arquivo e o caminho da pasta padrão de documentos do aplicativo.
Começamos o trecho principal do código do botão capturando e armazenando em variável o caminho da pasta de compartilhamento e de documentos do Android.
Verificamos se a pasta “tmp” existe dentro da pasta de compartilhamentos, caso contrário a criamos utilizando o TDirectory.CreateDirectory(Path).
if Switch1.IsChecked then
begin
SFile := 'Motorola_One.pdf';
TFile.Copy(SPathDocs + SFile, Path + SFile, True);
end
else
begin
SFile := 'printid.pdf';
Str := TStringStream.Create;
Http.Get('https://www.controlid.com.br/userguide/printid.pdf', Str);
Str.Position := 0;
Str.SaveToFile(Path + SFile);
Str.DisposeOf;
end;
No trecho anterior verificamos se o Switch está ligado. Se estiver, guardamos o nome do arquivo ‘Motorola_One.pdf’ na variável SFile. Esse arquivo foi incorporado ao APK através do Project > Deployment. Por fim copiamos esse arquivo para a pasta compartilhada com TFile.Copy.
Se o switch não estiver marcado, então que faremos a carga do arquivo PDF direto pela internet. Usei um arquivo qualquer que encontrei, nesse caso o manual da minha impressora não fiscal presente no endereço:
https://www.controlid.com.br/userguide/printid.pdf
Criamos um TStream, baixamos o PDF para ele, posicionamos e salvamos usando o SaveToFile. Pronto, o arquivo está gravado localmente.
Obs. Aqui vem um segredo. No bloco anterior pegamos o arquivo da pasta de documentos e copiamos para a pasta compartilhada. No segundo bloco do IF, salvamos direto na pasta compartilhada (variável Path).
O código final
Perfeito, até aqui tranquilo. Agora só a parte final de execução do arquivo PDF. No código abaixo você verá as últimas linhas do nosso exemplo. Nós marcamos o Intent inicializando ele com a JavaClass ACTION_VIEW. Isso invocará o Android e ele entenderá que desejamos visualizar o arquivo.
Na sequência pegamos a URI completa do arquivo através da função GetFileURI que codificamos lá em cima. Logo em seguida definimos o seu “Data” e “Type” como “application/pdf” passando também o URI recebido na linha anterior.
É importante “setarmos” a flag “FLAG_GRANT_URI_PERMISSION“, nós estamos fazendo isso em dois locais específicos, apenas pra termos certeza. Uma através do setFlags no código e a outra no AndroidManifest, veremos mais abaixo.
A última linha é a chamada e execução propriamente dita do Activity;
TAndroidHelper.Activity.startActivity(Intent);
Na grande maioria dos casos apenas isso é suficiente e tudo correrá bem. Em meus testes todo esse procedimento funcionou nas seguintes situações:
Delphi 10.3.3 Rio com API 29
-> Testado em Android 7, 8.1 e 10
Delphi 10.4.1 Sidney com API 29
-> Testado em Android 7, 8.1 e 10
Ainda não acabou. Tem a cereja do bolo
Se você executou todo esse procedimento, tomou todos os cuidados, mas no seu Android não funcionou…talvez precise fazer mais uma pequena configuração e uma observação.
O Android é um sistema operacional aberto, portanto as fabricantes tem a oportunidade de baixar o kernel, modificá-lo (dentro das diretivas que o Google passa) e fazer pequenas alterações, boas ou ruins.
Em alguns aparelhos sem nenhuma alteração adicional, o exemplo funcionou de boas. Testamos em um Motorola G9 Play, um Moto C Plus com Android 10 e em um Redmi 9S e os resultados foram diferentes.
No Remi nenhuma alteração no que foi visto precisou ser efetuada, já para o Motorola G9 Play foi preciso abrir o arquivo AndroidManifest.Template.xml presente na raiz do projeto Delphi e adicionar duas linhas ao final da tag application.
<application android:persistent="%persistent%"
android:restoreAnyVersion="%restoreAnyVersion%"
android:label="%label%"
android:debuggable="%debuggable%"
android:largeHeap="%largeHeap%"
android:icon="%icon%"
android:theme="%theme%"
android:hardwareAccelerated="%hardwareAccelerated%"
android:resizeableActivity="false"
android:grantUriPermissions="true"
android:requestLegacyExternalStorage="true">
A penúltima linha refere-se a tag GRANT_URI_PERMISSION que já havíamos inserido via programação pouco antes. E a segunda linha foi crucial no Motorola G9.
...
android:requestLegacyExternalStorage="true">
Sem ela não foi possível abrir o arquivo PDF, por isso é importantíssimo observar todos os pontos, aparelhos e versões do Android.
Onde o procedimento desse artigo não funcinou?
Rodei o mesmo código usando Delphi 10.3.3 e 10.4.1 em um equipamento destinado a pagamentos por cartão de crédito e débito. O processo não funcionou e é natural, haja vista que demandamos mais segurança nesse equipamento e então alguns recursos do Android são travados para uso.
Conclusão
Construir aplicativos móveis pode ser muito divertido, mas podemos navegar em águas bastante turvas se não conhecermos corretamente os caminhos. Entender como o sistema operacional funciona, suas nuances, atualizações e principalmente entender exatamente o que se está fazendo e seus motivos, nos dá mais controle e tranquilidade na hora de desenvolver.
Espero que o artigo tenha o ajudado.
Bora estudar? 😉
Obrigado, bons estudos e até a próxima.