android Webview : cargar navegador en apps

articulo creado por http://danielme.com/

Android WebView: incrustar un navegador en nuestras apps
VERSIONES
1.     19/05/2012 (Primera publicación)
2.     28/10/2012:
·         Añadida nota a la introducción sobre la versión de WebKit
·         Tratamiento de favicon (sólo en el código de GitHub)
3.     06/12/2012:
·         Integración del favicon con el código de ejemplo del tutorial
·         Comprobación de conectividad (anteriormente incluído en los comentarios)
·         Solucionado el problema que no mostraba el teclado virtual en los input
·         Gestión de rotaciones
·         Launcher icon personalizado
4.     16/02/2013:
·         Descargas asíncronas
·         Activación de plugins
·         Actualización del entorno de pruebas
·         Enlaces a los tips Android relacionados
5.     17/11/2013:
·         Código revisado y probado con Android 4.3 y 4.4
·         Mejorado historial (sólo en demo de GitHub)
·         Añadida descripción de novedades de WebView en Android 4.4
·         Sólo se ofrece como código de ejemplo la demo completa en GitHub para facilitar el mantenimeinto y actualización del artículo.

NOTA
Este artículo explica la mayor parte de una demo más completa publicada en GitHub.


Android SDK proporciona un widget denominado WebView para renderizar páginas web, ya sea obteniéndolas través de una URL o bien recibiendo el html directamente desde una Activity.

WebView está basado en el proyecto Open Source WebKit, que incluye un motor de renderizado de html y un intérprete de javascript. WebKit es utilizado en numerosas aplicaciones y es la base de navegadores como SafariChromiun – Google Chrome oMidori.

En este artículo se va a exponer de forma práctica cómo incrustar y utilizar en una app un navegador web; WebView, además de renderizar html, es muy potente y altamente configurable y proporciona numerosos métodos para controlar la navegación, gestionar cookies, procesar los errores, etc. El uso que veremos será básico y orientado a la navegación web, ya que con WebView se podría incluso realizar las interfaces gráficas de nuestra app en html y utilizar javascript para ejecutar código Android al estilo de PhoneGap. Para este uso he escrito otroartículo.
Nota: Para este tutorial se usará la API 7 (Android 2.1), pero la versión de WebKit dependerá de la versión de Android del dispositivo en el que se ejecute la demo. Para conocerla, bastará con ver el user-agent con el que se identifica el widget WebView o, más fácil todavía, visitar http://jimbergman.net/webkit-version-in-android-version/ .
Entorno de pruebas:
·         Lubuntu 11.10 64 bits
·         JDK 1.6.0_35
·         Eclipse 3.7.2 Indigo
·         Android Development Toolkit 21
·         Nexus 7 con Android 4.2.1
Requisitos: Conocimientos muy básicos de Android y Eclipse ADT.
Ejemplo básico
No nos complicamos la vida y creamos un proyecto de tipo Android con el asistente. Voy a usar la API 7 (2.1) para conseguir la mayor compatibilidad posible y que es más que suficiente para el propósito de este mini-tutorial.

WebView se utiliza como cualquier otro widget, lo incluímos en el main.xml creado por el asistente (y eliminamos también el Hello World):
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >
     
    <WebView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/webkit"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>
En nuestra Activity, establecemos la página que queremos mostrar:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.danielme.blog.android.webviewdemo;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;

public class WebViewdemoActivity extends Activity
{
    private WebView browser;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        browser = (WebView)findViewById(R.id.webkit);

                //habilitamos javascript y el zoom
        browser.getSettings().setJavaScriptEnabled(true);
        browser.getSettings().setBuiltInZoomControls(true);

        //habilitamos los plugins (flash)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO)
        {
            webview.getSettings().setPluginState(PluginState.ON);
        }
        else
        {
            //IMPORTANTE!! este método ha sido eliminado en Android 4.3
                        //por lo que si lo necesitamos para mantener la compatibilidad
                        //hacia atrás hay que compilar el proyecto con Android 4.2 como máximo
            webview.getSettings().setPluginsEnabled(true);
        }
         

