Input File no WebView Android
(15747) (5)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos construir um código Android que permita que a tag HTML <input type="file"> funcione para as versões Android mais utilizadas em mercado.
Quando digo "...mais utilizadas" estou me referindo a deixar de fora desse suporte menos de 1.5% dos usuários do Android, número que diminui a cada dia com o Google removendo o suporte às versões mais antigas do sistema operacional mobile deles.
Você provavelmente deve estar se perguntando: mas não já existem várias soluções para este problema, digo, o problema do <input type="file"> no WebView?
Na verdade não há uma "solução concreta", digo, não há uma solução que funcione para a maioria das APIs Android mais utilizadas.
Eu mesmo recomendo a solução a seguir, nos links do blog: Solução WebView Input File - Stackoverflow.
Mas já lhe adianto: a solução do link anterior não esta marcada como correta e nenhuma outra da página em que ela se encontra.
Frequentemente a recomendo como uma opção para os programadores que vem até mim com esse problema, mas nem sempre ela funciona.
Devido a este ser um problema antigo e ainda não ter uma solução definitiva, neste artigo vamos construir um algoritmo que envolve mais lógica de negócio do que tipo de tecnologia para um determinado tipo de API ou aparelho.
Conseguindo assim utilizar o <input type="file"> no WebView, nas APIs Android mais atuais (a partir da API 15, Ice Cream Sandwich).
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber, em primeira mão, todos os conteúdos de desenvolvimento Android exclusivos aqui do Blog...
... e também as versões em PDF de cada novo conteúdo (as versões em PDF são liberadas somente aos inscritos na lista de e-mails).
Abaixo os tópicos que estaremos abordando aqui no artigo:
- O problema: WebView x Input File;
- Como funciona hoje e a solução proposta no artigo;
- Projeto Web de exemplo:
- Projeto Android de exemplo:
- Configuração Gradle;
- Configuração AndroidManifest;
- Configuração Layout;
- Definição MainActivity e inicialização do WebView;
- Definição das classes do domínio do problema;
- Configurando a library Image Picker e escolhendo imagens;
- Criando hackcode para trabalho com diferentes APIs Android;
- Configurando a library Retrofit;
- Enviando dados para o back-end Web.
- Pontos negativos;
- Vídeo com implementação passo a passo do projeto;
- Conclusão;
- Fontes.
O problema: WebView x Input File
Principalmente para aqueles que estão iniciando no dev Android, descobrir que o Input File não funciona de maneira trivial como qualquer outra tag HTML é um ponto chave para persistir ou não utilizando WebView.
Pois muitas vezes o fornecimento de imagem de perfil, por exemplo, é requisito obrigatório no projeto de software.
Buscando em fóruns e na documentação, as vezes há respostas, mas o problema do Input File, até o momento, não foi resolvido, ao menos não tem uma solução única para as principais APIs em uso.
Apesar do conteúdo desse artigo ser direcionado a programadores Android que utilizam WebView ou que estão começando no mundo Android por essa View, eu fortemente recomendo que você estude também o dev Android Java API.
A limitação do Input File no WebView é apenas uma das várias limitações dessa View.
A execução de vídeos via código HTML é uma outra dor de cabeça para desenvolvedores que utilizam somente essa View.
Mesmo com suas limitações, para alguns casos (clientes e velocidade na entrega, por exemplo) temos o WebView como sendo a melhor solução, ainda mais quando o site já tem o CSS que o deixa responsivo.
Com isso vamos prosseguir com a apresentação e o projeto Web e Android de exemplo.
Como funciona hoje e a solução proposta no artigo
Assumindo um projeto Web convencional, mais precisamente um projeto Web com formulário HTML.
Quando preenchemos os dados e os enviamos o fluxo de processamento é similar ao da figura abaixo:
Para esse mesmo projeto Web, quando temos a opção de fornecimento de arquivo, o fluxo de processamento é como segue na figura abaixo:
Devido a maturidade das tecnologias na Web, pouco temos de fazer para ter todos esses processos funcionando corretamente.
Para os desenvolvedores Web que acreditaram que com o WebView teriam a mesma facilidade, acabaram tendo de buscar soluções para seus códigos.
Pois ao menos o fluxo de processos do "File Chooser Nativo" não é trivial quando estamos utilizando o WebView Android.
Para a solução proposta aqui, quando o front-end do projeto Web detectar que o device em uso é um device Android, digo, o site está dentro de um WebView.
Nesse caso o fluxo do envio do formulário será como o da figura abaixo:
Ressalto que caso seu projeto Web, que vai rodar em um WebView, não tenha necessidade de fornecimento de arquivos, imagens, ... nos formulários dele.
Nesse caso seu software híbrido, Web e Android, não tem a necessidade de ter o fluxo acima, alias essa seria uma péssima escolha.
O fluxo eficiente para ele seria o equivalente ao da primeira figura dessa seção.
Com um file chooser disponível em algum dos formulários de sua aplicação (como o do exemplo aqui no artigo), para esse formulário em um WebView o fluxo de escolha do arquivo seria como o da figura a seguir:
Essa será a solução apresentada, envolvendo os dois últimos fluxos descritos acima.
Com isso podemos prosseguir para os códigos.
Projeto Web de exemplo
O objetivo desse artigo é apresentar uma possível (e funcional) solução para seu sistema Web no qual você está migrando para uma APP Android por meio do uso do WebView.
Ou seja, o código de nosso projeto de exemplo Web, se posto aqui, poderia lhe atrapalhar, pois ele também tem uma série de algoritmos, classes e arquivos auxiliares.
Nesse caso você deve acompanhar o artigo, com o projeto Web e Android do exemplo, se possível, para entender a lógica utilizada e então utilizar o seu projeto Web.
Para seguir com o projeto Web do exemplo (fortemente recomendo que faça isso), baixe ele no GitHub: https://github.com/viniciusthiengo/webview-user-signup-web.
Note que como linguagem de back-end utilizei o PHP. Como servidor local, ou melhor dizendo, pacote de aplicações local utilizei o MAMP. Caso esteja com o Windows você tem o WAMP. Caso esteja com o Linux, você tem o LAMP.
Não se preocupe com sistema de banco de dados, não utilizei nenhum banco relacional. Nesse projeto estamos manipulando um arquivo .txt. Sério!
Essa abordagem foi mais que o suficiente.
Assumindo que está seguindo com o projeto Web de exemplo, assim que executa-lo em seu servidor local ou de produção, terá uma página similar à seguir:
Clicando em "Escolher imagem" e logo depois preenchendo os campos "Email" e "Password" e seguindo com o envio do formulário com o clique em "Cadastrar" você terá algo similar a figura abaixo:
Destrinchando os diretórios do projeto Web você verá que o arquivo /data/data.txt foi alterado e o diretório /img ganhou uma nova imagem, com um nome não legível e aleatório:
É assim que esse projeto funciona, para manter a simplicidade não coloquei validação de dados de entrada.
Caso esteja interessado, a fonte utilizada no projeto é a Bungee Shade.
No Google Fonts tem muitas boas fontes, além de serem gratuitas.
Se notou, o projeto está também com o Favicon definido.
Para gerá-lo utilizei uma imagem do projeto no site Favicon Generator.
Estrutura HTML do index.php
Abaixo é apresentado o conteúdo HTML que terá ligação direta com os códigos Java Android.
Segue página index.php:
<!DOCTYPE html>
<html lang="pt-br">
<head>
<?php
include_once('header.php');
?>
</head>
<body>
<?php
include_once('top.php');
?>
<form id="form-sign-up" method="post" action="../package/ctrl/CtrlUser.php" enctype="multipart/form-data">
<div class="box-img">
<img src="../img/default.png" width="150" height="150">
<input type="file" id="in-img" name="in-img">
<a class="bt-load" href="#" title="Escolher imagem">
<span class="bg"></span>
<span class="label">
Escolher imagem
</span>
</a>
<a href="#" title="Remover" class="bt-remove">
<i class="fa fa-trash" aria-hidden="true"></i>
</a>
<div class="box-loading">
<div class="fade"></div>
<div class="label">
Carregando...
</div>
</div>
</div>
<input type="email" id="in-email" name="in-email" placeholder="Email">
<input type="password" id="in-password" name="in-password" placeholder="Senha">
<input type="hidden" id="in-method" name="in-method" value="form-sign-up">
<button id="in-submit" type="submit" title="Cadastrar">
Cadastrar
</button>
</form>
<?php
include_once('copyright.php');
?>
<!-- SCRIPT JAVASCRIPT VEM AQUI -->
</body>
</html>
Omiti os códigos JavaScript, pois falaremos sobre eles na seção abaixo.
De qualquer forma, note a marcação das tags de formulário, digo, de todo o conteúdo no contexto da tag <form>.
A marcação é a convencional, incluindo os atributos da tag <form>.
Estaremos mudando pouca coisa nesse código HTML, mais precisamente estaremos alterando o conteúdo da tag <img> para darmos suporte a devices com Android abaixo da API 19, mas isso abordaremos mais a frente no artigo.
Código jQuery essencial
Rodando o projeto e executando como explicado na seção Projeto Web de exemplo você notará que não há na página o botão convencional da tag Input File.
Isso, pois com o CSS nós escondemos essa tag e modificamos o modo de trabalho de uma tag <a> para que o clique nessa tag ative o Input File escondido.
Veja abaixo o código jQuery da página:
...
<script src="https://code.jquery.com/jquery-3.1.1.min.js"
integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="
crossorigin="anonymous"></script>
<script type="text/javascript">
/* BOX-IMG BOTÃO QUE RETORNA A IMAGEM PADRÃO - REMOVER IMAGEM CARREGADA */
$('div.box-img a.bt-remove').click(function( e ){
e.preventDefault();
$(this).siblings('img').prop('src', '../img/default.png');
$(this).hide();
});
/* MUDANDO O MODO DE TRABALHO DO LINK DA BOX-IMG PARA ATIVAR O INPUT FILE */
$('div.box-img a.bt-load').click(function( e ){
e.preventDefault();
showHideLoadingBox( true );
$(this).siblings('input[type=file]').trigger('click');
});
/* OUVINDO MUDANÇAS DE VALOR NO INPUT FILE */
$('div.box-img input[type=file]').change(function(){
var $handle = $(this);
var reader = new FileReader();
showHideLoadingBox( false );
reader.onload = function(e){
/* VERIFICA SE FOI REALMENTE UMA IMAGEM CARREGADA, CASO NÃO, ABORTE O PROCESSAMENTO */
if( e.target.result.indexOf('data:image/') == -1 ){
$handle.siblings('a.bt-remove').trigger('click'); /* VOLTA COM A IMAGEM PADRÃO */
return;
}
loadImageSrc( e.target.result );
};
reader.readAsDataURL(this.files[0]);
});
function showHideLoadingBox( status ){
if( status ){
$('div.box-loading').stop().show('fast');
}
else{
$('div.box-loading').stop().hide('fast');
}
}
function loadImageSrc( imagePath ){
/* UMA IMAGEM FOI ESCOLHIDA, COLOQUE-A NA TAG IMG DO FORMULÁRIO */
$('div.box-img img').prop('src', imagePath);
$('div.box-img a.bt-remove').show();
}
</script>
...
Esse é o exato código omitido na demonstração da página index.php da seção anterior.
Também trabalhamos um botão de remoção de imagem.
O jQuery que estamos utilizando está no CDN provido pelo próprio site da library, jQuery.com.
Caso não tenha intimidade com o jQuery e sim com outra tecnologia, AngularJS, por exemplo.
Pode utiliza-la, não há necessidade de ser o jQuery, até mesmo JavaScript puro é válido (mas com muito mais código).
Vamos voltar a esse script da página index.php para realizar algumas modificações, incluindo o código de identificação de device Android.
Note o uso da classe FileReader. Essa é maneira segura de termos acesso a uma preview do arquivo selecionado. Em nosso exemplo, uma imagem.
Ressalto que o projeto aqui é para solução de suporte ao Input File em devices Android.
Para devices com IOS ou qualquer outro mobile OS, busque nos fóruns deles caso tenham a mesma limitação como com o WebView.
Projeto Android de exemplo
Como fiz com o projeto de exemplo versão Web, com o projeto Android também há um GitHub, para acesso completo, incluindo recursos, entre em: https://github.com/viniciusthiengo/webview-user-signup-android.
Mas diferente do projeto Web, nesse vamos apresentar todos os arquivos necessários para um projeto Android funcional.
Isso, pois você pode estar partindo exatamente desse ponto: um novo projeto Android.
O projeto Web no Android rodará como se estivesse vindo de uma página Web pública, ou seja, não utilizaremos HTML local, no folder assets, por exemplo.
Mas a solução proposta aqui funciona também para o projeto com HTML local no Android, sem quase nenhuma alteração.
Para projetos que você não tem controle do HTML, digo, do código front-end completo da página Web.
Para esses projetos a solução aqui não serve, pois alterações no JavaScript serão necessárias.
Caso esteja nessa situação, veja se o link do Stackoverflow indicado no início do artigo lhe ajuda.
No final da codificação do desse projeto Android vamos ter um algo como o apresentado na figura abaixo:
Crie um novo projeto Android, um projeto "Empty", com o nome WebView User Sign Up.
Então siga com a configuração.
Configuração Gradle
Abaixo a configuração do Gradle Top Level, ou build.gradle (Project: WebViewUserSignUp):
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
}
}
allprojects {
repositories {
jcenter()
maven { url "https://jitpack.io" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Note que em allprojects adicionamos maven { url "https://jitpack.io" }, pois é nesse repositório que se encontra a library ImagePicker que vamos utilizar.
Agora a configuração do Gradle APP Level, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "24.0.3"
defaultConfig {
applicationId "br.com.thiengo.webviewusersignup"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.0.0'
testCompile 'junit:junit:4.12'
compile 'com.github.nguyenhoanglam:ImagePicker:1.1.2'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.google.code.gson:gson:2.7'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}
O que foi alterado / adicionado está destacado. As dependências são referentes as libraries ImagePicker, Retrofit e Gson (uma dependência do Retrofit).
Note que alteramos a minSdkVersion para 15, pois a library de ImagePicker sendo utilizada dá suporte somente a partir dessa versão de API.
Na época desse artigo ainda havia 1.4% de usuários com Android APIs abaixo da 15. Estude o Android Dashboard para saber se realmente vale a pena o suporte para versões muito antigas.
Caso realmente precise desse suporte a versões anteriores a API 15, veja se alguma das libraries de Image Picker no Android Arsenal têm esse suporte ainda mais abrangente.
Configuração AndroidManifest
Com o AndroidManfest.xml não alteramos muita coisa.
Somente adicionamos algumas permissões e a orientação como somente Portrait para a atividade do WebView.
Segue:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.webviewusersignup">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Você provavelmente deve estar se perguntando: permissões de leitura e acesso ao SDCard, isso significa que terei de colocar todo aquele código de solicitação de permissão em tempo de execução?
Não, não será necessário.
Essas permissões, READ_EXTERNAL_STORAGE e WRITE_EXTERNAL_STORAGE, são sim dangerous permissions no Android.
Porém o código que precisa dela é o código da library ImagePicker e essa já manipula a solicitação de permissão dentro dos códigos dela.
A permissão de Internet é necessária, pois o código que estaremos utilizando, digo, as páginas Web, serão de origem externa ao Android.
A orientação somente Portrait é opcional.
Configuração Layout
O único layout que teremos é o da MainActivity, /layout/activity_main.xml.
Segue:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="br.com.thiengo.webviewusersignup.MainActivity">
<WebView
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:id="@+id/wb_content"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
Simples, não?! Um outro XML que quero apresentar é o /values/styles.xml:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="android:windowNoTitle">true</item>
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Note que os trechos destacados não vem configurados em um novo projeto Android "Empty", logo, terá de adiciona-los ao seu, caso esteja acompanhando no código do projeto de exemplo.
Esses trechos destacados servem para que não seja utilizada a Toolbar padrão dos projetos Android.
Para as cores chave do projeto, utilizei as seguintes (/values/colors.xml):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#FF5722</color>
<color name="colorPrimaryDark">#E64A19</color>
<color name="colorAccent">#FFEB3B</color>
</resources>
Caso queira buscar um novo conjunto de cores, tente o Material Palette.
Definição MainActivity e inicialização do WebView
E então vamos ao Java API, abaixo o código inicial da MainActivity:
public class MainActivity extends AppCompatActivity implements Observer {
private WebView webView;
private UserJS userJS;
@Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
setContentView( R.layout.activity_main );
userJS = new UserJS( this );
webView = (WebView) findViewById( R.id.wb_content );
webView.getSettings().setJavaScriptEnabled( true );
webView.setHorizontalScrollBarEnabled( true );
webView.addJavascriptInterface( this, "Android" );
webView.loadUrl( ServiceGenerator.API_BASE_URL + "view/index.php" );
webView.setWebViewClient( new CustomWebViewClient() );
}
@Override
public void update( Observable observable, Object o ) {
/* TODO */
}
}
Note que no código acima já adiantamos algumas configurações.
O UserJS é uma das classes do domínio do problema do projeto. Você utilizaria classes que respondem ao domínio do problema de seu projeto.
Na próxima seção vamos estar apresentando o código dessa classe e de outras.
A implementação da Interface Observer é necessária para o código que utilizaremos em outra classe do domínio do problema, ImageJS.
O método update() é uma implementação obrigatória da Interface Observer e parte essencial do algoritmo que estaremos construindo.
A classe ServiceGenerator é, principalmente, da lógica de negócio que utilizaremos junto a library Retrofit.
Essa classe encapsula os códigos necessários para a criação de um Retrofit REST adapter.
Nas seções seguintes estaremos abordando o código da classe ServiceGenerator.
E agora o código da classe CustomWebViewClient:
public class CustomWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(
WebView view,
WebResourceRequest request) {
return super.shouldOverrideUrlLoading( view, request );
}
}
Se já utilizou o WebView com links, tags <a>, anteriormente, sabe o que significa o código acima: manter a abertura de novas páginas dentro do WebView da APP e não seguir para o navegador do device mobile.
O código utilizado para inicialização do WebView é tranquilo.
Mas caso esse seja realmente seu primeiro contato com essa View, abaixo deixo alguns artigos com vídeos que tem aqui no Blog sobre WebView:
- WebView no Android, Entendendo e Utilizando
- Monitoramento de Inicio e Fim de Carregamento de Página no WebView
- Integrando WebView Android Com JavaScript de Uma WebPage
Definição das classes do domínio do problema
As classes do domínio do problema desse exemplo são simples.
Vamos começar com a classe ImageJS:
public class ImageJS extends Observable {
private Uri uri;
private String base64;
ImageJS( Observer observer ){
addObserver( observer );
}
public void setUri( String path ){
uri = Uri.parse( path );
}
public File getAsFile(){
return new File( uri.toString() );
}
public String getBase64(){
return base64;
}
public void generateBase64(){
new Thread( new Runnable() {
@Override
public void run() {
Bitmap bitmap = generateBitmap();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(
Bitmap.CompressFormat.PNG,
100,
byteArrayOutputStream
);
byte[] byteArray = byteArrayOutputStream.toByteArray();
String imageBase64 = Base64.encodeToString( byteArray, Base64.DEFAULT );
base64 = "data:image/png;base64," + imageBase64;
setChanged();
notifyObservers();
}
}).start();
}
private Bitmap generateBitmap(){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = BitmapFactory.decodeFile( uri.toString(), options );
return bitmap;
}
}
O método generateBase64() será o que vai permitir que trabalhemos a imagem selecionada, com a library ImagePicker, para posteriormente ser enviada o HTML do WebView.
Para melhor entender os passos de execução desse método, veja a lista a seguir:
- Iniciar Thread secundária para execução assíncrona;
- Transformar imagem do path informado em um Bitmap com o método generateBitmap();
- Comprimir Bitmap;
- Transformar Bitmap comprimido em array de bytes;
- Transformar o array de bytes em um String Base64;
- Notificar os observadores do objeto ImageJS com os métodos setChanged() e notifyObservers().
Devido a quantidade de passos e delay de processamento desses, é necessário colocar essa execução em uma Thread secundária, caso contrário são grandes as chances de uma Exception.
Note que ImageJS herda de Observable.
É nessa classe que nossa MainActivity ficará inscrita, isso para receber a imagem em base64 assim que terminar o processamento em generateBase64().
Estamos assim implementando a versão nativa do Java do Padrão de Projeto Observer.
Se quiser saber mais sobre padrões e técnicas de código limpo, tenho uma coleção desses conteúdos em meu livro Refatorando Para Programas Limpos.
Agora podemos seguir com nossa última classe de domínio do problema, UserJS:
public class UserJS {
private String method;
private String email;
private String password;
private ImageJS imageJS;
public UserJS( Observer observer ){
imageJS = new ImageJS( observer );
}
public RequestBody getMethodRequestBody() {
return RequestBody.create(
MediaType.parse( "multipart/form-data" ),
method );
}
public void setMethod( String method ) {
this.method = method;
}
public RequestBody getEmailRequestBody() {
return RequestBody.create(
MediaType.parse( "multipart/form-data" ),
email );
}
public void setEmail( String email ) {
this.email = email;
}
public RequestBody getPasswordRequestBody() {
return RequestBody.create(
MediaType.parse( "multipart/form-data" ),
password );
}
public void setPassword( String password ) {
this.password = password;
}
public ImageJS getImageJS() {
return imageJS;
}
public MultipartBody.Part getImageJSRequestBody() {
File file = imageJS.getAsFile();
RequestBody requestFile = RequestBody.create(
MediaType.parse("multipart/form-data"),
file );
return MultipartBody
.Part
.createFormData( "in-img", file.getName(), requestFile );
}
}
Essa classe é responsável por manter todos os dados do formulário que estará no WebView e também por auxiliar as entidades Retrofit, encapsulando trechos de código necessários para envio dos dados ao back-end Web.
Note que no construtor de UserJS inicializamos a variável imageJS e inscrevemos a MainActivity nela como entidade observadora.
Configurando a library Image Picker e escolhendo imagens
No Gradle APP Level já adicionamos a referência a essa library.
Lembrando que é devido a ela que a API mínima de suporte é a API 15.
Para acessar o GitHub dessa library entre em: https://github.com/nguyenhoanglam/ImagePicker.
Para essa configuração do ImagePicker voltamos a MainActivity.
Acrescente o seguinte método:
...
@JavascriptInterface
public void callGallery(){
ImagePicker.create( this )
.folderMode( true )
.folderTitle( "Galeria" )
.imageTitle( "Clique para selecionar" )
.single()
.limit( 1 )
.showCamera( true )
.imageDirectory( "Camera" )
.start( REQUEST_IMAGE_CODE );
}
...
O código dessa library é bem simples de entender e utilizar. Do exemplo na página dela no GitHub, somente removi algumas opções que não seriam úteis aqui.
A annotation @JavascriptInterface é necessária, pois será um método no JavaScript da página no WebView que invocará o método callGallery().
No topo da MainActivity vamos declarar a constante REQUEST_IMAGE_CODE:
public class MainActivity extends AppCompatActivity implements Observer {
private static final int REQUEST_IMAGE_CODE = 2546;
...
}
O número inteiro é aleatório.
E agora, ainda na MainActivity, adicione o método onActivityResult() para que seja possível obter o path da imagem selecionada:
...
@Override
protected void onActivityResult(
int requestCode,
int resultCode,
Intent data ) {
super.onActivityResult( requestCode, resultCode, data );
if( resultCode == RESULT_OK && requestCode == REQUEST_IMAGE_CODE ){
ArrayList<Image> images = data.getParcelableArrayListExtra( ImagePickerActivity.INTENT_EXTRA_SELECTED_IMAGES );
if( images != null ){
final Image image = images.get( 0 );
userJS.getImageJS().setUri( image.getPath() );
userJS.getImageJS().generateBase64();
}
}
}
...
Sem sombra de dúvidas essa library é uma baita "mão na roda", pois é possível também utilizar a câmera para fotos.
Veja que logo depois de verificado se há imagem e que tudo está correto, iniciamos o script de geração da String Base64, em modo assíncrono, pois generateBase64(), como apresentado anteriormente, inicia uma nova Thread.
Para finalizar essa seção, veja abaixo como a library ImagePicker é quando em execução:
Criando hackcode para trabalho com diferentes APIs Android
A solução apresentada aqui, devido a algumas limitações do Android OS, tem um trecho onde funciona para APIs acima ou igual a API 19 e outro trecho para as demais APIs.
Para a identificação de API utilizamos a classe Util:
public class Util {
public static boolean isPreKitKat(){
int currentapiVersion = android.os.Build.VERSION.SDK_INT;
return currentapiVersion < android.os.Build.VERSION_CODES.KITKAT;
}
}
Agora, no método update() da MainActivity vamos acrescentar o código necessário para enviar a imagem em formato String Base64 para o código Web no WebView:
...
@Override
public void update(
Observable observable,
Object o ) {
runOnUiThread( new Runnable() {
@Override
public void run() {
String src = userJS.getImageJS().getBase64();
loadWebViewDataSupport( src );
}
});
}
...
O runOnUiThread() é necessário para que seja garantido o uso da Thread principal, pois a invocação do método update() vai vir do método generateBase64() de ImageJS.
Ou seja, invocação dentro de uma Thread secundária.
Mas qual o problema nesse caso?
O problema é que estaremos acessando uma View no método update(), mais precisamente a WebView.
Se isso não fosse feito uma Exception seria gerada. Views somente podem ser acessadas na Thread principal.
Agora acrescente o método abaixo, loadWebViewDataSupport(), logo depois do método update():
private void loadWebViewDataSupport( String srcBase64 ){
if( Util.isPreLollipop() ){
String postData = null;
try {
postData = "image="+ URLEncoder.encode( srcBase64, "UTF-8" );
}
catch( UnsupportedEncodingException e ) {
e.printStackTrace();
}
webView.postUrl(
ServiceGenerator.API_BASE_URL + "view/index.php",
postData.getBytes()
);
}
else{
webView.loadUrl( "javascript:loadImageSrc('" + srcBase64 + "')" );
}
}
Veja que com devices com a API abaixo da 19 temos de realizar um novo carregamento de página. Enviando como dado Post a String Base64 da imagem.
Não tente enviar como Get, pela url da página, pois há um limite de caracteres quando na url e Strings Base64 podem facilmente passar dos 20.000 caracteres, um número muito além do permitido.
Veja novamente o uso da constante API_BASE_URL da classe ServiceGenerator. Na seção abaixo estaremos apresentando ela.
Esse trecho com o envio da imagem em String Base64 para o WebView é opcional, pois o path da imagem já está referenciado na variável uri de ImageJS.
Isso é o necessário para o correto envio da imagem para o back-end Web pelo Retrofit.
Porém nos sistemas Web e Mobile é comum ter um preview do arquivo selecionado, por isso adicionei essa característica a esse exemplo.
Note que aqui estou utilizando somente imagens, mas você poderia utilizar um File Chooser para qualquer tipo de arquivo, somente teria de adaptar, a princípio, as suas classes do domínio do problema.
A partir desse ponto podemos voltar ao código HTML e atualiza-lo, acrescente os códigos destacados:
<?php
/*
* VERIFICANDO SE O CÓDIGO DE SUPORTE PARA APIs
* ABAIXO DA API 19 ESTÁ SENDO UTILIZADO - OBTENDO
* A IMAGEM, NESSE CASO
* */
$imageSrc = isset( $_POST['image'] ) ? $_POST['image'] : '../img/default.png';
?>
<!DOCTYPE html>
<html lang="pt-br">
...
<body>
...
<form
id="form-sign-up"
method="post"
action="../package/ctrl/CtrlUser.php" enctype="multipart/form-data">
<div class="box-img">
<img src="<?php echo $imageSrc; ?>" width="150" height="150">
...
</div>
...
</form>
...
</body>
</html>
Lembre que quando o device estiver com uma API Android abaixo da API 19 o WebView recarregará a página com um dado Post sendo enviado.
O código adicionado acima nos permite trabalhar com uma preview de imagem também para essas versões de API.
O ponto negativo desse recarregamento é que caso a página HTML carregada esteja remota em um servidor Web (caso do nosso exemplo), pode haver um delay grande quando com imagens de muitos bytes.
Mas, a princípio, essa é melhor solução para HTMLs remotos que utilizam recursos locais no Android.
Caso esteja trabalhando com o WebView somente com recursos locais, veja no link a seguir para saber como referencia-los dentro do HTML, dispensando recarregamento de página: Stackoverflow para WebView local resources.
Agora vamos alterar o script jQuery da página acima, index.php, para casos de APIs maiores ou iguais a 19.
Acrescente o código destacado:
...
<script type="text/javascript">
var isAndroid = false;
try{
isAndroid = Android != undefined;
}
catch(e){}
...
/* MUDANDO O MODO DE TRABALHO DO LINK DA BOX-IMG PARA ATIVAR O INPUT FILE */
$('div.box-img a.bt-load').click(function( e ){
e.preventDefault();
showHideLoadingBox( true );
if( isAndroid ){
Android.callGallery(); /* MÉTODO INVOCADO NA MAINACTIVITY */
}
else{
$(this).siblings( 'input[type=file]' ).trigger( 'click' );
}
});
...
function loadImageSrc( imagePath ){
showHideLoadingBox( false ); /* NECESSÁRIO POR CAUSA DO ANDROID */
/* UMA IMAGEM FOI ESCOLHIDA, COLOQUE-A NA TAG IMG DO FORMULÁRIO */
$('div.box-img img').prop( 'src', imagePath );
$('div.box-img a.bt-remove').show();
}
</script>
...
Primeiro o código inicial que permite a identificação se é ou não um device Android. Nesse código você deve ter notado uma "suposta" gambiarra.
Na verdade a escolha de verificação da classe Android foi utilizada para que o site possa se aberto também no navegador convencional do Android, sem problemas com interceptação de nosso código alterado no front-end.
Pois caso contrário o código Web, se estivesse utilizando um código de verificação de device Android comum ao jQuery, não funcionaria em navegadores Android.
Logo depois desse código de verificação, há a utilização dele para poder escolher o trecho de código correto a ser invocado.
O showHideLoadingBox() em loadImageBox() é somente para respeitar a lógica de negócio do projeto e então esconder a caixa de "Carregando..." para imagens, mesmo quando estiver no device Android.
Configurando a library Retrofit
Enfim o código da library Retrofit.
Como no caso do WebView, onde indiquei artigos que já falei sobre ele, detalhando mais o use dessa View.
Com o Retrofit também há um artigo aqui no Blog: Library Retrofit 2 no Android.
Leia ele para saber mais detalhes. Aqui somente vou configurá-lo.
O Retrofit, na lógica de negócio da resolução do problema proposto, será a entidade que permitirá que o formulário seja enviado ao back-end Web.
Vamos começar com nossa Interface de definição de envio, SignUpConnection:
public interface SignUpConnection {
@Multipart
@POST("package/ctrl/CtrlUser.php")
public Call<ResponseBody> sendForm(
@Part("in-method") RequestBody form,
@Part("in-email") RequestBody email,
@Part("in-password") RequestBody password,
@Part("in-is-android") RequestBody isAndroid,
@Part MultipartBody.Part image
);
}
Caso você ainda não tenha enviado arquivos binários pelo Retrofit, essa é a sintaxe, utilizando Multipart.
No artigo sobre ele, indicado anteriormente, trabalho com um exemplo de envio binário, uma imagem.
O isAndroid é necessário para que no back-end Web possamos retornar a resposta correta.
Agora, a classe encapsuladora de criação de um Retrofit REST adapter, ServiceGenerator:
public class ServiceGenerator {
public static final String API_BASE_URL = "http://192.168.25.221:8888/webview-user-signup/";
private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
private static Retrofit.Builder builder = new Retrofit
.Builder()
.baseUrl( API_BASE_URL )
.addConverterFactory( GsonConverterFactory.create() );
public static <S> S createService( Class<S> serviceClass ) {
Retrofit retrofit = builder.client( httpClient.build() ).build();
return retrofit.create( serviceClass );
}
}
E então o trecho de código na MainActivity que permitirá que o código JavaScript do WebView ative o envio de dados pelo Android Retrofit.
Adicione o seguinte método a MainActivity:
...
@JavascriptInterface
public void sendForm(
String method,
String email,
String password ){
userJS.setMethod( method );
userJS.setEmail( email );
userJS.setPassword( password );
SignUpConnection signUpConnection = ServiceGenerator.createService( SignUpConnection.class );
Call<ResponseBody> call = signUpConnection.sendForm(
userJS.getMethodRequestBody(),
userJS.getEmailRequestBody(),
userJS.getPasswordRequestBody(),
RequestBody.create( MediaType.parse( "multipart/form-data" ), "1" ),
userJS.getImageJSRequestBody()
);
call.enqueue(new Callback<ResponseBody>(){
@Override
public void onResponse(
Call<ResponseBody> call,
Response<ResponseBody> response ) {
try {
String url = response.body().string();
webView.loadUrl( "javascript:loadSignUpDonePage('" + url + "')" );
}
catch( IOException e ) {
e.printStackTrace();
}
}
@Override
public void onFailure(
Call<ResponseBody> call,
Throwable t ){
Log.e( "Upload error:", t.getMessage() );
}
});
}
...
Como explicado anteriormente, o Retrofit falo mais sobre no artigo feito somente para ele aqui no Blog, indicado no início da seção.
De qualquer forma, o código acima, junto a outras entidades já apresentadas aqui (UserJS, por exemplo) é responsável pelo envio dos dados ao servidor.
Note o @JavascriptInterface que permite que um código JavaScript no WebView invoque o método sendFom().
Veja também o código dentro de onResponse():
...
try {
String url = response.body().string();
webView.loadUrl("javascript:loadSignUpDonePage('" + url + "')");
} catch (IOException e) {
e.printStackTrace();
}
...
Com esse código obtemos a resposta do back-end Web e então a enviamos ao JavaScript do WebView para que uma outra página seja carregada, mais precisamente a página de "Welcome".
Esse trecho de código é necessário, pois o envio dos dados foi feito pelo Java API e não pelo WebView, logo um carregamento automático de página direto do back-end não surgiria efeito algum.
Veja o trecho de código de verificação de device no back-end Web:
...
if( isset( $_POST['in-is-android'] ) ){
echo 'http://192.168.25.221:8888/webview-user-signup/view/congratulations.php?in-email=' . $user->email;
}
else{
header( 'Location: ../../view/congratulations.php?in-email=' . $user->email );
}
Quando o envio sai do Retrofit nós adicionamos um campo a mais, o isAndroid (no envio: in-is-android) com valor igual a 1.
Dessa forma, no back-end, nós identificamos o tipo de device em uso e então aplicamos o retorno correto.
No caso do Android é a url que o JavaScript deverá ativar o carregamento.
Agora vamos a atualização do jQuery da página index.php, carregada no WebView.
Adicione o código destacado:
...
<script type="text/javascript">
...
/* ENVIANDO DADOS PELO ANDROID */
$('#in-submit').click(function(e) {
var $handle = $(this);
if( $handle.prop('title').indexOf('Enviando...') > -1 ){
e.preventDefault();
return;
}
$handle.prop('title', 'Enviando...').html('Enviando...');
if( isAndroid ){
e.preventDefault();
Android.sendForm(
$('#in-method').val(),
$('#in-email').val(),
$('#in-password').val()
);
}
});
...
function loadSignUpDonePage( signUpDonePage ){
window.location = signUpDonePage;
}
</script>
...
Para o botão de submit do formulário, nós removemos o comportamento quando já está enviando dados, title tag igual a "Enviando...", ou quando é um device Android.
O comportamento padrão é o envio do formulário.
Removemos esse comportamento utilizando o código e.preventDefault().
O método loadSignUpDonePage() é o que permiti o envio da url de resposta do Android Java API para o JavaScript da página Web. Assim carregando a página de "Welcome" ou qualquer outra página de seu projeto.
Com isso podemos ir aos testes.
Enviando dados para back-end Web
Abra a aplicação em seu device ou emulador e então preencha o formulário, escolha a imagem e então clique em "Cadastrar":
Logo depois terá um resultado similar ao abaixo:
Observação: os testes da aplicação de exemplo foram realizados com emuladores rodando as APIs: 16 (Jelly Bean), 19 (KitKat), 21 (Lollipop) e 23 (Marshmallow).
Pontos negativos
- Código de configuração não sendo simples, incluindo o entendimento da library Retrofit e de alguma library de Image Picker;
- Recarregamento de página necessário para apresentação de preview de imagem quando em devices com API abaixo da 19.
Vídeo com implementação passo a passo do projeto
Abaixo o vídeo com a implementação passo a passo do projeto de exemplo do artigo:
Para acessar os projetos por completo entre nos respectivos GitHubs:
- Projeto Web: https://github.com/viniciusthiengo/webview-user-signup-web
- Projeto Android: https://github.com/viniciusthiengo/webview-user-signup-android
Conclusão
Como já discutido no início do artigo, o problema do Input File no WebView é de longa data.
Porém a solução apresentada aqui tende a funcionar para mais de 98% dos usuários Android, onde este número aumenta cada dia mais com o desuso das versões antigas deste sistema operacional.
Obviamente que a solução aqui não é a única que atinge esse número de usuários.
Destrinchando ainda mais o código você consegue criar uma que permite abranger 100%.
A parte mais importante deste conteúdo, que deve ser "internalizada", é o modelo de funcionamento do formulário no WebView, principalmente quando trabalhando com Input File.
Neste caso o uso dos códigos da Java API são inevitáveis.
Caso você esteja precisando de um WebView com funcionamento do Input File, utilize o projeto apresentado aqui.
Mas não deixe de estudar a Java / Kotlin API, pois a qualidade de aplicativos Android desenvolvidos em linguagens oficiais (Kotlin, Java, C++ e C) tende a ser maior.
Curtiu o conteúdo? Não esqueça de compartilha-lo.
E, por fim, se inscreva na 📩 lista de e-mails, respondo às suas dúvidas também por lá e...
... e também envio as versões em PDF de cada novo conteúdo somente aos inscritos na lista de e-mails.
Abraço.
Fontes
Retrofit — Getting Started and Create an Android Client
Retrofit 2 — How to Upload Files to Server
Comentários Facebook