Input File no WebView Android

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Input File no WebView Android

Input File no WebView Android

Vinícius Thiengo
(15408) (5)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição1ª
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

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

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:

Diagrama da solução Input File em projeto Web convencional, solução para dados em texto

Para esse mesmo projeto Web, quando temos a opção de fornecimento de arquivo, o fluxo de processamento é como segue na figura abaixo:

Diagrama da solução Input File em projeto Web convencional, solução para dados em binário

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:

Diagrama da solução Input File WebView proposta em artigo, para dados em texto

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:

Diagrama da solução Input File WebView proposta em artigo, para dados em binário

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:

Página de cadastro do app Web de exemplo

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:

Página de Sucesso no Cadastro do app Web de exemplo

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:

Estrutura física do projeto Web no PHPStorm IDE

É 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:

Tela de cadastro do app Android de exemplo

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_STORAGEWRITE_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:

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:

  1. Iniciar Thread secundária para execução assíncrona;
  2. Transformar imagem do path informado em um Bitmap com o método generateBitmap();
  3. Comprimir Bitmap;
  4. Transformar Bitmap comprimido em array de bytes;
  5. Transformar o array de bytes em um String Base64;
  6. 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: 

Tela da Image Picker API com as imagens do aparelho Android

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":

Realizando um cadastro no aplicativo Android de exemplo

Logo depois terá um resultado similar ao abaixo:

Cadastro realizado com sucesso no aplicativo Android de exemplo

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:

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 

Building Web Apps in WebView

Retrofit — Getting Started and Create an Android Client

Retrofit 2 — How to Upload Files to Server

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Padrão de Projeto: Cláusula de GuardaPadrão de Projeto: Cláusula de GuardaAndroid
OneSignal Para Notificações em Massa no AndroidOneSignal Para Notificações em Massa no AndroidAndroid
Proguard AndroidProguard AndroidAndroid
Lint Tool Para Alta Performance em APPs AndroidLint Tool Para Alta Performance em APPs AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (5)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Rodrigo Marden Silva (1) (0)
16/06/2019
Olá Thiengo,

Estou passando por um problema relacionado a upload de arquivos, porém utilizando alguns outros componentes, não consegui achar um outro post que poderia resolver o meu problema.

Estou tentando retornar um arquivo que esteja no celular para fazer download, estou utilizando a seguinte forma:

private void escolherArquivo() { // Abre a lista de arquivos internos para escolher

        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.setType("*/*");
        startActivityForResult(intent, 1);
    }

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);

        //Testar processo de retorno dos dados
        if(requestCode==1 && resultCode == RESULT_OK && data != null) {

            //Verificar tamanho do arquivo
            File f = new File(data.getDataString());
            Log.v("Arquivo", String.valueOf(f.length()));
            Log.v("Arquivo", f.getName());
            Log.v("Arquivo", data.getData().getPath());

            //Recupera local do recurso
            listaLocalArquivoSelecionado.add(data.getData());
            listaNomeArquivos.add(data.getData().getLastPathSegment());
            adapter.notifyDataSetChanged();

        }
    }

Só que o data.getData() ou o data.getData().getPath() retorna um URI onde o nome do arquivo vem como uma ID, mesmo que o nome do arquivo seja "arquivo.pdf" ele só me retorna um ID 6 por exemplo. E também não consigo obter as informações como o tamanho do arquivo, ou qual o tipo do arquivo, se é pdf, png etc.

Poderia me dar uma ajuda?

Obrigado
Responder
Vinícius Thiengo (0) (0)
16/08/2019
Rodrigo, tudo bem?

O que eu faria em seu caso, já que percebi que você está construindo, na "unha", o algoritmo de escolha de arquivo. Eu utilizaria uma API de File Chooser, especifica para acesso de arquivos internos ao aparelho do usuário.

Sendo assim eu teria certeza que o nome e meta dados do arquivo escolhido estariam sim presentes no "data" retornado em onActivityResult().

Rodrigo, o que você esta fazendo quando escolhe desenvolver na "unha" o algoritmo de seleção de arquivo é: construir um trecho de código que tem certa complexidade e não é o núcleo de seu domínio de problema.

Passe essa responsabilidade para alguma API especifica de File Chooser e já bem aceita na comunidade de desenvolvedores Android.

No link a seguir tem uma das mais utilizadas atualmente:

-> Android File-Picker: https://github.com/DroidNinja/Android-FilePicker

E abaixo o link com inúmeras outras APIs de File Chooser:

-> File Chooser APIs no Android-Arsenal: https://android-arsenal.com/tag/35?sort=rating

Abraço.
Responder
Levi Saturnino (1) (0)
28/08/2018
Legal esse post, não tinha visto antes. Mas sempre aparece alguns problemas com relação a upload principalmente nas versões do Android. Ultimamente estou usando a tag   <input type="file" name="pic" accept="image/*"> para aceitar imagens pelo webview por ser menos complicado o seu upload e aceito pela demais versões.  Valeu Thiengo pelo Artigo.
Responder
Vinícius Thiengo (0) (0)
28/08/2018
Levi, tudo bem?

Exatamente, esse é o problema, a tag form: <input type=?file" />

Ela não funciona de maneira consistente no WebView. Confesso que não sei lhe informar se o problema é somente referente a versão do Android (até a versão Lollipop sei que tem este problema).

Mas desde a construção do conteúdo acima a melhor maneira que encontrei para contornar esse problema foi utilizando alguma API de comunicação remota para envio da imagem ou qualquer recurso que deve ser carregado via <input type=?file" />. Exatamente como feito acima.

Mas me diga, em todos os seus testes o <input type=?file" /> funcionou sem problemas?

Abraço.
Responder
Levi Saturnino (1) (0)
28/08/2018
Acabei de finalizar os meus testes, fiz nas versões do Android: 5.0, 6.0, 7.0 e 8.0. Em todos funcionou, eu usei o WebChromeClient para ter permissão de Camera e de Read External para que ele pega-se as imagens gravadas no app para ser enviada e no lado do servidor usar o base64. a tag usada html <input type="file" accept=".gif, .jpeg" />.
Responder