        browser.loadUrl("http://danielme.com");
    }
}
También necesitaremos en el AndroidManifest.xml solicitar permiso para la conexión a Internet.
7
<uses-permission android:name="android.permission.INTERNET"/>
Veamos que tal se renderiza WordPress en un móvil ejecutando el proyecto en el emulador de ADT para la API 7.

Si se pulsa un enlace, Android lo abrirá con la aplicación por defecto para tal fin, esto es, el propio navegador. Si queremos permanecer en nuestra app hay que especializar la clase WebViewClient y sobreescribir el métodoshouldOverrideUrlLoading para que devuelva false. Puesto que en este método se recibe como parámetro la url solicitada se podría utilizar también para filtrar las direcciones a las que se permite el acceso.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.danielme.blog.android.webviewdemo;

import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;

public class WebViewdemoActivity extends Activity
{
    private WebView browser;

    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        browser = (WebView)findViewById(R.id.webkit);
         
                //habilitamos javascript y el zoom
        browser.getSettings().setJavaScriptEnabled(true);
        browser.getSettings().setBuiltInZoomControls(true);
                 
                //habilitamos los plugins (flash)
        browser.getSettings().setPluginsEnabled(true);
         
                browser.loadUrl("http://danielme.com");
         
        browser.setWebViewClient(new WebViewClient()
        {
                        // evita que los enlaces se abran fuera nuestra app en el navegador de android
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url)
            {
                return false;
            }  
             
        });
    }  

}
Barra de progreso
En el ejemplo anterior, el usuario no puede saber si una página se está cargando hasta que WebView la muestre, creando la sensación de que el navegador no responde. Afortunadamente es fácil proporcionar una barra de progreso con el widget ProgressBar y haciendo uso de la clase WebChromeClient para saber la evolución del proceso de carga de la página. Esta barra la ocultaremos una vez haya finalizado la carga de la página para ahorrar espacio en pantalla.
Primero añadimos la barra a nuestra interfaz, por ejemplo justo arriba del navegador.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ProgressBar
        android:id="@+id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="3dp"
        android:visibility="gone"/>

    <WebView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/webkit"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>
Añadimos el código que gestionará la barra:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.danielme.blog.android.webviewdemo;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;

public class WebViewdemoActivity extends Activity
{
        private WebView browser;

    private ProgressBar progressBar;
     
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        browser = (WebView)findViewById(R.id.webkit);
         
                 //habilitamos javascript y el zoom
        browser.getSettings().setJavaScriptEnabled(true);
        browser.getSettings().setBuiltInZoomControls(true);

                //habilitamos los plugins (flash)
        browser.getSettings().setPluginsEnabled(true);     

        browser.loadUrl("http://danielme.com");
         
        browser.setWebViewClient(new WebViewClient()
        {
                        // evita que los enlaces se abran fuera nuestra app en el navegador de android
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url)
            {
                return false;
            }  
             
        });

        progressBar = (ProgressBar) findViewById(R.id.progressbar);
         
        browser.setWebChromeClient(new WebChromeClient()
        {
            @Override
            public void onProgressChanged(WebView view, int progress)
            {              
                progressBar.setProgress(0);
                progressBar.setVisibility(View.VISIBLE);
                WebViewdemoActivity.this.setProgress(progress * 1000);

                progressBar.incrementProgressBy(progress);

                if(progress == 100)
                {
                    progressBar.setVisibility(View.GONE);
                }
            }
        });
    }  

}
El resultado:
Lamentablemente la barra de progreso no permite hacer demasiadas florituras, así que hay que ingeniárselas para personalizar y mejorar su aspecto. Personalmente yo no me complicaría tanto y superpondría sobre la barra un TextView, ubicando ambos widgets en un FrameLayout. Es la soución por la que he optado en el ejemplo que he publicado en GitHub.
También existe la posibilidad de utilizar la barra de progreso propia de Android ubicada debajo de la barra de título de la aplicación tal y como se hace en el navegador de Android. No obstante, esta solución es menos flexible y sólo es válida si nuestra app utiliza la barra de título por defecto, algo que hoy en día no es demasiado habitual.
“Title” de la página
Con WebChromeClient es posible recuperar el title de la página que se esté cargando en cuanto este sea leído gracias al método onReceivedTitle. Esto permite, por ejemplo, mostrarlo en la barra de la aplicación al estilo de los navegadores “tradicionales”.
//la implementación de este método permite para mostrar el title de la página en la barra de título de la aplicación
            @Override
            public void onReceivedTitle(WebView view, String title)
            {
                WebViewdemoActivity.this.setTitle("WebView demo: " + WebViewdemoActivity.this.browser.getTitle());
            }
Barra de navegación
Sigamos completando nuestro navegador con una barra de navegación que constará de los siguientes elementos:
·         Campo de texto para introducir una dirección url
·         Botón “Ir” para cargar la url del cuadro de texto
·         Botón de navegación hacia atrás. Sólo se mostrará cuando haya una página anterior a la que volver.
·         Botón de navegación hacia adelante. Sólo se mostrará cuando haya al menos una página por delante en el historial.
·         Botón para cancelar la carga de la página. Habilitado mientras se esté cargando una página.
Usando un RelativeLayout y el editor visual de ADT añadimos los elementos anteriores a nuestro Layout. Excepto el botón de “Ir” que se mostrará siempre, todos los demás estarán por defecto deshabilitados y se controlarán programáticamente
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <ProgressBar
        android:id="@+id/progressbar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_margin="3dp"
        android:visibility="gone"/>

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <EditText
            android:id="@+id/url"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_alignParentTop="true"
            android:layout_toLeftOf="@+id/buttonIr"
            android:ems="10"
            android:inputType="textUri"
            android:selectAllOnFocus="true">
        </EditText>

        <Button
            android:id="@+id/buttonIr"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignBaseline="@+id/url"
            android:layout_alignBottom="@+id/url"
            android:layout_alignParentRight="true"
            android:onClick="ir"
            android:text="@string/ir" />

        <Button
            android:id="@+id/buttonAnt"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_below="@+id/url"
            android:enabled="false"
            android:onClick="anterior"
            android:text="@string/anterior" />

        <Button
            android:id="@+id/buttonSig"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/url"
            android:layout_toRightOf="@+id/buttonAnt"
            android:enabled="false"
            android:onClick="siguiente"
            android:text="@string/siguiente" />

        <Button
            android:id="@+id/buttonDetener"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@+id/url"
            android:layout_toRightOf="@+id/buttonSig"
            android:enabled="false"
            android:onClick="detener"
            android:text="@string/detener" />
    </RelativeLayout>

    <WebView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/webkit"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" />

</LinearLayout>

Los métodos asociados a los botones no tiene ninguna complicación, simplemente invocan métodos de WebView. En el caso del botón “Ir” he incluído el código para ocultar el teclado.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
package com.danielme.blog.android.webviewdemo;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;

public class WebViewdemoActivity extends Activity
{
        private WebView browser;

    private ProgressBar progressBar;
     
    private EditText url;
     
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        browser = (WebView)findViewById(R.id.webkit);
         
                 //habilitamos javascript y el zoom
        browser.getSettings().setJavaScriptEnabled(true);
        browser.getSettings().setBuiltInZoomControls(true);

                //habilitamos los plugins (flash)
        browser.getSettings().setPluginsEnabled(true);

        browser.setWebViewClient(new WebViewClient()
        {
                        // evita que los enlaces se abran fuera nuestra app en el navegador de android
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url)
            {
                return false;
            }  
             
        });

        url = (EditText) findViewById(R.id.url);
        url.setText("http://");
         
        progressBar = (ProgressBar) findViewById(R.id.progressbar);
         
        browser.setWebChromeClient(new WebChromeClient()
        {
            @Override
            public void onProgressChanged(WebView view, int progress)
            {              
                progressBar.setProgress(0);
                progressBar.setVisibility(View.VISIBLE);
                WebViewdemoActivity.this.setProgress(progress * 1000);

                progressBar.incrementProgressBy(progress);

                if(progress == 100)
                {
                    progressBar.setVisibility(View.GONE);
                }
            }
             

            //la implementación de este método permite para mostrar el title de la página en la barra de título de la aplicación
            @Override
            public void onReceivedTitle(WebView view, String title)
            {
                WebViewdemoActivity.this.setTitle("WebView demo: " + WebViewdemoActivity.this.browser.getTitle());
            }


        });
    }  
     
    // //////////BOTONES DE NAVEGACIÓN /////////

    public void ir(View view)
    {
        // oculta el teclado al pulsar el botón
        InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(url.getWindowToken(), 0);

        // he observado que si se pulsa "Ir" sin modificarse la url no se
        // ejecuta el método onPageStarted, así que nos aseguramos
        // que siempre que se cargue una url, aunque sea la que se está
        // mostrando, se active el botón "detener"
        ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);

        browser.loadUrl(url.getText().toString());

    }

    public void anterior(View view)
    {
        browser.goBack();
    }

    public void siguiente(View view)
    {
        browser.goForward();
    }

    public void detener(View view)
    {
        browser.stopLoading();
    }

}
Lo más interesante de la barra es cómo controlar la carga de la página para activar/desactivar los botones, y para ello implementaremos dos nuevos métodos en nuestro WebViewClient
·         onPageStarted: Se ejecuta al iniciarse la carga de una página, una vez por cada frame(el control de este último caso no se contempla en este tutorial) .Es aquí donde se habilitará siempre el botón “Detener”.Asimismo, en el cuadro de texto mostraremos la url que se está cargando ya que es posible que se trate de un link que el usuario haya pulsado.
44
45
46
47
48
49
50
51
52
// gestión de los botones de navegación (inicio de carga de una página)
            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon)
            {
                // mostramos la url de los enlaces seleccionados
                WebViewdemoActivity.this.url.setText(url);

                ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
            }
·         onPageFinished: Se invocará cuando haya terminado la carga de la página, momento en el que hay que deshabilitar el botón Detener al carecer de sentido. También se gestionarán aquí los botones Anterior ySiguiente.
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// gestión de los botones de navegación (final de carga de una
        // página)
        @Override
        public void onPageFinished(WebView view, String url)
        {
            ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(false);
            Button botonAnterior = (Button) WebViewdemoActivity.this.findViewById(R.id.buttonAnt);

            if (view.canGoBack())
            {
                botonAnterior.setEnabled(true);
            }
            else
            {
                botonAnterior.setEnabled(false);
            }

            Button botonSiguiente = (Button) WebViewdemoActivity.this.findViewById(R.id.buttonSig);

            if (view.canGoForward())
            {
                botonSiguiente.setEnabled(true);
            }
            else
            {
                botonSiguiente.setEnabled(false);
            }
        }
¿Y el botón Back?
En nuestra demo al pulsar el botón “Back” del dispositivo finalizaremos la Activity, mientras que en el navegador por defecto de Android se vuelve a la página anterior y la Activity no se finaliza hasta que no pulsemos Back desde la primera página mostrada. Este comportamiento puede ser implementado fácilmente, aunque en el caso de que incrustemos el navegador en una app probablemente resulte más conveniente mostrar una barra de navegación y no modificar el comportamiento por defecto del botón Back.
//simula el comportamiento del botón Back en el navegador de Android
    @Override
    public void onBackPressed()
    {
        if (browser.canGoBack())
        {
            browser.goBack();
        }
        else
        {
            super.onBackPressed();
        }
    }
Gestión de errores
WebView mostrará en pantalla los errores que se produzcan, pero podemos recuperalos para actuar en consecuencia implementando el método onReceivedError de WebViewClient.
//gestión de errores
            @Override
            public void onReceivedError(WebView view, int errorCode, String description, String failingUrl)
            {
                AlertDialog.Builder builder = new AlertDialog.Builder(WebViewdemoActivity.this);
                builder.setMessage(description).setPositiveButton("Aceptar", null).setTitle("onReceivedError");            
                builder.show();
             }

Descargas
Otra interesante funcionalidad proporcionada por WebView es el listener DownloadListener que nos permitirá saber cuándo WebView considera una url solicitada como descarga. A continuación se expone una posible implementación de ejemplo que, con la aprobación del usuario, descarga el fichero en la tarjeta SD por lo que hay darle permisos de escritura a la aplicación. Por simplicidad los textos se han incluído en el código, en una aplicación real deberían estar en el strings.xml para su abstracción y una posible internacionalización. Para mayor información sobre la obtención de recursos web, consultar el tip #8.Para el uso del espacio de almacenamiento externo, ver el tip #5.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
@Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        browser = (WebView) findViewById(R.id.webkit);
        faviconImageView = (ImageView) findViewById(R.id.favicon);
        WebIconDatabase.getInstance().open(getDir("icons", MODE_PRIVATE).getPath());

        // habilitamos javascript y el zoom
        browser.getSettings().setJavaScriptEnabled(true);
        browser.getSettings().setBuiltInZoomControls(true);
         
        //habilitamos los plugins (flash)
        browser.getSettings().setPluginsEnabled(true);

        //implementa la acción a realizar cuando WebView considere que el enlace solicitado es una descarga
        browser.setDownloadListener(new DownloadListener()
        {
            public void onDownloadStart(final String url, String userAgent, String contentDisposition, String mimetype, long contentLength)
            {              
                AlertDialog.Builder builder = new AlertDialog.Builder(WebViewdemoActivity.this);
                builder.setTitle("Descarga");
                builder.setMessage("¿Desea guardar el fichero en su tarjeta SD?");
                builder.setCancelable(false).setPositiveButton("Aceptar", new DialogInterface.OnClickListener()
                {
                    public void onClick(DialogInterface dialog, int id)
                    {
                        descargar(url);
                    }
                     
                }).setNegativeButton("Cancelar", new DialogInterface.OnClickListener()
                {
                    public void onClick(DialogInterface dialog, int id)
                    {
                        dialog.cancel();
                    }
                });
                builder.create().show();               
                 
                 
            }
             
            private void descargar(final String url)
            {
               String resultado ="";
                 
               URL urlObject = null;
               InputStream inputStream = null;
               HttpURLConnection urlConnection = null;
               try
                {
                    urlObject = new URL(url);
                    urlConnection = (HttpURLConnection) urlObject.openConnection();
                         
                    inputStream = urlConnection.getInputStream();
     
                    //se crea el fichero en el que se almacenará
                    String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/webviewdemo";
                    File directorio = new File(fileName);
                    File file = new File(directorio, url.substring(url.lastIndexOf("/")));
                    //asegura que el directorio exista
                    directorio.mkdirs();                   
                     
                    FileOutputStream fileOutputStream = new FileOutputStream(file);
                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
     
                    byte[] buffer = new byte[1024];
                     
                    int len = 0;
                    while (inputStream.available() > 0 && (len = inputStream.read(buffer)) != -1)
                    {
                        byteArrayOutputStream.write(buffer, 0, len);
                    }
                    fileOutputStream.write(byteArrayOutputStream.toByteArray());
                    fileOutputStream.flush();
                    resultado = "guardado en : " + file.getAbsolutePath();
                }
                catch (Exception ex)
                {
                    resultado = ex.getClass().getSimpleName() + " " + ex.getMessage();
                }
                finally
                {
                    if (inputStream != null)
                    {
                        try
                        {
                            inputStream.close();
                        }
                        catch (IOException e)
                        {
                             
                        }
                    }
                    if (urlConnection != null)
                    {
                        urlConnection.disconnect();
                    }
                }
                 
                AlertDialog.Builder builder = new AlertDialog.Builder(WebViewdemoActivity.this);
                builder.setMessage(resultado).setPositiveButton("Aceptar", null).setTitle("Descarga");
                builder.show();
            }

        });

...
diálogo aceptar descarga
diálogo descarga realizada
Sin embargo, esta aproximación al tratamiento de las descargas no es correcta ya que a partir de HoneyComb no se pueden realizar llamadas a url dentro del hilo principal de la aplicación; de lo contrario obtenemos esta bonita excepción (aunque la aplicación no se cierra):
NetworkOnMainThread
El motivo está bien fundamentado: estamos realizando operaciones que pueden tomar bastante tiempo y no podemos dejar bloqueada la interfaz de usuario. Por ello es necesario realizar el proceso de descarga en segundo plano en otro hilo, usando por ejemplo Thread/handler o AsyncTask, estrategias comentadas en la sección Tips Android (#2 y #3 respectivamente). Usando AsyncTask para la descarga, el código quedará tal que así (también se validará la disponibilidad de almacenamiento externo):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class DownloadAsyncTask extends AsyncTask<String, Void, String>
       {
           @Override
           protected String doInBackground(String... arg0)
           {
               String result = null;
               String url = arg0[0];
                
               if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
               {
                   URL urlObject = null;
                   InputStream inputStream = null;
                   HttpURLConnection urlConnection = null;
                   try
                   {
                       urlObject = new URL(url);
                       urlConnection = (HttpURLConnection) urlObject.openConnection();
                       inputStream = urlConnection.getInputStream();
    
                       String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + "/webviewdemo";
                       File directory = new File(fileName);
                       File file = new File(directory, url.substring(url.lastIndexOf("/")));
                       directory.mkdirs();
    
                
                       FileOutputStream fileOutputStream = new FileOutputStream(file);
                       ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
                       byte[] buffer = new byte[1024];
                       int len = 0;
    
                       while (inputStream.available() > 0   && (len = inputStream.read(buffer)) != -1)
                       {
                           byteArrayOutputStream.write(buffer, 0, len);
                       }
    
                       fileOutputStream.write(byteArrayOutputStream.toByteArray());
                       fileOutputStream.flush();
                       result = "guardado en : " + file.getAbsolutePath();
                   }
                   catch (Exception ex)
                   {
                       result = ex.getClass().getSimpleName() + " " + ex.getMessage();
                   }
                   finally
                   {
                       if (inputStream != null)
                       {
                           try
                           {
                               inputStream.close();
                           }
                           catch (IOException ex)
                           {
                               result = ex.getClass().getSimpleName() + " " + ex.getMessage();
                           }
                       }
                       if (urlConnection != null)
                       {
                           urlConnection.disconnect();
                       }
                   }
               }
               else
               {
                   result = "Almacenamiento no disponible";
               }

               return result;
           }

           @Override
           protected void onPostExecute(String result)
           {
                AlertDialog.Builder builder = new AlertDialog.Builder(WebViewdemoActivity.this);
                builder.setMessage(result).setPositiveButton("Aceptar", null).setTitle("Descarga");
                builder.show();
           }

       }
Ahora, en lugar de llamar al método descargar(url) lanzamos la AsyncTask:
71
72
73
74
75
76
77
builder.setCancelable(false).setPositiveButton("Aceptar", new DialogInterface.OnClickListener()
{
    public void onClick(DialogInterface dialog, int id)
    {
        (new DownloadAsyncTask()).execute(url);
    }
...
En Gingerbread se introdujo el servicio DownloadManager diseñado para realizar grandes descargas en segundo plano. Hay un pequeño ejemplo de uso en este tip.
Favicon
En principio utilizar el favicon de las páginas visitadas por WebView parece sencillo ya que el métodoonPageStarted recibe un Bitmap llamado favicon. Sin embargo, este parámetro siempre es nulo ya que según la documentación la imagen sólo se recibe si ya existe previamente en una base de datos empleada por WebView (*): WebIconDatabase.
(*) Según la documentación oficial a partir de Android 4.3 ya no es necesaria utilizar esta clase por lo que ha sido “deprecada”. Sin embargo, he probado a no usarla y no se reciben los íconos.
WebIconDatabase
Siguiendo las instrucciones del segundo post de este hilo podemos incluir el procesamiento del favicon de forma muy simple y mostrarlo junto a la url:
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<RelativeLayout
      xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"> 
       
    <ImageView
          android:id="@+id/favicon"
          android:layout_width="32dp"
          android:layout_height="32dp"
          android:layout_alignBottom="@+id/url"
          android:layout_alignParentLeft="true"
          android:layout_alignParentTop="true"
          android:src="@drawable/favicon_default" />  

      <EditText
          android:id="@+id/url"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_alignParentTop="true"
          android:layout_toLeftOf="@+id/buttonIr"
          android:layout_toRightOf="@+id/favicon"
          android:ems="10"
          android:inputType="textUri" >
   
El icono por defecto que he usado pertenece a esta versión de la colección oficial de gráficos de Google, se encuentra en el el tema “Holo Dark” y se llama originalmente “7-location-web-site.png”. Está disponible en varias densidades.
Ahora para mostrar el favicon adecuado:
1.     Añadir el widget a la Activity
41
private ImageView faviconImageView;
2.     Iniciar WebIconDatabase en el onCreate de la activity:
50
51
faviconImageView = (ImageView) findViewById(R.id.favicon);
WebIconDatabase.getInstance().open(getDir("icons", MODE_PRIVATE).getPath());
3.     Recibir el favicon en cuanto sea obtenido por WebView gracias al método onReceivedIcon.Si no se inicia WebIconDatabase este método nunca se invoca.
241
242
243
244
245
@Override
public void onReceivedIcon(WebView view, Bitmap icon)
{              
   faviconImageView.setImageBitmap(icon);              
}
El resultado final con el favicon junto a la dirección:
url con favicon - android
Conectividad
Siempre que en una app necesitemos conectividad, es una buena práctica comprobar que la tenemos disponible antes de solicitar cualquier operación y esperar a recibir un timeout o error similar. Para ello hay que hacer uso deConnectivityManager y NetworkInfo , yo siempre utilizo el siguiente snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private boolean checkConnectivity()
        {
            boolean enabled = true;
     
            ConnectivityManager connectivityManager = (ConnectivityManager) this.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo info = connectivityManager.getActiveNetworkInfo();
             
            if ((info == null || !info.isConnected() || !info.isAvailable()))
            {
                enabled = false;
                Builder builder = new Builder(this);
                builder.setIcon(android.R.drawable.ic_dialog_alert);
                builder.setMessage(getString(R.string.noconnection));
                builder.setCancelable(false);
                builder.setNeutralButton(R.string.ok, null);
                builder.setTitle(getString(R.string.error));
                builder.create().show();       
            }
            return enabled;        
        }
Nota: El código anterior es muy básico, y si fuera necesario se puede obtener más información sobre la conexión activa a través del método getType(). Si necesitamos realizar un uso intensivo de la red, puede ser por ejemplo conveniente avisar al usuario si utiliza una conexión de datos móvil en lugar de wifi.
Obviamente necesitamos los nuevos textos en strings.xml:
8
9
10
<string name="noconnection">La conexión a internet no se encuentra disponible en estos momentos</string>
<string name="ok">Aceptar</string>
<string name="error">Error</string> 
También necesitamos añadir al manifest el permiso para acceder al estado de la red, con el de acceso a Internet que ya teníamos no es suficiente:
9
10
11
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> 
Para cubrir tanto las peticiones realizadas el escribir manualmente la url como los links pulsados, añadimos la validación a los métodos onPageStarted y los de los botones de navegación:
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
@Override
            public void onPageStarted(WebView view, String url, Bitmap favicon)
            {
                if (checkConnectivity())
                {
                    // mostramos la url de los enlaces seleccionados
                    WebViewdemoActivity.this.url.setText(url);             

                    ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);
                }
                else
                {
                    ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(false);
                }
            }
    
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
public void ir(View view)
{
    // oculta el teclado al pulsar el botón
    InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
    inputMethodManager.hideSoftInputFromWindow(url.getWindowToken(), 0);
     
    if(checkConnectivity())
    {
        // he observado que si se pulsa "Ir" sin modificarse la url no se
        // ejecuta el método onPageStarted, así que nos aseguramos
        // que siempre que se cargue una url, aunque sea la que se está
        // mostrando, se active el botón "detener"
        ((Button) WebViewdemoActivity.this.findViewById(R.id.buttonDetener)).setEnabled(true);     
        browser.loadUrl(url.getText().toString());
    }
     
}

public void anterior(View view)
{
    if(checkConnectivity())
    {
        browser.goBack();
    }
}

public void siguiente(View view)
{
    if(checkConnectivity())
    {
        browser.goForward();
    }
}
        

ConnectivityManager
Mostrar el teclado virtual en los input
En algunas pruebas he encontrado un error muy molesto:al hacer click en un input text de una web no se mostraba el teclado y, por lo tanto, no podía hacer uso de él. Parece ser que esto se debe al siguiente bug, la “solución”, simplemente copio y pego, es añadir al onCreate el siguiente código:
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
browser.setOnTouchListener(new View.OnTouchListener()
        {
            @Override
            public boolean onTouch(View v, MotionEvent event)
            {
                       switch (event.getAction())
                       {
                           case MotionEvent.ACTION_DOWN:
                           case MotionEvent.ACTION_UP:
                               if (!v.hasFocus())
                               {
                                   v.requestFocus();
                               } 
                               break;
                       }
                       return false;
                    }      
            });
Gestionar las rotaciones
Uno de mis últimos tutoriales estuvo dedicado a la gestión de rotaciones en Android. En el navegador que estamos implementando, si rotamos la pantalla la Activity se reinicia tal y como se analizó en ese artículo y volvemos a la pantalla en blanco de inicio, perdiendo lo que tuviéramos en pantalla. Para evitar esto, vamos a ir a la solución sencilla ya que en este caso no tenemos un layout para el modo landscape, y simplemente deshabilitamos la gestión de rotaciones por defecto de Android configurando nuestra única Activity de la siguiente forma:
<activity
           android:name=".WebViewdemoActivity"
           android:label="@string/app_name"
           android:configChanges="orientation|keyboardHidden">
Para más detalles, remito al lector al mencionado artículo.
Ejemplo más completo en GitHub
Aunque la demo cumple con lo que quería mostrar cuando me planteé la elaboración de este tutorial (que por otra parte es consecuencia de haber tenido que utilizar WebView en uno de mis proyectos), le he dedicado algo más de tiempo para implementar algunas mejoras:
·         Página de inicio en html
·         Información en la barra de progreso
·         Internacionalización
·         Historial
Esta nueva demo está publicada publicada en GitHub y en la sección de demos del blog. Para más información sobre cómo utilizar GitHub, consultar este artículo.
En el fondo la construcción del navegador no tiene mucho sentido como fin, pero sí como medio y divertimento para seguir aprendiendo a desarrollar en Android. Espero que mi trabajo os haya sido de ayuda ;)
Lo nuevo en Android 4.4 Kit Kat
Una de las grandes novedades de la última versión de Android en el momento de escribir estas líneas es el “nuevo” WebView ya que a partir de esta versión se basa directamente en el motor de Chromium, concretamente en su versión 30. Chromium es el navegador Open Source en el que se basa Google Chrome y según Google con este cambio podemos esperar una notable mejora del rendimiento en la ejecución de javascript gracias a su motor y de CSS gracias a la aceleración hardware. También se mejora el soporte de HTML 5. Este cambio debería repercutir en una importante mejora de las aplicaciones nativas basadas en tecnologías Web como las desarrolladas con PhoneGap. Por otra parte, la API del propio WebView que hemos visto en este artículo no ha sufrido cambios importantes.
Aquí se puede encontrar una pequeña guía de migración para las aplicaciones basadas en WebView.

1 comentario:

  1. esto ya no funciona! sirven algunas cosas como el progressbar los botones.. pero el sistema de descarga no funciona!

    ResponderEliminar

Todos los comentarios son bien recibidos...

CommentFB