INTRODUCCIÓN
ÍNDICE 1.1
INTRODUCCIÓN .............................................................. 3 1.1.1 Qué es Android .................................................................3 1.1.2 Proyecto libre (Open Source) ..........................................3 1.1.3 Su historia ..........................................................................3 1.1.4 Inconvenientes de Android..............................................4
1.2
QUÉ ES ECLIPSE ............................................................. 5 1.2.1 El Consorcio Eclipse ........................................................5 1.2.2 Instalación de Java Developmente Kit (JDK)................6 1.2.3 Instalación de Eclipse ......................................................7 1.2.4 Instalación de las librerías de Android...........................9 1.2.5 Añadir versiones y componentes de Android ............19 1.2.6 Definición del dispositivo virtual de Android ..............25
2
Introducción
1.1
INTRODUCCIÓN
1.1.1 Qué es Android Android es un sistema operativo, inicialmente diseñado para teléfonos móviles como los sistemas operativos iOS (Apple), Symbian (Nokia) y Blackberry OS. En la actualidad, este sistema operativo se instala no sólo en móviles, sino también en múltiples dispositivos, como tabletas, GPS, televisores, discos duros multimedia, mini ordenadores, etcétera. Incluso se ha instalado en microondas y lavadoras. Está basado en Linux, que es un núcleo de sistema operativo libre, gratuito y multiplataforma. Este sistema operativo permite programar aplicaciones empleando una variación de Java llamada Dalvik, y proporciona todas las interfaces necesarias para desarrollar fácilmente aplicaciones que acceden a las funciones del teléfono (como el GPS, las llamadas, la agenda, etcétera) utilizando el lenguaje de programación Java. Su sencillez principalmente, junto a la existencia de herramientas de programación gratuitas, es la causa de que existan cientos de miles de aplicaciones disponibles, que extienden la funcionalidad de los dispositivos y mejoran la experiencia del usuario.
1.1.2 Proyecto libre (Open Source) Una de las características más importantes de este sistema operativo reside en que es completamente libre. Es decir, ni para programar en este sistema ni para incluirlo en un teléfono hay que pagar nada. Por esta razón, es muy popular entre los fabricantes de teléfonos y desarrolladores, ya que los costes para lanzar un teléfono o una aplicación son muy bajos. Cualquier programador puede descargarse el código fuente, inspeccionarlo, compilarlo e incluso modificarlo.
1.1.3 Su historia Android era un sistema operativo para móviles prácticamente desconocido hasta que en 2005 lo compró Google. En noviembre de 2007 se creó la Open Handset Alliance, que agrupó a muchos fabricantes de teléfonos móviles, procesadores y Google. Este año se lanzó la primera versión de Android, junto con el SDK (del inglés, Software Development Kit, que significa Kit del desarrollo de software) para que los programadores empezaran a crear sus aplicaciones para este sistema operativo.
3
El despegue del sistema operativo fue lento porque se lanzó antes el sistema operativo que el primer terminal móvil, aunque rápidamente se ha colocado como el sistema operativo de móviles más vendido del mundo. En febrero de 2011 se anunció la versión 3.0 de Android, cuyo nombre en clave es Honeycomb, que está optimizada para tabletas en lugar de para teléfonos móviles.
Versiones disponibles: Las versiones de Android reciben nombre de postres en inglés. En cada versión el postre elegido empieza por una letra distinta siguiendo un orden alfabético:
C: D: E: F: G: H: I: J:
Cupcake (v1.5), magdalena glaseada. Donut (v1.6), rosquilla. Éclair (v2.0/v2.1), pastel francés conocido en España como pepito. Froyo (v2.2), (abreviatura de «frozen yogurt») yogur helado. Gingerbread (v2.3), pan de jengibre. Honeycomb (v3.0/v3.1), panal. IceCream Sandwich (4.0), sandwich de helado. Jelly Bean (¿¿??), gomitas de gelatina)
En el siguiente enlace puedes encontrar una descripción de la funcionalidad que incluye cada versión de Android.
1.1.4 Inconvenientes de Android Android ha sido criticado muchas veces por la fragmentación que sufren sus terminales (con versiones distintas de Android), ya que las actualizaciones no se despliegan automáticamente en estos terminales una vez que Google lanza una nueva versión. Cada fabricante debe crear su propia versión. Sin embargo, esa situación cambiará ya que los fabricantes se han comprometido a aplicar actualizaciones al menos durante los 18 meses siguientes desde que empiezan a vender un terminal en el mercado. Además, actualmente Google tiene la intención de unificar la funcionalidad entre las versiones del sistema operativo para tabletas y móviles en la versión 4.0. Disponer del código fuente del sistema operativo no significa que se pueda tener siempre la última versión de Android en un determinado móvil, ya que el código para soportar el hardware de cada fabricante normalmente no es público; así que faltaría un trozo básico del firmware (controladores) para poder hacerlo funcionar en dicho terminal. Hay que tener en cuenta que las nuevas versiones de Android suelen requerir más recursos, por lo que, en los modelos más antiguos, no se puede instalar la última versión por insuficiente memoria RAM, velocidad de procesador, etcétera. 4
Introducción
1.2
QUÉ ES ECLIPSE
Eclipse es un entorno de software multi-lenguaje de programación que incluye un entorno de desarrollo integrado (IDE). Inicialmente, se diseñó pensando principalmente en el lenguaje de programación Java y se puede utilizar para desarrollar aplicaciones en este lenguaje. En la web oficial de Eclipse (www.eclipse.org), se define como “An IDE for everything and nothing in particular” (un IDE para todo y para nada en particular). Eclipse es, en realidad, un armazón (workbench) sobre el que se pueden instalar herramientas de desarrollo para cualquier lenguaje, mediante la implementación de los plugins adecuados. El término plugin procede del inglés to plug, que significa enchufar. Es un software que permite cambiar, mejorar o agregar funcionalidades. La arquitectura de plugins de Eclipse permite, además de integrar diversos lenguajes sobre un mismo IDE, introducir otras aplicaciones accesorias que pueden resultar útiles durante el proceso de desarrollo, tales como herramientas UML (modelado de objetos), editores visuales de interfaces, ayuda en línea para librerías, etcétera. Usando distintas librerías es posible servirse de este entorno de desarrollo para otros lenguajes de programación, como Ada, C, C + +, COBOL, Perl, Delphi, PHP, Python, R. Ruby, Scala, Clojure y Scheme. A menudo el IDE Eclipse añade un apellido a su nombre cuando se usa para programar otro lenguaje. Por ejemplo, se llama Eclipse ADT (Ada Development Toolkit) para Ada, Eclipse CDT para C / C + +, Eclipse JDT para Java y Eclipse PDT para PHP. Esta lista de lenguajes aumenta con los años, ya que este IDE se está convirtiendo en el entorno de desarrollo de muchos programadores por su simplicidad y facilidad de uso.
1.2.1 El Consorcio Eclipse En su origen, el Proyecto Eclipse era un proyecto de desarrollo OpenSource, desarrollado y mantenido en su totalidad por IBM. Bajo la dirección de IBM, se fundó el Consorcio Eclipse, al cual se unieron algunas empresas importantes como Rational, HP o Borland. Desde el año 2004, el Consorcio Eclipse es independiente de IBM y entre otras empresas, está integrado por HP, QNX, IBM, Intel, SAP, Fujitsu, Hitachi, Novell, Oracle, Palm, Ericsson y RedHat, además de por algunas universidades e institutos tecnológicos.
5
1.2.2 Instalación de Java Developmente Kit (JDK) Es muy importante tener en cuenta que, para poder ejecutar el entorno de desarrollo Eclipse y las librerías de Android, es necesario tener instaladas en el ordenador las librerías de desarrollo de Java. Aunque ya está disponible la versión 1.7, para poder compilar aplicaciones Android, es necesario instalar la versión 6 de Java (también conocida como Java 1.6). Podemos descargar la versión correcta del JDK de Java en: http://java.sun.com/javase/downloads
6
Introducción
Nota: en el caso de Linux o Mac, es posible también instalar Java usando los programas habituales del sistema operativo que permiten la actualización de paquetes. Nota: si vas a instalar Eclipse y las librerías de Android en Linux, lee las notas que se encuentran en “Preguntas y Respuestas” de esta Introducción en la mesa del curso.
1.2.3 Instalación de Eclipse La instalación es muy sencilla. Simplemente accedemos a la página web: http://www.eclipse.org/downloads/
En esta página seleccionamos el tipo de Sistema Operativo donde vamos a instalar Eclipse y descargamos el archivo "Eclipse Classic 3.7".
7
Hay que tener en cuenta que debemos descargar la versión 32 bits o 64 bits en función del sistema operativo de que dispongamos. En el caso de Windows podemos ver el tipo de sistema operativo haciendo clic con el botón derecho del ratón en el icono "Equipo" o Mi PC del Escritorio y haciendo clic de nuevo en "Propiedades":
En el caso de Linux, desde la línea de comandos podemos ejecutar el siguiente comando para saber si el sistema operativo es de 64bits:
$ uname -m x86_64
En el caso de Apple Mac, utiliza el Perfil de Sistema para determinar si estás utilizando un kernel de 64 bits.
1. En el menú Apple ( "Más información":
8
), selecciona Acerca de este Mac y a continuación, haz clic en
Introducción
2. En el panel "Contenido", selecciona "Software". 3. Si Extensiones y kernel de 64 bits está configurada como Sí, estás utilizando un kernel de 64 bits.
Cuando hayamos descargado el fichero correspondiente, lo copiamos a un directorio o carpeta del ordenador y descomprimimos este fichero. Es recomendable usar un directorio sencillo que podamos recordar fácilmente, por ejemplo C:\cursos_Mentor\eclipse. Además, es muy importante que los nombres de los directorios no contengan espacios, pues Eclipse puede mostrar errores y no funcionar correctamente. Una vez descomprimido el fichero, Eclipse está listo para ser utilizado; no es necesario hacer ninguna operación adicional. Recomendamos hacer un acceso directo en el Escritorio del ordenador para arrancar rápidamente el entorno de programación Eclipse.
1.2.4 Instalación de las librerías de Android A continuación, debemos instalar el Paquete de Desarrollo de iniciación (en inglés, SDK Starter Package) de Android. Este paquete no incluye las librerías de desarrollo completas, sino que únicamente es el núcleo del SDK que se utiliza para descargar el resto de los componentes SDK, como la última plataforma Android. 9
Para descargar el fichero necesario, accedemos a la página de descarga del SDK de Android y nos bajamos la versión que corresponda en función del sistema operativo donde vayamos a instalarlo. Recomendamos que hay que descargar el archivo .zip, ya que con éste la instalación es más sencilla y rápida:
Cuando hayas descargado la versión .zip o .tgz, descomprímelo en el disco duro. Recomendamos que conviene usar el directorio C:\cursos_Mentor\Android\android-sdkwindows. Si usas otro directorio, toma nota del mismo, ya que, más adelante, tendrás que usar el nombre de este directorio para acabar de configurar el plugin de Android en Eclipse. Ahora vamos a instalar las librerías necesarias en Eclipse. Estas librerías se denominan Android Development Tools (ADT). Para ello, arrancamos Eclipse haciendo doble clic sobre el acceso directo que hemos creado anteriormente. A continuación, Eclipse pedirá que seleccionemos el "workspace", es decir, el directorio donde queremos guardar los proyectos.
10
Introducción
Seleccionaremos un directorio sencillo y fácil de recordar. Importante: Recomendamos usar el directorio C:\cursos_Mentor\Android\proyectos como carpeta personal
Finalmente hacemos clic en OK para abrir Eclipse:
Ahora vamos a configurar las preferencias de la versión de Java en Eclipse para compilar los proyectos de Android. Para ello, hacemos clic en la opción del menú “Window-> Preferences...”, hacemos clic en el panel izquierdo sobre “Java->Installed JREs” y seleccionamos “jre6” en el campo “Installed JREs”:
Si no podemos seleccionar "jre6" debemos usar el botón "Add" para añadir las librerías del JRE 6. Por ejemplo, en Windows, se pueden encontrar en el directorio: "C:\Program Files\Java\jre6". 11
Para finalizar, en esta ventana hay que seleccionar la versión de Java utilizada para compilar los proyectos de Android. Para ello hacemos clic en “Java->Compiler” y elegimos “1.6” en el campo “Compiler compliance settings”:
Si no hemos seleccionado la versión 6 de JRE en el paso anterior aparecerá el siguiente mensaje de error en Eclipse:
12
Introducción
Importante: Es necesario disponer de conexión a Internet para poder continuar con los siguientes pasos y poder descargar las librerías necesarias.
A continuación, seleccionamos en el menú Help -> Install New Software... En el cuadro de diálogo que aparecerá, introducimos la dirección del sitio de descarga de las librerías ADT: https://dl-ssl.google.com/android/eclipse/ Y pulsamos la tecla Intro. A continuación, marcamos todas las opciones tal y como se muestra en la siguiente captura de pantalla:
Es muy Importante comprobar la versión de Java. Si no, no se instalará bien el software de Android.
13
Hacemos clic en el botón "Next". Nota: este proceso puede llevar un rato en función de la conexión a Internet y de la potencia del ordenador que tengamos. Después, aparecerá la siguiente ventana:
Hacemos clic de nuevo en "Next", seleccionamos "I accept..." en el acuerdo de licencia y hacemos clic en "Finish":
14
Introducción
A continuación, se instalará el software necesario de Android:
Se mostrará este aviso de seguridad y pulsaremos en "OK" para continuar la instalación:
Al acabar la instalación, es necesario reiniciar Eclipse. Para ello haremos clic en "Restart":
15
Al arrancar de nuevo Eclipse ya dispondremos de las librerías necesarias para empezar a trabajar con Android:
Podemos hacer clic en la X de la pestaña "Welcome" para acceder al entorno de desarrollo:
La primera vez que accedemos al entorno Eclipse, aparece la siguiente ventana en la que debemos indicar dónde hemos instalado el SDK de Android:
16
Introducción
Hacemos clic en el botón “Browse...” e indicamos el directorio donde hemos instalado el SDK de Android. Si has seguido las instrucciones de Windows, el directorio por recomendado
para
descomprimir
el
archivo
del
SDK
es
“C:\cursos_Mentor\Android\android-sdk-windows”. Pulsamos el botón “OK” para finalizar la configuración.
A continuación, aparece otra ventana solicitando nuestra conformidad para enviar estadísticas de uso del SDK de Android. No es necesario hacerlo si no lo deseamos: 17
Pulsamos el botón “Finish” para finalizar la configuración. A continuación, aparece el siguiente mensaje indicando que no hemos instalado ninguna versión del sistema operativo Android:
Pulsamos el botón “OK”, en el siguiente paso, instalamos la versión de Android sobre la que vamos a trabajar en este curso. En el caso de que no aparezca la ventana que permite indicar a Eclipse dónde se encuentra el SDK de Android, podemos hacerlo manualmente. Para ello, hacemos clic en la opción del menú “Window-> Preferences...” izquierdo:
18
y seleccionamos “Android” en el panel
Introducción
Para acabar, pulsamos el botón “OK”.
1.2.5 Añadir versiones y componentes de Android El último paso en la configuración de las librerías de Android es descargar e instalar los componentes esenciales del SDK para el entorno de desarrollo. El SDK utiliza una estructura modular que separa las distintas versiones de Android, complementos, herramientas, ejemplos y la documentación en un único paquete que se puede instalar por separado. Para desarrollar una aplicación en Android, es necesario descargar, al menos, una versión. En este curso vamos a usar la versión 2.3, por ser la más extendida en el momento de redacción de la documentación. No obstante, vamos a emplear sentencias compatibles y recompilables en otras versiones. Para añadir esta versión hay que hacer clic en la opción “Android SDK Manager” del menú principal “Window” de Eclipse:
19
Se abrirá la ventana siguiente:
Para instalar la versión 2.3.3, seleccionamos los paquetes que se muestran en la siguiente ventana:
20
Introducción
Nota: la revisión de las versiones de Android puede ser superior cuando al alumno o alumna instale el SDK. Una vez hemos pulsado el botón “Install 6 packages”, aparece esta ventana y seleccionamos la opción “Accept All” y, después, hacemos clic en “Install”:
El instalador tarda un rato (10-20 minutos) en descargar e instalar los paquetes: 21
Para acabar, reiniciamos el ADB (Android Debug Bridge):
La instalación ha finalizado correctamente:
Ahora vamos a ver la estructura que tiene el SDK de Android. Para ello, abrimos el explorador en el directorio “C:\cursos_Mentor\Android\android-sdk-windows” o en el directorio donde lo hayamos descomprimido. La siguiente tabla describe los subdiretorios que contiene esta carpeta:
22
Introducción
NOMBRE CARPETA
DESCRIPCIÓN
add-ons/
Contiene los paquetes “add-on” del SDK de Android que permiten desarrollar aplicaciones usando librerías externas disponibles para algunos dispositivos o terminales.
docs/
Documentación completa en formato HTML, incluyendo la Guía del desarrollador y la guía de la API. Para leer la documentación, puedes abrir el fichero offline.html en un navegador Web.
platformtools/
Contiene las herramientas de desarrollo comunes del SDK que se actualizan con cada nueva versión de Android, tales como el ADB (Android Debug Bridge), así como otras herramientas que no se suelen utilizar directamente.
platforms/
Contiene las versiones de Android con las que se puede desarrollar aplicaciones en Eclipse. Cada versión se encuentra en un directorio independiente.
Directorio de la plataforma de la versión correspondiente, por ejemplo, "Android-10". Todos los directorios de la versión de Android contienen un
/ conjunto similar de archivos y la misma estructura de subdirectorios. Además, también incluye la librería de Android (android.jar) que se utiliza para compilar aplicaciones con esta versión de Android.
samples/
Contiene los ejemplos de código para esa versión específica de Android.
tools/
Contiene el conjunto de herramientas de desarrollo y creación de perfiles que son independientes de la versión de Android, como el emulador, el SDK de Android y AVD Manager, DDMS (Dalvik Debug Monitor Server), etcétera.
SDK Readme.txt
Archivo que explica cómo realizar la configuración inicial del SDK de Android.
SDK Manager.exe
Aplicación que inicia el SDK de Android y la herramienta AVD de gestión de paquetes. Sólo disponible en Windows.
23
Finalmente, vamos a incluir el directorio donde hemos instalado las librerías de Android en el PATH del sistema operativo. En concreto, vamos a incluir los directorios tools y platform-tools. Si has usado el directorio recomendado, los subdirectorios son: C:\cursos_Mentor\Android\android-sdk-windows\tools y C:\cursos_Mentor\Android\android-sdk-windows\platform-tools En Windows, se puede hacer esto accediendo al “Panel de control”, haciendo clic en el icono “Sistema”, seleccionando la pestaña “Opciones avanzadas” y haciendo clic en el botón “Variables de entorno”. A continuación, añadiremos los directorios anteriores a la variable PATH de la siguiente ventana:
En Windows 7 se puede acceder a la ventana anterior abriendo el “Panel de control”, haciendo clic en el icono “Sistema y Seguridad”, después en "Sistema" y, para acabar, en la opción "Configuración avanzada del sistema" para acceder a la ventana anterior:
24
Introducción
Escribiendo el comando path en una ventana de comandos de Windows podemos ver si se hemos modificado bien esta variable global del sistema:
Nota: en el resultado de este comando debemos ver el directorio de instalación de Android. Es muy importante que se muestre tal como aparece en la ventana anterior: con punto y coma al final del directorio SDK y sin espacios entre el punto y coma del directorio anterior. En OS X (Mac) y Linux, puedes agregar la ruta a la variable PATH con el comando SET o estableciendo la variable correspondiente en un script de inicio.
1.2.6 Definición del dispositivo virtual de Android Para poder hacer pruebas de las aplicaciones Android que desarrollemos sin necesidad de disponer de un teléfono Android, el SDK incluye la posibilidad de definir un Dispositivo Virtual de Android (en inglés, AVD, Android Virtual Device). Este dispositivo emula un terminal con Android instalado. Para definir el AVD, hacemos clic en la opción “Android AVD Manager” del menú principal “Window” de Eclipse:
25
Aparecerá la siguiente ventana:
Hacemos clic en el botón “New” de la ventana anterior y la completamos como se muestra en la siguiente ventana:
26
Introducción
La opción “Snapshot-> Enabled” permite guardar el estado del dispositivo de manera que todos los cambios que hagamos, como cambiar la configuración de Android o instalar aplicaciones, queden guardados. Así, la próxima vez que accedamos al emulador, se recupera automáticamente el último estado.
Importante: En el curso hemos creado un dispositivo virtual que no guarda el estado porque puede producir problemas de ejecución con Eclipse. En todo caso, el alumno o alumna puede usar la opción “Edit” del AVD cuando crea necesario que los últimos cambios sean almacenados para la siguiente sesión de trabajo
Para acabar, basta con hacer clic en “Create AVD”:
27
En esta Introducción puedes encontrar el vídeo “Cómo instalar Eclipse y el plugin Android”, que muestra de manera visual los pasos seguidos en las explicaciones anteriores
28
INTRODUCCIÓN AL ENTORNO ANDROID
ÍNDICE 1.1
INTRODUCCIÓN AL ENTORNO DE ANDROID .................... 31 1.1.1 Introducción ...................................................................................31 1.1.2 Características de Android...........................................................31 1.1.3 Arquitectura de Android ...............................................................33 1.1.4 Creación de un proyecto por líneas de comando ....................35
1.2
CONCEPTOS DE LAS APLICACIONES ANDROID ............... 37 1.2.1 Características de las aplicaciones Android.......................... 37 1.2.2 Componentes de las aplicaciones......................................... 37
1.3
CÓMO CREAR UN PROYECTO ANDROID ........................... 40 1.3.1 Un vistazo general al IDE de Eclipse ..................................... 40 1.3.1.1 Editores .................................................................................41 1.3.1.2 Vistas ................................................................................ …..41 1.3.1.3 Barras de Herramientas principal y secundarias ............43 1.3.1.4 Perspectivas .........................................................................43 1.3.2 Cómo crear un proyecto Android ...............................................46 1.3.2.1 Creación de un nuevo proyecto ........................................47 1.3.2.2 Nombre de la actividad .......................................................49 1.3.2.3 Descripción de los ficheros por defecto del proyecto....49 1.3.2.4 Ejecución del proyecto Android ........................................53 1.3.2.5 Cómo usar el emulador de Android (AVD) .......................56
1.4
CÓMO CREAR LA PRIMERA APLICACIÓN CON ANDROID63 1.4.1 Cambio en la Interfaz de usuario con Layout .........................66
1.5
DISEÑO DE LA INTERFAZ DE USUARIO .............................. 70 1.5.1 Cómo diseñar la interfaz de usuario mediante Vistas ..............70 1.5.2 Vistas disponibles de Android .....................................................71
2
Introducción al entorno Android
1.1
INTRODUCCIÓN AL ENTORNO DE ANDROID
1.1.1 Introducción En esta Unidad vamos a explicar las características y la arquitectura de Android. Además, describiremos el entorno de desarrollo Eclipse y crearemos nuestro primer proyecto Android. También, detallaremos los ficheros básicos que componen un proyecto Android. Finalmente, usaremos Paneles de diseño (Layout) y Componentes (View) para diseñar la interfaz de usuario en ejemplos de aplicaciones de Android.
1.1.2 Características de Android A continuación se muetra un resumen de las características más importantes:
Diseñado para dispositivo pequeños
El sistema operativo es compatible con pantallas VGA (y mayores), gráficos 2D y gráficos 3D presentes en muchos teléfonos tradicionales.
Almacenamiento
Dispone de la base de datos ligera SQLite donde se almacenan los datos de las aplicaciones.
Conectividad
Android soporta las siguientes tecnologías de conectividad: GSM/EDGE, IDEN, CDMA, EV-DO, UMTS, Bluetooth, Wi-Fi, LTE y WiMAX. Algunas son muy populares en los teléfonos actuales y otras se están desarrollando.
Mensajería
Se pueden usar tanto SMS como MMS.
Navegador web
El navegador web incluido en Android está basado en el motor del navegador de código abierto WebKit. Este navegador es muy eficiente y permite cargar las páginas Web rápidamente.
Soporte de Java
Aunque las aplicaciones se escriben en el lenguaje Java, no hay una Máquina Virtual de Java en el sistema operativo para ejecutar el código. Este código Java se compila en un ejecutable Dalvik y se ejecuta en la Máquina Virtual Dalvik. Dalvik es una máquina virtual especializada, diseñada específicamente para Android y optimizada para dispositivos móviles que funcionan con batería y que tienen memoria y procesador limitados. Es posible incluir las librerías J2ME nativas de Java mediante aplicaciones de terceros, como J2ME MIDP Runner. 31
Android soporta los siguientes formatos multimedia: WebM, H.263, H.264 (en 3GP o MP4), MPEG-4 SP, AMR, AMR-WB (en un contenedor Soporte multimedia 3GP), AAC, HE-AAC (en contenedores MP4 o 3GP), MP3, MIDI, Ogg Vorbis, WAV, JPEG, PNG, GIF y BMP.
Soporte para streaming (distribución en Internet)
Android soporta los siguientes formatos de streaming: RTP/RTSP, descarga progresiva de HTML (tag de HTML5). Adobe Flash Streaming (RTMP) es soportado mediante la instalación de Adobe Flash Player, pero sólo para algunos terminales.
Android puede manejar cámaras de fotos, de vídeo, pantallas táctiles, Soporte para GPS, acelerómetros, giroscopios, magnetómetros, sensores de hardware adicional proximidad y de presión, termómetro, aceleración 2D y 3D.
Entorno de desarrollo
El entorno de desarrollo es Eclipse 3.7 y el plugin de Herramientas de Desarrollo de Android (ADT) que incluye un emulador de dispositivos, herramientas de depuración y análisis de rendimiento.
Market (Mercado de aplicaciones)
El Android Market es un catálogo de aplicaciones gratuitas y de pago que pueden ser descargadas e instaladas desde los propios dispositivos Android.
Multi-táctil
Android tiene soporte nativo para pantallas multi-táctiles que permiten manejar la pantalla táctil con más de 1 dedo.
Bluetooth
En la versión 2.2 de Android se incluye la funcionalidad completa.
Videollamada
Android incluye la posibilidad de videollamada a través de Google Talk.
Multitarea
Existe la multitarea real de aplicaciones, es decir, las aplicaciones que no se están ejecutando en primer plano reciben ciclos de reloj del procesador para actualizar su estado.
Características basadas en voz
Es posible dar órdenes de voz al terminal. Por ejemplo, la búsqueda en Google a través de la voz ya estaba disponible desde la primera versión.
Android incluye la compartición de la conexión a Internet (en inglés, Tethering tethering), que permite usar el teléfono como un punto de acceso (compartición de inalámbrico, de manera que un ordenador puede usar la conexión 3G conexión a Internet) del móvil Android.
32
Introducción al entorno Android
1.1.3 Arquitectura de Android Los componentes principales de la arquitectura del sistema operativo Android son los siguientes: Aplicaciones: todas las aplicaciones están escritas en lenguaje de programación Java. Las aplicaciones incluidas por defecto son un cliente de correo electrónico, programa de SMS, calendario, mapas, navegador, contactos, etcétera. Todas las aplicaciones de Android usan el siguiente conjunto de servicios y sistemas: o Un conjunto de componentes (Views) que se usan para crear las interfaces de usuario. Por ejemplo, botones, listas, tablas, cajas de texto, etcetera. o Proveedores de contenidos (Content Providers) que permiten a las aplicaciones acceder a la información de otras aplicaciones (por ejemplo, los Contactos del teléfono) o compartir datos entre ellas. o Gestor de recursos (Resource Manager), que permite acceder a recursos que no sean del código fuente, tales como textos de internacionalización, imágenes y ficheros de estilos (layout). o Gestor de notificaciones (Notification Manager), que permite
a todas las
aplicaciones mostrar alertas en la barra de estado de Android. o Gestor de actividades (Activity Manager), que controla el ciclo de vida de la aplicación. Marco de desarrollo de aplicaciones: los programadores tienen acceso completo a las mismas APIs (librerías) del Framework (marco de desarrollo) utilizadas por las aplicaciones base. La arquitectura está diseñada para simplificar la reutilización de componentes, es decir, cualquier aplicación puede publicar sus capacidades y cualquier otra aplicación puede hacer uso de estas capacidades. Librerías: Android incluye también un conjunto de librerías de C/C++ usadas por varios componentes del sistema. Entre ellas, se encuentran: System C library (implementación de la librería C estándar), librerías de medios, bibliotecas de gráficos, 3D y SQLite, entre otras. El programador puede hacer uso de estas librerías. Runtime (ejecutable) de Android: Android también incluye un conjunto de librerías base que proporcionan la mayor parte de las funciones del lenguaje Java. Cada aplicación Android ejecuta un proceso con instancia individual de la máquina virtual Dalvik. Núcleo Linux: Android está basado en Linux para los servicios base del sistema, como seguridad, gestión de memoria, procesos y controladores.
33
DIAGRAMA DE LA ARQUITECTURA ANDROID
La utilidad de línea de comandos genera automáticamente todos los archivos necesarios para crear un proyecto Android; incluso permite crear un proyecto para Eclipse.
Android usa Java como lenguaje base para el desarrollo de las aplicaciones. Por lo tanto, hace uso de los Paquetes Java (Package en inglés). Estos paquetes son contenedores de clases que permiten agrupar las distintas partes de un programa cuya funcionalidad tienen elementos comunes. El uso de paquetes proporciona las siguientes ventajas:
34
o
Agrupamiento de clases con características comunes
o
Reutilización de código
o
Mayor seguridad al existir niveles de acceso
Introducción al entorno Android
1.1.4 Creación de un proyecto por líneas de comando Usando la línea de comandos vamos a crear un proyecto Android. Es importante usar el directorio de trabajo que hemos creado anteriormente con Eclipse: C:\cursos_Mentor\Android\proyectos. Desde este directorio, debemos ejecutar el siguiente comando:
android create project --package es.mentor.eje1.unidad1.bienvenido --activity Bienvenido --target android-10 --path unidad1.eje1.bienvenido
para crear los ficheros básicos de un proyecto Android. Fíjate en qué la orden anterior es una única línea que debes ejecutar en la línea de comandos de tu sistema operativo.
C:\>cd C:\cursos_Mentor\Android\proyectos C:\cursos_Mentor\Android\proyectos> android create project --package es.mentor.unidad1.eje1.bienvenido --activity Bienvenido --target android-10 bienvenido
--path
Created project directory: C:\cursos_Mentor\Android\proyectos\bienvenido Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\src\es\mentor\eje1\unidad1\bienvenido Added file C:\cursos_Mentor\Android\proyectos\bienvenido\src\es\mentor\eje1\unidad1\bienvenido\Bienv enido.java Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\bin Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\libs Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res\values Added file C:\cursos_Mentor\Android\proyectos\bienvenido\res\values\strings.xml Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res\layout Added file C:\cursos_Mentor\Android\proyectos\bienvenido\res\layout\main.xml Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res\drawable-hdpi Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res\drawable-mdpi Created directory C:\cursos_Mentor\Android\proyectos\bienvenido\res\drawable-ldpi Added file C:\cursos_Mentor\Android\proyectos\bienvenido\AndroidManifest.xml Added file C:\cursos_Mentor\Android\proyectos\bienvenido\build.xml Added file C:\cursos_Mentor\Android\proyectos\bienvenido\proguard.cfg C:\cursos_Mentor\Android\proyectos>
35
Ahora no vamos a examinar en detalle los ficheros que hemos creado y que conjuntamente forman el proyecto Android. En el siguiente apartado de esta Unidad los detallaremos con un ejemplo real en Eclipse.
Este script genera los siguientes directorios y archivos principales: /src: en este directorio es donde se almacenan los archivos de código fuente Java (con extensión .java). /assets: en este directorio se guardan los recursos que utiliza la aplicación. /res: es el directorio principal de recursos (resources). Aquí guardaremos imágenes o archivos multimedia que utilice nuestra aplicación. /res/drawable-Xdpi: son los directorios de recursos gráficos o imágenes que utilizará nuestra aplicación con los nombres drawable-hdpi, drawable-mdpi y drawable-ldpi; en ellos se almacenan las imágenes dependiendo de la densidad de puntos por pulgada que tenga el dispositivo en el que se ejecute la aplicación. /res/layout: en Android hay que separar el código Java de la aplicación y la interfaz gráfica. En este directorio es donde colocaremos los archivos xml que definen las vistas que utilizará la aplicación. /res/values: de igual forma que separamos el código Java y la interfaz gráfica, Android separa también las cadenas constantes de texto (Internacionalización de la aplicación), las matrices, la paleta de colores, etcétera. AndroidManifest.xml: es el archivo de configuración de la aplicación en el que se define lo que puede hacer nuestra aplicación, es decir, en él informamos al sistema operativo de las capacidades que tiene esta aplicación. En este archivo también indicaremos las actividades o servicios que ejecutará nuestra aplicación y los permisos de seguridad especiales necesarios si va a acceder a recursos compartidos del sistema, como por ejemplo el acceso al listado de contactos, empleo del GPS o la posibilidad de enviar mensajes SMS. default.properties: fichero de proyecto para Eclipse. Nota: una vez analizado los ficheros básicos del proyecto, podríamos importarlo en Eclipse. No obstante, por simplificación, no lo vamos a hacer, pues es más sencillo crear un proyecto nuevo directamente desde Eclipse.
Así pues, borramos el directorio C:\cursos_Mentor\Android\proyectos\bienvenido, para crear este mismo proyecto desde Eclipse directamente.
36
Introducción al entorno Android
1.2
CONCEPTOS DE LAS APLICACIONES ANDROID
1.2.1 Características de las aplicaciones Android Las Aplicaciones Android se forman con uno o más de los siguientes componentes: actividades, servicios, proveedores de contenidos y receptores de mensajes. Cada componente tiene una funcionalidad diferente en la aplicación; incluso la aplicación puede activar cada uno de los componentes de forma individual; es más, otras aplicaciones también los pueden activar. El archivo de manifestación (manifest) indica todos los componentes que usa la aplicación; en él también deben declararse todos los requisitos necesarios de la misma como, por ejemplo, la versión mínima de Android, las configuración mínima de hardware, etcétera. El código que no es de la aplicación, como las imágenes, cadenas de internacionalización, diseño de la interfaz de usuario, otros recursos, etcétera, puede incluir distintas configuraciones en función del idioma del teléfono o diseños de la interfaz del usuario en función de los diferentes tamaños de pantalla. Las aplicaciones de Android están escritas en el lenguaje de programación Java. Las herramientas de SDK de Android compilan este código, junto con sus datos y los archivos de recursos, en un paquete Android. Este archivo tiene la extensión .apk y lo utiliza Android para instalar la aplicación. Una vez instalada una aplicación en un dispositivo, Android la aloja en su propia caja de arena (sandbox). Sandbox, palabra del inglés que significa caja de arena (Sand+box), es un sistema informático que aísla los procesos; de esta manera se pueden ejecutar aplicaciones de forma independiente. Se utiliza para evitar la corrupción de datos del sistema donde éstos se ejecutan. El sistema Android aplica el principio de “privilegios mínimos” (recuerda que Android se basa en el kernel de Linux). Es decir, cada aplicación, por defecto, sólo tiene acceso a los componentes que necesita para hacer su trabajo y nada más. Esto crea un entorno muy seguro en el que una aplicación no puede acceder a las partes de un sistema para las que no tiene permiso.
1.2.2 Componentes de las aplicaciones Los Componentes de las aplicaciones son los elementos esenciales de una aplicación Android. Cada componente es un punto diferente de entrada por el que el sistema operativo puede interaccionar con la aplicación. No todos los componentes son verdaderos puntos de entrada, sino que algunos dependen unos de otros, aunque cada uno exista como una entidad separada en Android y desempeñe un papel específico que define el comportamiento general de la aplicación. 37
Existen los siguientes tipos de componentes de la aplicación:
Actividades (Activities): una actividad representa una pantalla única con una interfaz de usuario. Por ejemplo, una aplicación de correo electrónico puede tener una actividad que muestra una lista de correo electrónico nuevo, otra actividad que compone un correo y otra actividad que lee los mensajes. Aunque las actividades trabajan conjuntamente para dar la sensación de una única aplicación, cada una de ellas es independiente de las otras. Por lo tanto, otra aplicación externa diferente podría iniciar cualquiera de estas actividades (si la aplicación de correo electrónico lo permite). Por ejemplo, una aplicación que gestiona los contactos podría iniciar la actividad que compone nuevos mensajes de correo indicando como destinatario del mensaje al contacto seleccionado en la primera aplicación. Una actividad se implementa a partir de la clase Java Activity. Más adelante veremos cómo se usa. Puedes pensar en una actividad de Android como si fuera una ventana en una aplicación de escritorio o una página HTML en una aplicación Web. Android está diseñado para cargar muchas actividades pequeñas, por lo que se permite al usuario abrir nuevas actividades y pulsar el botón “Atrás” para ir a un estado anterior, al igual que se hace en un navegador web. Servicios (Services): un servicio es un componente que se ejecuta en segundo plano y que realiza operaciones cada cierto tiempo. Un servicio no proporciona una interfaz gráfica al usuario. Por ejemplo, un servicio puede reproducir música en segundo plano mientras el usuario está en otra aplicación, o puede obtener información de Internet sin la interacción del usuario. Otros componentes, como una actividad, pueden iniciar un servicio e interactuar con él si es necesario. Un servicio se implementa a partir de la clase Java Service. Más adelante veremos cómo se usa. Proveedores de contenidos (Content providers): un proveedor de contenidos maneja el conjunto de datos compartido de la aplicación. Puede almacenar información en el sistema de archivos, en una base de datos SQLite, en Internet o en cualquier otro lugar de almacenamiento permanente al que la aplicación tenga acceso. A través del proveedor de contenidos, otras aplicaciones pueden consultar e incluso modificar los datos (si el proveedor de contenidos lo permite). Por ejemplo, Android proporciona un proveedor de contenidos que gestiona la información de los contactos del teléfono. Por lo tanto, cualquier aplicación, con los permisos adecuados, puede hacer
una
consulta
al
proveedor
de
contenido
de
los
contactos
(ContactsContract.Data) para leer y escribir información sobre una persona en particular. 38
Introducción al entorno Android
Los proveedores de contenidos se utilizan también para escribir y leer datos que son privados de la aplicación y no se comparten. Por ejemplo, una aplicación de Notas puede utilizar un proveedor de contenidos para guardar las notas. Un proveedor de contenidos se implementa a partir de la clase ContentProvider y debe implementar un conjunto estándar de métodos (API) que permiten a otras aplicaciones interaccionar con él. Más adelante veremos cómo se usa. Receptores de mensajes (Broadcast receivers): un receptor de mensajes responde a mensajes difundidos (broadcast) a todos los elementos del sistema. Por ejemplo, un mensaje puede anunciar que la pantalla se ha apagado, la batería está descargada o que se ha capturado una foto. Las aplicaciones también pueden emitir este tipo de mensajes para, por ejemplo, indicar a otras aplicaciones que ciertos datos ya han sido descargados en el dispositivo y están disponibles para ser utilizados. Aunque estos receptores de mensajes no muestran información en la interfaz del usuario, sí que pueden crear una notificación en la barra de estado (la barra que aparece arriba en Android) para alertar al usuario cuando se produce este tipo de mensajes. Un receptor de mensajes se implementa a partir de la clase BroadcastReceiver y cada mensaje emitido es un objeto del tipo Intent (Intención). Más adelante veremos cómo se usa. Componentes de la pantalla de inicio (Widgets): estos componentes visuales se usan principalmente en la Pantalla de inicio (HomeScreen) de Android para mostrar información que se actualiza periódicamente como, por ejemplo, un reloj, la previsión del tiempo, etcétera. Al tratarse de programación avanzada no los estudiaremos en este curso de iniciación a Android. Otros componentes de Android son las Carpetas animadas (Live Folders) y los Fondos de pantalla animados (Live Wallpapers) en la Pantalla de Inicio. Las carpetas animadas permiten a Android mostrar información en la pantalla inicial sin necesidad de lanzar la aplicación correspondiente. Igualmente, al tratarse de programación avanzada, no los estudiaremos en este curso de iniciación a Android.
Un aspecto único del diseño del sistema Android es que cualquier aplicación puede iniciar un componente de otra aplicación. Por ejemplo, si es necesario para la aplicación abierta capturar una imagen con la cámara de fotos, seguramente ya exista otra aplicación que hace eso y que la aplicación inicial puede reutilizar en lugar de desarrollar el código necesario para capturar la foto. Únicamente hay que iniciar la actividad de la aplicación de la cámara de fotos y capturar la imagen. La sensación del usuario es como si la cámara formara parte de la aplicación inicial. Cuando el sistema arranca un componente, éste inicia un proceso (si no está ya en ejecución) para que la aplicación cree las instancias de las clases necesarias del componente. 39
Por ejemplo, si una aplicación inicia la actividad de la aplicación de la cámara que hace fotos, la actividad se ejecuta en el proceso que pertenece a la aplicación de la cámara, no en el proceso de la aplicación original que ha hecho la llamada a la otra aplicación. Por lo tanto, a diferencia de otros sistemas operativos, las aplicaciones de Android no tienen un punto de entrada único (no hay función main()). Debido a que el sistema ejecuta cada aplicación en un proceso independiente con permisos restringidos, ésta no puede activar directamente un componente de otra aplicación, sino que es el sistema operativo Android el encargado de hacerlo. Por lo tanto, para activar un componente de otra aplicación es necesario enviar un mensaje al sistema que especifica su intención (clase Intent) de iniciar un componente en particular. Por lo tanto, el sistema operativo es el encargado de activar el componente solicitado.
1.3
CÓMO CREAR UN PROYECTO ANDROID
1.3.1 Un vistazo general al IDE de Eclipse Antes de crear el primer proyecto de Android, vamos a echar un primer vistazo al entorno de desarrollo de Eclipse para conocer sus características básicas, la forma en que organiza el proyecto y las herramientas adicionales que ofrece. La primera vez que se ejecuta Eclipse se puede ver una pantalla muy similar a la que se muestra a continuación.
40
Introducción al entorno Android
1.3.1.1
Editores
La ventana principal (la más grande) se llama “Editor”. El Editor es el espacio donde se escribe el código fuente de los programas que estamos desarrollando. Es posible tener varios ficheros de código fuente abiertos a la vez, apilados uno encima de otro. En la parte superior de la ventana del Editor se muestran las pestañas que permiten acceder a cada uno de los ficheros abiertos (o bien cerrarlos directamente).
Editor
1.3.1.2
Vistas
Además del Editor, existe un segundo tipo de ventanas “secundarias”, que se llaman Vistas. Las Vistas son ventanas auxiliares para mostrar información, introducir datos, etcétera. Las Vistas se usan con múltiples propósitos, desde navegar por un árbol de directorios, hasta mostrar el contenido de una consulta SQL.
41
Vistas
En función de las librerías de desarrollo (Android, GWT, Java, Delphi...) se definen Editores propios y todas las Vistas necesarias. En la ventana anterior están abiertas dos Vistas:
La Vista vertical de la izquierda muestra el árbol de directorios de los proyectos con los ficheros del mismo.
La Vista horizontal inferior muestra una pequeña “agenda” de tareas pendientes que pueden ser introducidas por el usuario, de forma directa, o por Eclipse, en función de determinados eventos (compilación del proyecto, depuración de código, etcétera).
Si deseamos cambiar las Vistas, se puede usar la opción “Show View” en el menú de la pestaña “Window”.
42
Introducción al entorno Android
1.3.1.3
Barras de Herramientas principal y secundarias
La barra de herramientas principal contiene los accesos directos a las operaciones más comunes, como abrir y guardar archivos. Además, también es posible ejecutar herramientas externas y tareas relacionadas con el Editor activo, como ejecutar un programa, depurar el código fuente, etcétera.
Además de la barra de herramientas principal (imagen anterior), cada Vista puede tener su propia barra de herramientas secundaria.
1.3.1.4
Perspectivas
Una Perspectiva es un conjunto de ventanas (Editores y Vistas) agrupadas que simplifican el desarrollo de un proyecto. Al seleccionar una Perspectiva se carga una configuración guardada de las Vistas y Editores de nuestro entorno de desarrollo Eclipse. Por ejemplo, existe una Perspectiva "Java Browsing" que facilita el desarrollo de aplicaciones Java y que incluye, además del Editor, Vistas para navegar por las clases, los paquetes, etcétera. Se puede cambiar la perspectiva activa utilizando la opción “Open Perspective” del menú de Windows. Desde este mismo menú también es posible definir Perspectivas personalizadas.
43
También existe un botón en la barra de herramientas principal para cambiar de Perspectiva:
Si el alumno tiene dudas sobre el uso avanzado de Eclipse, en Internet existen muchos tutoriales que indican cómo utilizarlo. Además, es posible usar el menú "Help" o la tecla [F1] para solicitar ayuda. Desgraciadamente, a día de hoy, esta ayuda sólo se encuentra en inglés.
Importante: para importar en Eclipse el código fuente de los ejemplos del curso hay que usar la opción del menú principal: File -> Import.
44
Introducción al entorno Android
Después, hay que marcar Existing Proyects into Workspace en la ventana emergente y pulsar en el botón “Next”:
Finalmente, seleccionamos el directorio de trabajo donde debemos haber copiado previamente
los
ficheros
con
el
código
fuente
de
los
ejemplos:
“C:\cursos_Mentor\Android\proyectos” y hacemos clic en “Finish”:
45
Nota: en esta Unidad 1 puedes encontrar el vídeo “Cómo cargar los ejemplos en Eclipse”, que muestra cómo se importan los ficheros con el código fuente de los proyectos que son los ejemplos del curso.
Importante: en el apartado “Problemas al cargar proyectos de Android” de Preguntas y Respuestas (FAQ) de la Unidad 1 puedes encontrar soluciones a los problemas que ocurren al importar el código fuente de los ejemplos del curso.
1.3.2 Cómo crear un proyecto Android A continuación, vamos a describir cómo crear un proyecto usando Eclipse y las librerías de Android que hemos instalado con anterioridad. Se trata del primer proyecto que el alumno va a crear, por lo que es muy importante prestar atención a los pasos seguidos, ya que los proyectos siguientes se generan de manera similar.
Así pues, arrancamos Eclipse.
46
Introducción al entorno Android
1.3.2.1
Creación de un nuevo proyecto
En el menú de Eclipse hacemos clic en File->New->Project:
También podemos hacer clic en el botón del menú de herramientas de Eclipse haciendo clic en la opción “Open a wizard to help create a new Android project”:
A continuación, aparece una nueva ventana en la que escribimos el nombre de proyecto "unidad1.eje1.bienvenido", "es.mentor.unidad1.eje1.bienvenido" para el paquete de java (Package) y “Bienvenido” para Activity. El resto de opciones las dejamos como aparecen en la siguiente captura de pantalla:
47
Pulsamos el botón “Finish” para crear los ficheros del proyecto. A continuación, describimos los apartados que genera un proyecto Android:
Project Name: como su nombre indica es el nombre del proyecto Eclipse; corresponde con el nombre del directorio que contiene los ficheros del proyecto.
Build Target: indica la versión del Android SDK que vamos a usar para compilar la aplicación. Por ejemplo, si seleccionas Android 2.3, la aplicación se compilará para funcionar en esta versión de Android y las siguientes. La versión seleccionada aquí no tiene que coincidir con la versión del Emulador de Android (AVD), ya que las aplicaciones de Android están diseñadas de manera que se
48
Introducción al entorno Android
ejecutan en la plataforma en la que se desarrollaron y en todas las versiones superiores de Android. Por ejemplo, una aplicación que de desarrolló para la versión 2.1 se ejecutará bien en la versión 2.3.3. Al contrario no funciona.
Application Name: es el nombre de la aplicación que aparece en el icono del Escritorio de Android. Es el texto que ve el usuario del teléfono.
Package Name: es el nombre del paquete Java en el que se almacena todo el código fuente de la aplicación.
Create Activity: define el nombre de la Actividad.
Importante: El nombre del paquete debe ser único en relación con todos los paquetes instalados en Android. Por esta razón, es importante utilizar el estándar de dominio para nombrar los paquetes de las aplicaciones. En el ejemplo anterior se utiliza el nombre de paquete "es.mentor". A la hora de desarrollar tus propias aplicaciones y distribuirlas en el Android Market (Mercado de aplicaciones Android) debes utilizar nombres propios de paquetes. En ningún caso debes utilizar el nombre “es.mentor” para distribuir aplicaciones en el Android Market, pues sólo es válido para los ejemplos del curso.
1.3.2.2
Nombre de la actividad
Importante: El nombre de la actividad no puede incluir tildes, la letra “ñ”, ni caracteres raros.
1.3.2.3
Descripción de los ficheros por defecto del proyecto
Para ver los ficheros del proyecto Android creados por Eclipse, en la barra lateral Package Explorer, desplegamos las entradas haciendo clic en los ficheros marcados con flechas rojas de los diferentes paquetes:
49
A continuación, vamos a explicar la estructura y contenido de los ficheros del proyecto. Carpeta/src/ Contiene todo el código fuente de la aplicación, código de la interfaz gráfica, clases auxiliares, etcétera. Inicialmente, Eclipse crea el código básico de la Actividad (Activity) principal de la aplicación, debajo del paquete Java definido. Carpeta /res/ Contiene todos los ficheros de recursos necesarios para el proyecto: imágenes, vídeos, cadenas de texto (para internacionalización de la aplicación), etcétera. Los diferentes tipos de recursos se deben distribuir entre las siguientes subcarpetas:
/res/drawable-X/: contiene las imágenes de la aplicación. Se divide en /drawableldpi, /drawable-mdpi y /drawable-hdpi para utilizar diferentes recursos dependiendo de la resolución del dispositivo.
/res/layout/: contiene los ficheros de definición de las diferentes pantallas de la interfaz gráfica. Se puede usar la carpeta /layout y /layout-land para definir los diferentes diseños en función de la orientación del dispositivo.
/res/anim/. alberga la definición de las animaciones utilizadas por la aplicación.
/res/menu/: contiene la definición de los menús de la aplicación.
/res/values/: contiene otros recursos de la aplicación como, por ejemplo, cadenas de texto (strings.xml), estilos (styles.xml), colores (colors.xml), etcétera.
50
/res/xml/: contiene los ficheros XML utilizados por la aplicación.
Introducción al entorno Android
/res/raw/: contiene los recursos adicionales, normalmente en diferente formato a XML, que no se incluyan en el resto de carpetas de recursos.
Carpeta /gen/ Reúne una serie de elementos de código generados automáticamente al compilar el proyecto. Cada vez que compilamos el proyecto, Android genera una serie de ficheros fuente Java dirigidos al control de recursos de la aplicación. El archivo más importante es el que se puede observar en la imagen anterior, el fichero R.java que define la clase Java denominada R. Esta clase R contiene un conjunto de constantes con los ID de todos los recursos de la aplicación incluidos en la carpeta /res/, de forma que el programador pueda acceder fácilmente a estos recursos desde el código fuente a través de esta clase. Así, por ejemplo, la constante R.drawable.icon define el ID de la imagen “icon.png” contenida en la carpeta /res/drawable/. Veamos como ejemplo la clase R creada por defecto para el proyecto nuevo:
/* AUTO-GENERATED FILE. DO NOT MODIFY. * * This class was automatically generated by the * aapt tool from the resource data it found. It * should not be modified by hand. */ package es.mentor.unidad1.eje1.bienvenido; public final class R { public static final class attr { } public static final class drawable { public static final int icon=0x7f020000; } public static final class layout { public static final int main=0x7f030000; } public static final class string { public static final int app_name=0x7f040001; public static final int hello=0x7f040000; } }
Importante: Esta clase la crea automáticamente Android por lo que no debemos modificarla.
51
Carpeta /assets/ Alberga el resto de ficheros auxiliares necesarios para que aplicación funcione, como los ficheros de configuración, de datos, etcétera. La diferencia entre los recursos incluidos en la carpeta /res/raw/ y los incluidos en la carpeta /assets/ es que para los primeros se generará un ID en la clase R y se deberá acceder a ellos usando un método de esta clase. Sin embargo, para los segundos no se generarán un ID y se puede acceder a ellos por su ruta como a cualquier otro fichero del sistema. Aplicaremos unos u otros según las necesidades de nuestra aplicación. Carpeta /bin/ Es el directorio donde se guarda la aplicación una vez se ha compilado. Carpeta /libs/ Es el directorio donde se almacenan las librerías de tipo JAR que amplían las funcionalidades de la aplicación. Fichero AndroidManifest.xml: Contiene la definición en formato XML de las características principales de la aplicación, como, por ejemplo, su identificación (nombre, versión, icono, etcétera), sus componentes (Actividades, Mensajes, Servicios, etcétera) o los permisos necesarios para su ejecución. Veremos más adelante otros detalles de este fichero.
Importante: Haciendo doble clic sobre estos ficheros podemos abrirlos en el Editor de Eclipse. Es importante que el alumno se familiarice con este entorno de desarrollo y pruebe las distintas opciones del mismo.
Al abrir los distintos ficheros veremos la siguiente ventana:
52
Introducción al entorno Android
1.3.2.4
Ejecución del proyecto Android
Una vez hemos creado el proyecto, vamos a explicar cómo ejecutamos esta aplicación de prueba con Eclipse y el emulador de Android (AVD: Android Virtual Device). Para ello, hacemos clic en el botón "Ejecutar" de la barra de herramientas principal
o en la opción "Run" de menú “Run”. También disponemos del atajo del teclado [Ctrl+F11]
53
Si no aparece ningún problema de compilación, entonces aparecerá la siguiente ventana:
Eclipse inicia el emulador de Android AVD en el ordenador que estés utilizando, para que puedas probar el proyecto que has desarrollado. Ten en cuenta que el dispositivo virtual tarda un rato en cargar cada vez que lo inicias. Hay que tener un poco de paciencia hasta que parezca la ventana de inicio.
54
Introducción al entorno Android
En la consola de Eclipse puedes ir viendo el progreso de todo el proceso. Eclipse instala automáticamente la nueva aplicación en el AVD y la ejecuta:
Cuando accedemos por primera vez al emulador, aparece la pantalla de bienvenida con el terminal bloqueado:
Para desbloquear la pantalla hay que arrastrar el icono "candado" con el ratón hacia la derecha. Una vez desbloqueado el AVD, podemos ver el aspecto de la aplicación instalada:
55
Importante: En general, cada vez que modifiquemos el código fuente y deseemos probar de nuevo nuestro proyecto no es necesario parar el emulador de aplicaciones y arrancarlo de nuevo; simplemente hacemos clic de nuevo en el botón “Run” y Eclipse compilará, reinstalará y ejecutará la aplicación modificada. Una vez hayamos acabado de probar nuestro proyecto, es necesario parar el emulador de Android.
Atención: En el Emulador de Android es posible probar varios proyectos a la vez.
Nota: En esta Unidad 1 puedes encontrar el vídeo “Cómo ejecutar un proyecto Android”, que muestra cómo usar Eclipse para compilar y ejecutar los proyectos que son los ejemplos del curso.
1.3.2.5
Cómo usar el emulador de Android (AVD)
Como puedes observar, el Emulador de Android simula un teléfono con botones (lado derecho de la ventana). Si ya sabes utilizar este tipo de teléfonos no tendrás ningún problema en manejar el emulador como si fuera un teléfono más. Si no conoces Android, lee las siguientes instrucciones para ver cómo se maneja. Cambiaremos el idioma del sistema operativo. 56
Introducción al entorno Android
El botón “Volver a atrás”, permite cerrar la aplicación y volver al “Escritorio” de Android:
En la pantalla que aparece a continuación, debemos desbloquear el dispositivo virtual. Para ello, arrastramos con el ratón la barra que tiene un candado dibujado hacia la derecha:
Después, aparece la pantalla denominada “Pantalla Inicial” (en inglés se denomina Home Screen), podemos acceder a todas las actividades instaladas haciendo clic en el icono marcado con una flecha roja en la imagen siguiente:
57
Si lo hacemos, veremos las aplicaciones instaladas. A esta pantalla se la denomina Pantalla de lanzamiento (en inglés se denomina Launcher Screen):
58
Introducción al entorno Android
Para movernos en esta pantalla, podemos usar el ratón como si fuera el dedo de tu mano. Es decir, para ver los iconos que están abajo hay que hacer clic en la pantalla y, sin soltar el botón del ratón, arrastrar la ventana hacia arriba:
Arrastrar hacia abajo con el ratón
Haciendo clic con el ratón sobre el icono de una de las aplicaciones, el emulador la ejecutará. A continuación, vamos a modificar el idioma del sistema operativo. Para ello, haciendo clic en el icono “Settings” aparece la siguiente pantalla:
Arrastrar hacia con el ratón
abajo
59
Desplazando con el ratón hacia arriba esta ventana hacemos clic en “Language & keyboard”:
En la siguiente pantalla hacemos clic en “Select language”:
60
Introducción al entorno Android
Para acabar, desplazamos de nuevo la pantalla hacia arriba hasta que veamos el idioma en el que deseamos configurar Android:
Arrastrar hacia con el ratón
abajo
Hacemos clic sobre el idioma correspondiente y el sistema operativo queda configurado:
61
Después, debemos desmarcar la opción “Japanese IME” de esta pantalla:
Si usamos el botón “Volver atrás”, veremos que el idioma del sistema operativo ha cambiado en la Pantalla Inicial (Home Screen):
62
Introducción al entorno Android
En el apartado “Uso del emulador de Android” de la Unidad 2 puedes encontrar una descripción más ampliada y detallada del AVD.
1.4
CÓMO CREAR LA PRIMERA APLICACIÓN CON ANDROID
A continuación, vamos a explicar cómo crear un proyecto sencillo usando Eclipse y las librerías Android. Vamos a partir del proyecto de ejemplo que hemos creado en el punto anterior. El primer proyecto Android consiste en una pantalla muy sencilla que muestra un mensaje de bienvenida. En la barra lateral Package Explorer de Eclipse, desplegamos las entradas haciendo clic en las flechas de los diferentes paquetes.
Si abrimos el fichero BienvenidoActivity.java, veremos el código fuente de la aplicación Android:
63
package es.mentor.unidad1.eje1.bienvenido; import android.app.Activity; import android.os.Bundle; import android.widget.TextView;
public class BienvenidoActivity extends Activity { /** Método que se llama cuando se crea una actividad. */ public void onCreate(Bundle savedInstanceState) { // Llamamos al método de la clase superior (Activity) super.onCreate(savedInstanceState); // Establecemos los contenidos de la Intefaz de usuario // de forma “programada”. TextView tv = new TextView(this); tv.setText("¡Bienvenido al curso de Android de Mentor!"); setContentView(tv); // Descomentar la siguiente sentencia para usar los layout en // el diseño de la Interfaz Usuario. Si lo haces, debes // comentar las 3 sentencias anteriores. // setContentView(R.layout.main); } }
Fíjate en que la clase principal BienvenidoActivity de la aplicación se basa en la clase Activity de Android. Una actividad (Activity) es el componente de la aplicación que realiza acciones. Una aplicación puede tener muchas actividades, si bien el usuario sólo interactúa con ellas de una en una. Android llama al método onCreate() cuando una actividad se inicia. En este método se lleva a cabo toda la inicialización de variables y configuración de la interfaz de usuario. Una actividad no está obligada a tener una interfaz de usuario, aunque generalmente la suele tener.
En la Unidad 2 veremos en detalle todos los métodos disponibles en esta clase básica de Android. 64
Introducción al entorno Android
La interfaz de usuario de Android se compone de vistas (Views). Una vista es un objeto que define el diseño de la interfaz de usuario. como un botón, una imagen, una etiqueta de texto, etcétera. Cada uno de estos objetos se hereda de la clase principal View. En este ejemplo hemos utilizado la subclase TextView, que crea una etiqueta de texto. En el ejemplo se crea una etiqueta TextView en el constructor de la Actividad. Para crear esta etiqueta es necesario pasar como parámetro una instancia del Contexto (Context) de la aplicación Android. Un Contexto es un identificador del sistema que sirve para tener acceso a recursos, a preferencias, a bases de datos, etcétera, de la aplicación. La clase Actividad se hereda de la clase Contexto; por lo tanto, se puede pasar esta Actividad como el Contexto de la aplicación escribiendo this. Con el método setText() establecemos el texto contenido en la etiqueta.
Para acabar, usamos el método setContentView() para indicar a la Actividad el contenido de la interfaz de usuario.
Si ejecutas la aplicación en Eclipse deberás ver la siguiente ventana en el emulador:
65
Nota: Al ejecutar varias veces una aplicación desde Eclipse puede ocurrir que aparezcan los siguientes mensajes de error en la consola:
Estos mensajes de error: [2011‐11‐20 09:18:15 ‐ unidad1.eje1.bienvenido] Application already deployed. No need to reinstall. [2011‐11‐20 09:18:15 ‐ unidad1.eje1.bienvenido] Starting activity es.mentor.unidad1.eje1.bienvenido.BienvenidoActivity on device emulator‐5554 [2011‐11‐20 09:18:16 ‐ unidad1.eje1.bienvenido] ActivityManager: Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=es.mentor.unidad1.eje1.bienvenido/.BienvenidoActivity} [2011‐11‐20 09:18:16 ‐ unidad1.eje1.bienvenido] ActivityManager: Warning: Activity not started, its current task has been brought to the front Indican únicamente que no se ha modificado el código fuente y que la aplicación se muestra de nuevo en el primer plano de la pantalla del dispositivo virtual.
1.4.1 Cambio en la Interfaz de usuario con Layout Los Layout son elementos no visibles que establecen cómo se distribuyen en la interfaz del usuario los componentes (widgets) que incluyamos en su interior. Podemos pensar en estos elementos como paneles donde vamos incorporando, de forma diseñada, los componentes con los que interacciona el usuario. Nota: La clase Layout se hereda, como el resto de componente, de la clase Vista. A lo largo del curso nos referimos a los componentes de Android como Vistas (Views) o como Widgets, tanto si son visibles (botones, texto, menús….) como si son elementos de diseño (layout).
En el ejemplo anterior hemos utilizado un diseño de interfaz de usuario "programado", es decir, se construye esta interfaz con sentencias Java en el código fuente. Si ya has desarrollado interfaces de esta manera, sabrás que pequeños cambios en su diseño pueden dar lugar a grandes modificaciones en el código fuente.
66
Introducción al entorno Android
Al ser Android un lenguaje nuevo, permite desarrollar interfaces usando archivos de diseño (Layout) XML. La forma más fácil de explicar este concepto es mostrar un ejemplo. El fichero res/layout/main.xml define el diseño de la interfaz del usuario:
La estructura general de un archivo de diseño de interfaz XML de Android es simple. Se trata de un árbol de elementos XML, donde cada nodo es el nombre de una clase Vista (en este ejemplo, usamos las clases LinearLayout y TextView). Puedes utilizar el nombre de cualquier clase de tipo Vista (View) de Android o, incluso, una clase Vista personalizada por el programador. El Layout LinearLayout apila secuencialmente todos sus elementos hijos de forma horizontal o vertical. En el apartado siguiente veremos diferentes tipos de paneles de diseño y sus características. Esta estructura XML hace que sea más fácil y rápido crear las interfaces de usuario. Este modelo se basa en el modelo de desarrollo web, donde se separa la presentación (interfaz de usuario) de la lógica de la aplicación (encargada de leer y escribir la información). En el ejemplo de XML anterior sólo hay un elemento Vista: TextView, que tiene tres atributos y un elemento de diseño Layout: LinearLayout, que tiene cuatro atributos. A continuación, mostramos una descripción de los atributos:
67
Atributo
Descripción
xmlns:android
Esta declaración indica que vamos a usar el espacio de nombres (la terminología) de Android para referirnos a los atributos que se definen a continuación.
android:id
Asigna un identificador único para el elemento correspondiente. Este identificador sirve para poder acceder al componente desde el código fuente o desde las declaraciones de otros elementos en este archivo XML.
Define el largo que debe ocupar la Vista. En este caso, indicamos que android:layout_width el TextView debe ocupar toda la pantalla con "fill_parent”.
android:layout_height
android:text
Similar al atributo anterior, en este caso, se refiere al ancho de la Vista.
Establece el texto que la Vista TextView debe mostrar. En este ejemplo se utiliza una cadena que se establece en el archivo res/values/strings.xml.
El título de la aplicación “Unidad1 - Ejemplo 1: Bienvenido” y la frase "¡Bienvenido al curso de Android de Mentor!", que aparecen en el área del usuario, se definen en el fichero res/values/strings.xml. El SDK de Android permite definir los ficheros de tipo XML de dos formas: a través de un editor visual o directamente en el archivo XML. Se puede cambiar entre las dos formas haciendo clic en la pestaña de la parte inferior de la ventana. Por ejemplo, en el Package Explorer, seleccionamos res/layout/main.xml y hacemos clic en “Graphical Layout”:
68
Introducción al entorno Android
En esta ventana podemos diseñar visualmente la pantalla de la aplicación Android arrastrando con el ratón los componentes que aparecen en el apartador “Palette”. Si en el Package Explorer seleccionamos res/layout/strings.xml y hacemos clic en “Resources”:
Usando en esta ventana el botón “Add” podemos añadir visualmente los diferentes tipos de recursos de Android. Para crear la primera aplicación hemos usado componentes (Widgets o Vistas) usuales de Android. En el siguiente apartado de teoría explicaremos en detalle el tipo de componentes disponibles por defecto y cómo usarlos para diseñar las pantallas que servirán de interfaz gráfica al usuario. Fichero AndroidManifest.xml: contiene la definición en formato XML de las características principales de la aplicación, como su identificación (nombre, versión, icono, etcétera), sus componentes (Actividades, Mensajes, Servicios, etcétera) o los permisos necesarios para su ejecución. Más adelante veremos otros detalles de este fichero. En este fichero hay que declarar la Actividad para que Android tenga acceso a la misma. Si abres el archive manifest, verás que existe el siguiente elemento :
69
...
En este fichero se pueden definir varios atributos para establecer la etiqueta de la actividad, su icono o el tema de estilo de la interfaz de usuario. El único atributo obligatorio es android:name, que especifica el nombre de clase de la actividad. Es importante usar siempre el mismo nombre de Actividad ya que otra aplicación puede iniciarla. Importante: Aunque el código fuente de este ejemplo se encuentra disponible en la carpeta de ejemplos de esta unidad, es fundamental que crees este proyecto Android desde el principio para entender la secuencia de pasos dados y los ficheros necesarios. Además, si no has usado nunca el entorno de desarrollo Eclipse - Android, adquirirás soltura utilizándolo.
1.5
DISEÑO DE LA INTERFAZ DE USUARIO
1.5.1 Cómo diseñar la interfaz de usuario mediante Vistas Una de las características más importante de Android es la posibilidad de usar componentes gráficos dinámicos y reutilizables (en inglés se denominan Views). Mediante el SDK de Android, el programador puede utilizar clases prediseñadas para implementar elementos y comportamientos en la interfaz del usuario, que, de otra forma, éste tendría que crear desde cero, tales como botones, cuadros de edición complejos, arrastrar y soltar, o menús en árbol. Nota: Como en la literatura inglesa de Android se habla genéricamente de Vista (View) para referirse a estos componentes visuales y en Internet siempre aparecen referencias a esta palabra, vamos a usar esta nomenclatura a partir de ahora. Es importante no confundir los widgets (componentes o Vistas) que usamos al desarrollar las interfaces de usuario en Android con “Widget de la pantalla principal” (Screen Home), que son pequeñas aplicaciones que el usuario del teléfono puede añadir a esta pantalla, tales como un calendario dinámico, previsión meteorológica, etcétera.
70
Introducción al entorno Android
1.5.2 Vistas disponibles de Android Construir interfaces de usuario en las aplicaciones de Android es muy sencillo y rápido gracias a que podemos utilizar Vistas. Como hemos visto en el apartado anterior, las Vistas visibles deben situarse dentro de otro tipo de Vista denominada Layout (Panel de diseño). Estos paneles de diseño (Layout) de Android se usan para diseñar la interfaz gráfica del usuario de la aplicación. Estos paneles se usan para separar simbólicamente el área de la aplicación. Dentro de estos paneles se incluye la mayoría de las Vistas, como botones, cuadros de texto, etcétera. Además, dentro de un panel se pueden incluir otros paneles para hacer diseños complejos. Nota: Cuando se describan los métodos más importantes de cada Vista (View), sólo se incluirán aquéllas que no se hayan señalado con anterioridad o se invoquen con diferentes argumentos. Además, el entorno de Eclipse dispone de una ventana emergente de ayuda que, al escribir código fuente, muestra los diferentes métodos disponibles para esa clase. De esta forma, evitamos errores de codificación y el desarrollo de las aplicaciones web es mucho más rápido y eficiente. En la siguiente imagen mostramos el aspecto que tiene esta ventana de ayuda. Con el atajo de teclado [CTRL+BARRA_ESPACIADORA] podemos acceder a esta ventana de ayuda emergente.
71
Tipos de paneles (Layout)
Panel Marco (FrameLayout)
Éste es el panel más sencillo de todos los Layouts de Android. Un panel FrameLayout coloca todos sus componentes hijos alineados pegados a su esquina superior izquierda de forma que cada componente nuevo añadido oculta por el componente anterior. Por esto, se suele utilizar para mostrar un único control en su interior, a modo de contenedor (placeholder) sencillo para un único elemento, por ejemplo, una imagen. Los componentes incluidos en un FrameLayout pueden establecer las propiedades android:layout_width y android:layout_height, que pueden establecerse con los valores:
fill_parent para que el componente hijo tenga la dimensión del layout que lo contiene.
wrap_content para que el componente hijo ocupe el tamaño de su contenido.
Ejemplo
72
Introducción al entorno Android
Panel Lineal (LinearLayout) Vertical y Horizontal
El panel LinearLayout apila todos sus componentes hijos de forma horizontal o vertical, según se establezca la propiedad android:orientation
con el valor “vertical” u
“horizontal”. De igual forma que en un FrameLayout, se pueden establecer las propiedades android:layout_width
y
android:layout_height.
Además,
existe
la
propiedad
android:layout_weight, en el caso de un panel LinearLayout, que permite establecer las dimensiones de los componentes contenidos proporcionales entre ellos. Ejemplo
Si incluimos en un panel vertical dos cuadros de texto (EditText) y en uno de ellos establecemos un layout_weight=”1 y en el otro un layout_weight=”2 , conseguiremos que toda la superficie del panel esté ocupada por los dos cuadros de texto y, además, que el segundo sea el doble (relación entre sus propiedades weight) de alto que el primero.
73
Panel Tabla (TableLayout)
El panel TableLayout permite distribuir todos sus componentes hijos como si se tratara de una tabla mediante filas y columnas. La estructura de la tabla se define de manera similar a una tabla en formato HTML, es decir, indicando las filas que compondrán la tabla (objetos TableRow) y las columnas de cada una de ellas. Por norma general, el ancho de cada columna corresponde al ancho del mayor componente de dicha columna, pero existen una serie de propiedades pueden modificar este comportamiento:
android:stretchColumns: indica el número de columna que se expande para ocupar el espacio libre que dejan el resto de columnas a la derecha de la pantalla.
android:shrinkColumns: indica las columnas que se pueden contraer para dejar espacio al resto de columnas de lado derecho de la pantalla.
android:collapseColumns: indica las columnas de la tabla que se pueden ocultar completamente. Todas estas propiedades del TableLayout pueden establecerse con una lista de
índices
de
las
columnas
separados
por
comas,
por
ejemplo:
android:stretchColumns=”1,2,3 o un asterisco para indicar que se debe aplicar a todas las columnas, de esta forma: android:stretchColumns=”*”.
android:layout_span: una celda determinada puede ocupar el espacio de varias columnas de la tabla (análogo al atributo colspan de HTML) del componente concreto que ocupa dicho espacio.
Ejemplo
74
Introducción al entorno Android
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Panel tabla) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el emulador el resultado del programa anterior, en el que hemos utilizado el Layout TableLayout.
Panel Relativo (RelativeLayout)
El panel RelativeLayout permite especificar la posición de cada componente de forma relativa a su elemento padre o a cualquier otro elemento incluido en el propio layout. Así, al incluir un nuevo componente X podemos indicar, por ejemplo, que debe situarse debajo del componente Y y alineado a la derecha del layout padre (el que lo contiene). Un panel RelativeLayout dispone de múltiples propiedades para colocar cada componente. Las principales son: Posición relativa a otro control:
android:layout_above: arriba. android:layout_below: debajo. android:layout_toLeftOf: a la izquierda de. android:layout_toRightOf: a la derecha de. android:layout_alignLeft: alinear a la izquierda. android:layout_alignRight: alinear a la derecha. android:layout_alignTop: alinear arriba. android:layout_alignBottom: alinear abajo. android:layout_alignBaseline: alinear en la base.
Posición relativa al layout padre:
android:layout_alignParentLeft: alinear a la izquierda. android:layout_alignParentRight: alinear a la derecha. android:layout_alignParentTop: alinear arriba. android:layout_alignParentBottom: alinear abajo. android:layout_centerHorizontal: alinear horizontalmente al 75
centro.
android:layout_centerVertical: alinear verticalmente al centro. android:layout_centerInParent: centrar.
Opciones de margen (también disponibles en el resto de layouts):
android:layout_margin: establece el margen. android:layout_marginBottom: establece el margen inferior. android:layout_marginTop: establece el margen superior. android:layout_marginLeft: establece el margen izquierdo. android:layout_marginRight: establece el margen derecho.
Opciones de espaciado o padding (también disponibles en el resto de layouts):
android:padding: establece la separación entre componentes. android:paddingBottom: establece la separación inferior. android:paddingTop: establece la separación superior. android:paddingLeft: establece la separación izquierda. android:paddingRight: establece la separación derecha.
Ejemplo
En este ejemplo, el botón BtnAceptar se coloca debajo del cuadro de texto TextoNombre (android:layout_below=”@id/TxtNombre”) y alineado a la derecha del layout padre (android:layout_alignParentRight=”true”); además, se establece un margen a su izquierda de 10 pixeles (android:layout_marginLeft=”10px”).
76
Introducción al entorno Android
Panel Marco (FrameLayout
El panel FrameLayout permite superponer en el área de la pantalla varios componentes hijos. Por lo general, este panel debe contener pocos componentes hijos, ya que puede ser difícil organizarlos sin que se superpongan unos con otros en los diferentes tamaños de pantalla de los teléfonos. El atributo más importante de los componentes hijos que contiene es layout_gravity, que permite controlar su posición relativa. Ejemplo
En este ejemplo, mostramos una imagen que ocupa todo el área de la pantalla usando el atributo android:scaleType="center" y una etiqueta superpuesta en la parte inferior de la pantalla con el atributo android:layout_gravity="center_horizontal|bottom".
77
Componentes Básicos Como hemos comentado anteriormente, los interfaces de las aplicaciones de usuario en Android se construyen usando componentes o Vistas que están contenidas en paneles de diseño (layout). Los componentes permiten al usuario interaccionar con la aplicación. Los paneles ordenan la posición de estos elementos en la interfaz del usuario. A continuación, vamos a mostrar los componentes básicos. Botones
Los botones se usan para que el usuario interactúe con la aplicación Web: El SDK de Android proporciona tres tipos de botones: el botón clásico (Button), el de tipo on/off (ToggleButton) y el que puede albergar una imagen (ImageButton).
El componente Button es el botón básico de Android. En el ejemplo siguiente
definimos un botón con el texto “Haz clic aquí” asignando la propiedad android:text.
android:id="@+id/boton" android:layout_height="wrap_content" android:layout_width="135dp">
Además,
podemos
utilizar
otras
propiedades,
como
el
color
de
fondo
(android:background), el estilo de la fuente (android:typeface), el color de fuente (android:textcolor), el tamaño de la fuente (android:textSize), etcétera.
El componente ToggleButton es un tipo de botón que puede encontrarse en dos
estados: pulsado (ON) o no_pulsado (OFF). En este caso, en lugar de definir un único texto, podemos establecer dos en función del estado asignando las propiedades android:textOn
y
android:textoOff,
respectivamente.
Veamos
un
continuación:
78
ejemplo
a
Introducción al entorno Android
El componente ImageButton es un botón que muestra una imagen en lugar de un
texto asignando la propiedad android:src. Normalmente, indicamos esta propiedad usando el descriptor de alguna imagen que hayamos copiado en la carpeta /res/drawable. Así, por ejemplo, en nuestro caso hemos incluido la imagen “stop.png”, a la que hacemos referencia en “@drawable/ok“.
Los botones disponen de eventos que se puede capturar. El más común es el evento onClick. En la Unidad 2 veremos qué son los Eventos y los Listerners, si bien en este apartado se incluyen algunos ejemplos para que la teoría sea consistente. Para
definir
la
lógica
de
este
evento
hay
que
definir
un
nuevo
objeto
View.OnClickListener() y asociarlo al botón mediante el método setOnClickListener(). La forma de hacerlo es la siguiente:
final Button btnBoton1 = (Button)findViewById(R.id.boton); btnBoton1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { lblEtiqueta.setText("¡Has pulsado el Botón!"); } });
En el caso del botón de tipo ToggleButton suele ser más útil conocer el estado en el que está el botón tras ser pulsado. Para esto, se usa el método isChecked(). En el siguiente ejemplo se comprueba el estado del botón después de ser pulsado y se realizan diferentes acciones según su resultado: 79
final ToggleButton btnBoton2 = (ToggleButton)findViewById(R.id.toggleButton); btnBoton2.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View arg0) { if(btnBoton2.isChecked()) lblEtiqueta.setText("Botón Encendido"); else lblEtiqueta.setText("Botón Apagado"); } });
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el emulador el resultado del programa anterior.
Etiqueta (TextView)
La etiqueta (o TextView en inglés) permite mostrar un determinado texto al usuario. El texto se establece mediante la propiedad android:text. Además de esta propiedad, se puede cambiar el formato del texto usando las siguientes propiedades: android:background (color de fondo), android:textColor (color del texto), android:textSize (tamaño de la fuente) y android:typeface (estilo del texto: negrita, cursiva). Fíjate en el código del siguiente ejemplo:
android:layout_height="wrap_content" />
80
Introducción al entorno Android
android:layout_height="wrap_content" android:textSize="25dp" android:typeface="serif" />
android:layout_height="wrap_content" android:textStyle="bold" />
android:layout_height="wrap_content" android:textSize="40dp" android:textColor="#FF0000" android:textStyle="italic" />
Además, podemos modificar estas propiedades desde nuestro código Java usando los métodos getText() para recuperar el texto de una etiqueta, setText() para actualizar el texto y setBackgroundColor() para cambiar el color de fondo. Por ejemplo, así: // Buscamos la etiqueta con el id texto1 final TextView lblEtiqueta = (TextView)findViewById(R.id.texto1); String texto = lblEtiqueta.getText().toString(); texto += " abc"; lblEtiqueta.setText(texto);
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente TextWiew
Imagen (ImageView)
La imagen (o ImageView en inglés), como su propio nombre indica, permite mostrar imágenes en la aplicación. La propiedad más útil es android:src y permite establecer la imagen que se muestra. De nuevo, lo usual es indicar como origen de la imagen el identificador de un recurso de la carpeta /res/drawable. Además de esta propiedad, existen 81
otras, como las destinadas a establecer el tamaño máximo que puede ocupar la imagen: android:maxWidth y android:maxHeight. Fíjate en el código del siguiente ejemplo:
En la lógica de la aplicación, podemos establecer la imagen mediante el método setImageResorce(): ImageView img= (ImageView)findViewById(R.id.ImgFoto); img.setImageResource(R.drawable.icon);
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente ImageView.
Cuadro de Texto (Text Edit)
El Cuadro de Texto (o EditText en inglés) es el componente de edición de texto de Android que permite la introducción y edición de texto al usuario. Su propiedad más importante es android:text, que establece el texto que contiene. Fíjate en el código del siguiente ejemplo:
android:layout_width="match_parent" android:layout_height="wrap_content">
También es posible recuperar y establecer este texto mediante los métodos getText() y setText(nuevoTexto) respectivamente:
final EditText txtTexto = (EditText)findViewById(R.id.editTexto);
82
Introducción al entorno Android
texto = txtTexto.getText().toString(); txtTexto.setText("Esto es un texto");
En el código fuente anterior hemos hecho un cambio de formato usando el método toString() sobre el resultado de getText(). El método getText() no devuelve una cadena (String), sino un objeto de tipo Editable (tipo Spanned, algo así como una cadena de caracteres en la que podemos insertar etiquetas) Es decir, el componente EditText permite editar texto plano y texto enriquecido o con formato; por eso hemos tenido que usar un método para cambiar la cadena perdiendo el formato enriquecido. Para poder obtener el texto con el formato correspondiente, podemos usar la clase Html de Android, que dispone de los métodos para convertir cualquier objeto de tipo Spanned en su representación HTML equivalente. Veamos cómo funciona: //Obtiene el texto del componente con etiquetas de formato HTML String aux2 = Html.toHtml(txtTexto.getText());
La sentencia anterior devolvería una cadena de texto como ésta “Esto es una prueba .
”.
También es posible realizar la operación opuesta, es decir, establecer en un cuadro de texto (EditText) o en una etiqueta (TextView) un texto en formato HTML. Para ello, se utiliza el método Html.fromHtml(String) así: //Asigna el texto con formato HTML txtTexto.setText( Html.fromHtml("Esto es una prueba .
"), BufferType.SPANNABLE);
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente EditText.
83
Cuadro de Selección (CheckBox)
La caja de selección (o CheckBox en inglés) permite al usuario marcar o desmarcar opciones en una aplicación. La forma de definirlo en la interfaz y los métodos disponibles para manipularlos son análogos a los ya comentados para el componente ToggleButton. Fíjate en el código del siguiente ejemplo:
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:text="Confirmar Selección"
android:layout_gravity="center_vertical" />
Respecto a la personalización de estilo del componente, podemos emplear casi todas las opciones del componente TextView comentadas anteriormente. En el código de la aplicación podemos utilizar los métodos isChecked() para conocer el estado del componente y setChecked(boolean) para establecer un estado en concreto. En cuanto a los posibles eventos interesantes que puede lanzar este componente, el más interesante es onCheckedChanged que notifica que la selección ha cambiado. Por ejemplo, así: CheckBox.OnCheckedChangeListener CBCambioListener = new CheckBox.OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView,
boolean isChecked)
84
Introducción al entorno Android
{ if (isChecked) { txtTexto.setText("¡Checkbox "+ buttonView.getText() +
" marcado!"); } else { txtTexto.setText("¡Checkbox "+ buttonView.getText() +
" desmarcado!"); } }
};
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente Checkbox.
Botón de radio (RadioButton)
El botón de radio (o RadioButton en inglés) permite elegir una única opción de un grupo de opciones, es decir, si se marca una de ellas se desmarcará automáticamente la anterior. En Android, los botones RadioButton se agrupan dentro de un elemento RadioGroup. Veamos un ejemplo de cómo definir un grupo de botones RadioButton en la interfaz:
android:layout_height="fill_parent">
85
android:text="Opción 1" />
En primer lugar, hemos establecido la orientación (vertical u horizontal) como hicimos con el componente LinearLayout. Después, hemos añadido todos los componentes RadioButton necesarios, indicando su ID mediante la propiedad android:id y su texto mediante la propiedad android:text.
Una vez definida la interfaz, podemos manipular los componentes desde el código java haciendo uso de los diferentes métodos del componente RadioGroup como, por ejemplo:
check(id): selecciona una opción determinada mediante su ID.
clearCheck():desmarca la opción seleccionada.
getCheckedRadioButtonId():devuelve el ID de la opción seleccionada o -1 si no hay ninguna marcada.
En cuanto a los eventos iniciados por este componente, como los CheckBox, el más útil es el que informa de los cambios en el elemento seleccionado onCheckedChange. Fíjate en el siguiente ejemplo:
// Definimos el evento OnCheckedChange final RadioGroup rg = (RadioGroup)findViewById(R.id.gruporb); rg.setOnCheckedChangeListener( new RadioGroup.OnCheckedChangeListener() { public void onCheckedChanged(RadioGroup grupo, int checkedId) {
// Obtenemos el RadioButton que está seleccionado usando // el ID marcado checkedId final RadioButton rb = (RadioButton) findViewById(checkedId);
86
Introducción al entorno Android
txtTexto.setText("RadioButton seleccionado: " + rb.getText()); } });
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Componentes básicos) de la Unidad 1. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente
Si ejecutas en Eclipse este Ejemplo 3, verás que se muestra la siguiente aplicación en el Emulador:
87
Android es un sistema operativo, inicialmente diseñado para teléfonos móviles con sistemas operativos iOS (Apple), Symbian (Nokia) y Blackberry OS. Android, basado en Linux, es un sistema operativo libre y gratuito. Los componentes principales de la arquitectura del sistema operativo Android son las Aplicaciones, el Marco de desarrollo (SDK), las Librerías de Android, el Runtime (ejecutable) y el Núcleo de Linux. Todas las aplicaciones de Android usan el siguiente conjunto de servicios y sistemas: o Un conjunto de componentes (Views) o Proveedores de contenidos (Content Providers) o Gestor de recursos (Resource Manager) o Gestor de notificaciones (Notification Manager) o Gestor de actividades (Activity Manager) Android usa Java como lenguaje base para el desarrollo de las aplicaciones; por lo tanto, emplea Paquetes Java (Package en inglés). Estos paquetes son contenedores de clases que permiten agrupar las distintas partes de un programa cuya funcionalidad tienen elementos comunes. El nombre de los paquetes debe ser único en relación con los paquetes instalados en Android. Por esta razón, es importante utilizar el estándar de dominio “com.dominio…” para nombrarlos. Los ficheros que contengan el código fuente de las actividades del alumno han de guardarse
en
una
carpeta
personal.
Recomendamos
usar
el
directorio
C:\cursos_Mentor\Android\proyectos para este fin. Es muy importante conocer la estructura de directorios y el nombre de los ficheros que componen un proyecto Android.
88
Introducción al entorno Android
Para comprobar que una aplicación funciona, hay que hacer clic en la opción "Run" de menú "Run" de Eclipse (Atajo del teclado [Ctrl+F11]); después, arrancará el Emulador (AVD) de Android para ver el resultado de su ejecución. Un Dispositivo Virtual de Android (en inglés, AVD, Android Virtual Device) emula un terminal instalado con Android en el que podemos probar las aplicaciones desarrolladas. Todas las sentencias de Android (Java) deben acabar con ;. Las sentencias o instrucciones compuestas contienen varias sentencias simples y deben estar incluidas entre los signos { y }. Generalmente, una sentencia compuesta está integrada por sentencias simples de un bucle o de la declaración de una función que deben ejecutarse como un bloque. Para poder seguir mejor el flujo de un programa y ver más intuitivamente su código, conviene indentar (adentrar unos espacios) las sentencias que están incluidas dentro de una estructura. En Eclipse podemos usar el atajo de teclado [CTRL+I] para hacerlo automáticamente. Los comentarios ayudan mucho a comprender un programa. Los que sólo ocupan una línea deben ir precedidos de los signos //. Si el texto ocupa más de una línea, hay que incluirlo entre los signos /* y */. Android dispone de todas las variables, funciones, expresiones y operadores más usuales de Java. Una de las características más importante de Android es la posibilidad de usar componentes gráficos dinámicos y reutilizables (en inglés se denominan Views). Los paneles en Android se usan para diseñar (en inglés se denomina layout) la interfaz gráfica del usuario de la aplicación. El entorno de Eclipse ayuda al programador mostrando una ventana emergente de ayuda al escribir el código fuente. En ella se proponen los diferentes métodos disponibles
para
esa
clase.
Disponemos
también
del
atajo
de
teclado
[CTRL+BARRA_ESPACIADORA].
89
Las Vistas visibles están contenidos en los paneles y permiten interaccionar al usuario con la aplicación. Android define detectores de eventos (Listeners) que, asociados a un componente, permiten controlar la interacción del usuario sobre éste: clic del ratón, escribir en el teclado, etcétera. En la Unidad 2 se tratan en detalle estos detectores. En Android como en cualquier lenguaje las expresiones constituyen uno de los asuntos más importantes de la programación, pues intervienen en todas las sentencias y están integradas por todos los elementos de un lenguaje informático.
90
DISEÑO DEL INTERFAZ DE USUARIO
ÍNDICE 2.1 ACTIVIDADES - ANDROID .................................................. 93 2.1.1 Introducción..............................................................................93 2.1.2 Creación de una actividad......................................................93 2.1.3 Ciclo de vida de una actividad ..............................................94 2.1.4 Cómo se implementa el ciclo de vida de una actividad ....94 2.2 EVENTOS Y LISTENERS ...................................................... 100 2.2.1 Gestionando los eventos del usuario ............................. 100 2.2.2 Uso de los Event Listeners ............................................. 100 2.2.3 Gestores de Eventos (Event Handlers)........................... 104 2.2.4 Modo táctil de pantalla ................................................... 105 2.2.5 Controlando la Vista con el foco activo.......................... 106 2.3 USO DEL EMULADOR DE ANDROID ................................. 106 2.3.1 Teclado del emulador ..................................................... 108 2.3.2 Cómo introducir tildes con el Teclado del Emulador ..... 110 2.3.3 Limitaciones del Emulador ............................................. 111 2.3.4 Tamaño ventana emulador ............................................. 111 2.3.5 Otras opciones del Emulador ......................................... 112 2.3.6 Cómo configurar las opciones del Emulador ................. 113 2.4 COMPONENTES AVANZADOS ........................................... 114 2.4.1 Qué son los Adaptadores de Android (adapters) ........... 114 2.5 COMPONENTES PERSONALIZADOS................................ 127 2.5.1 Diseño de componentes personalizados ....................... 127 2.5.2 Cómo crear un componente extendido ......................... 127 2.5.3 Cómo combinar varios componentes para crear uno compuesto ........................................................................... 130
2
Diseño del interfaz de usuario
2.1
ACTIVIDADES - ANDROID
2.1.1 Introducción Una Actividad (Activity) es un componente de Android que ofrece una pantalla con la que los usuarios pueden interactuar con la aplicación, como marcar el teléfono, sacar una foto, enviar un correo electrónico o ver un mapa. Cada Actividad tiene asociada una ventana en la que se dibuja la interfaz de usuario. Normalmente, esta ventana ocupa toda la pantalla, aunque puede ser menor que ésta o flotar sobre otras ventanas. Por lo general, una aplicación de Android consta de múltiples actividades que están más o menos ligadas entre sí. Habitualmente, se define una actividad "principal", que es la que se presenta al usuario cuando se inicia la aplicación por primera vez. Una actividad puede iniciar otra actividad con el fin de realizar diferentes operaciones. Cada vez que comienza una nueva actividad, la actividad anterior se detiene y la envía a una pila de retroceso ("back stack"). Esta pila usa el mecanismo de cola LIFO ("last in, first out"), por lo que, cuando el usuario pulsa la tecla “Volver atrás” del móvil, se extrae de la pila la actividad anterior (destruyéndose la pila) y se reanuda. En la Unidad 3 veremos en detalle cómo funciona esta pila. Cuando una actividad se para porque se inicia una nueva actividad, se le notifica este cambio de estado a través de los métodos de llamada callback
del ciclo de vida de la
actividad. Una función de llamada (en inglés callback) es una función que se remite a Android cuando se inicia una Actividad, para que el sistema operativo la “llame” durante la ejecución de esta Actividad. Existen varios métodos de llamada callback que una actividad puede recibir debido a un cambio en su estado; por ejemplo, cuando el sistema crea la Actividad, cuando se reactiva o cuando se destruye. El programador puede aprovechar estos métodos para ejecutar sentencias específicas apropiadas para el cambio de estado. Por ejemplo, cuando una Actividad se suspende es recomendable liberar de la memoria todos los objetos grandes. Cuando la actividad se reanuda, se puede volver a reservar los recursos necesarios y continuar con las acciones que se interrumpieron. Estos cambios de estado forman parte del ciclo de vida de la actividad.
2.1.2 Creación de una actividad Para crear una Actividad debemos usar la clase Activity de Android. En la subclase creada es necesario implementar los métodos callback que el sistema puede invocar cuando hay un cambio en su ciclo de vida: la actividad se está creando, se detiene, se reanuda o se destruye. Los dos métodos callback más importantes son: 93
onCreate(): es obligatorio implementar este método ya que el sistema lo invoca cuando crea su actividad. Dentro de su implementación debemos iniciar los componentes esenciales de la actividad, como dibujar la interfaz de usuario empleado la función setContentView(). onPause(): el sistema llama a este método cuando el usuario detiene la actividad, aunque no significa que la actividad se destruya. Aquí es donde el programador debe guardar todos los cambios que deben persistir en la siguiente sesión del usuario, ya que éste podría no volver a la Actividad y que ésta se destruyera. Es posible utilizar otros métodos de tipo callback para proporcionar una experiencia de usuario fluida entre actividades y manejar el paso de una a otra. Lo veremos más adelante en este apartado.
2.1.3 Ciclo de vida de una actividad Una Actividad puede mantenerse en tres estados: Resumed: la actividad está en el primer plano de la pantalla y el usuario la está utilizando. Este estado también se denomina "running". Paused: la actividad es visible, pero hay otra actividad en primer plano que tiene el foco. La actividad pausada sigue “viva” ya que se mantiene en memoria y conserva toda la información de estado, si bien el sistema operativo puede eliminarla en caso de memoria disponible muy baja. Stopped: la actividad se oculta completamente por una nueva actividad (la actividad anterior se ejecuta en "background"). Una actividad detenida también se mantiene en memoria con toda la información de estado. Sin embargo, el usuario ya no la ve visible y el sistema operativo puede eliminarla cuando se necesita memoria para otra tarea. Si una actividad está “pausada” o detenida, el sistema puede eliminarla de la memoria invocando el método finish() de la Actividad o, simplemente, puede acabar (“kill”) con el proceso. Si se quiere abrir de nuevo esta actividad, después de haber finalizado, debe ser creada otra vez desde el principio
2.1.4 Cómo se implementa el ciclo de vida de una actividad Cuando una actividad cambia entre los diferentes estados descritos anteriormente, el sistema operativo le notifica el cambio mediante diferentes métodos callback. El programador puede usar todos estos métodos callback para ejecutar las órdenes apropiadas. El ejemplo siguiente incluye la estructura de cada uno de estos métodos fundamentales del ciclo de vida de una Actividad: 94
Diseño del interfaz de usuario
// Definimos el evento callback onCreate de la Actividad @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Usamos la clase Toast que muestra durante 1 segundo un // mensaje pequeño al usuario Toast.makeText(this, "Se ejecuta el método onCreate", 1).show(); } // Definimos el evento callback onPause de la Actividad @Override protected void onPause() { super.onPause(); // Aquí deberíamos guardar la información para la siguiente sesión Toast.makeText(this, "Se ejecuta el método onPause", 1).show(); } // Definimos el evento callback onRestart de la Actividad
@Override protected void onRestart() { super.onRestart(); Toast.makeText(this, "Se ejecuta el método onRestart", 1).show(); } // Definimos el evento callback onResume de la Actividad @Override protected void onResume() { super.onResume(); Toast.makeText(this, "Se ejecuta el método onResume", 1).show(); } // Definimos el evento callback onStart de la Actividad @Override protected void onStart() { super.onStart();
95
// Aquí deberíamos leer los datos de la última sesión para seguir la // aplicación donde la dejó el usuario Toast.makeText(this, "Se ejecuta el método onStart", 1).show();
} // Definimos el evento callback onDestroy de la Actividad @Override protected void onDestroy() { super.onDestroy(); Toast.makeText(this, "Se ejecuta el método onDestroy", 1).show(); } // Definimos el evento callback onStop de la Actividad @Override protected void onStop() { super.onStop(); Toast.makeText(this, "Se ejecuta el método onStop", 1).show(); }
IMPORTANTE: la implementación de estos métodos siempre debe incluir la llamada al método de la clase superior (superclase) antes de ejecutar cualquier otra sentencia, de la forma siguiente: super.onStop();.
A continuación, vamos a ver el ciclo de vida de una Actividad siguiendo sus estados y los métodos callback que desencadena cada uno de ellos: El ciclo de vida de una Actividad ocurre entre las llamadas a los métodos onCreate() y OnDestroy(). En el primer método la Actividad debe realizar la reserva de memoria, el diseño de la interfaz de usuario y recuperar el estado de la sesión anterior. En el segundo método, hay que liberar todos los recursos usados con anterioridad. El ciclo de vida "visible" de una Actividad ocurre entre las llamadas a los métodos OnStart() y OnStop(). Durante este tiempo el usuario puede ver e interactuar con la pantalla de la Actividad. El sistema invoca el método OnStop()cuando se inicia una nueva actividad y la actual ya no está visible al usuario. Entre estos dos métodos, el programador debe definir y destruir respectivamente los recursos necesarios para mostrar la Actividad para el usuario.
96
Diseño del interfaz de usuario El ciclo de vida "en ejecución" de una Actividad sucede entre las llamadas a los métodos OnResume() y OnPause(). Durante este tiempo la Actividad se ejecuta en primer plano y tiene el foco del usuario. A menudo, el sistema operativo invoca estos métodos, por ejemplo, cuando el teléfono se queda en modo espera o cuando aparece una ventana con un mensaje de la aplicación. Por esto, es conveniente incluir en estos métodos pocas y sencillas sentencias para que la aplicación no se pare cuando el usuario intenta hacer operaciones con ella. A continuación, se muestra un esquema visual de los cambios de estado posibles dentro del ciclo de vida de una Actividad. Los rectángulos representan los métodos callback que el sistema operativo puede invocar en las transiciones entre los estados de la Actividad.
97
Resumen de los métodos callback del ciclo de vida de una Actividad
Método
Descripción
Kill
Siguiente método
onCreate()
Se invoca cuando la Actividad se crea por primera vez. Aquí es donde se reserva la memoria necesaria, se crea la interfaz de usuario, se recupera el estado de la sesión anterior, etcétera.
No
onStart()
onRestart()
Se invoca cuando la Actividad está parada, justo antes de iniciarse de nuevo.
No
onStart()
onResume() o onStop()
Se invoca justo antes de que el usuario pueda ver la Actividad. onStart()
A continuación, se puede invocar el método onResume() si la Actividad vuelve al primer plano o onStop() si se oculta.
No
onResume()
Se invoca justo antes de que el usuario comience a interactuar con la Actividad.
No
onPause()
Sí
onResume() o onStop()
onPause()
Se invoca cuando el sistema está a punto de comenzar otra Actividad. Es recomendable usar este método para confirmar con el usuario si quiere guardar los cambios, desactivar animaciones y cualquier código que consuma CPU, etcétera. Estas sentencias deben ser muy rápidas porque la nueva Actividad no se inicia hasta que finaliza este método. A continuación, se puede invocar el método onResume() si la Actividad vuelve al primer plano o onStop() si se oculta.
98
Diseño del interfaz de usuario
Método
Descripción
onStop()
Se invoca cuando la actividad ya no es visible al usuario. Esto puede ocurrir porque se vaya a destruir la Actividad o porque otra Actividad (existente o nueva) se ha reanudado y vuelve al primer plano.
Kill
Siguiente método
Sí
onRestart() o onDestroy()
Sí
ninguno
A continuación, se puede invocar el método onRestart() si la Actividad vuelve al primer plano o onDestroy() si se destruye.
Se invoca antes de que se destruya una Actividad; se trata, pues, del último método.
onDestroy()
El sistema operativo puede invocar este método porque el usuario decide finalizar la aplicación (método finish())o porque es necesaria memoria libre. Se puede distinguir entre estos dos escenarios con el método isFinishing().
La columna "Kill" de esta tabla indica si el sistema puede matar (kill) el proceso de la Actividad cuando finaliza la ejecución del método sin ejecutar ninguna sentencia más de la Actividad. Hay tres métodos así: onPause(), onStop() y onDestroy(). onPause() es el método que se ejecuta siempre en caso de que el sistema operativo mate una Actividad. Sin embargo, no es posible asegurar que el sistema invoque los métodos OnStop() y OnDestroy() porque se haya ejecutado onPause() y la Actividad haya acabado. Por lo tanto, se debe utilizar el método onPause() para guardar los datos importantes y persistentes de la Actividad. No obstante, hay que ser selectivo sobre qué información debe guardarse durante onPause(), ya que este método bloquea el inicio de una nueva Actividad y el usuario podría notar que el teléfono se enlentece. Los métodos que se han marcado con "No" en la columna "Kill" están, a priori, protegidos. El sistema operativo sólo los "mata" en una situación de inestabilidad con falta de memoria. En la Unidad 3 veremos cómo usar esos eventos para guardar el estado de una Actividad y poder recuperarlo cuando ésta se reinicia.
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Actividades) de la Unidad 2. Estudia el 99
código fuente y ejecútalo para mostrar en el emulador una aplicación en la que hemos utilizado los métodos del ciclo de vida de una Actividad.
2.2
EVENTOS Y LISTENERS
2.2.1 Gestionando los eventos del usuario Hasta ahora, en el curso, hemos creado elementos de la interfaz de usuario utilizando los componentes disponibles en Android. En este apartado vamos a explicar cómo se gestiona la interacción del usuario sobre la interfaz de la aplicación. Como muchos otros entornos de desarrollo, Android está basado en Controladores de Eventos (Event Handlers). Es decir, se ejecuta un determinado código en respuesta a algún evento que ocurra. Normalmente, estos eventos se activan cuando el usuario interactúa con la interfaz de la aplicación. En Android el programador puede capturar los eventos específicos del objeto Vista (View) con la que el usuario interactúa y ejecutar sentencias. Por ejemplo, cuando el usuario toca con el dedo una Vista (por ejemplo, un botón), el sistema operativo invoca el método onTouchEvent() de ese objeto. Para interceptar este método deberíamos extender (crear una nueva clase heredada del botón) la clase y reemplazar el código del método de la clase original. Sin embargo, extender cada objeto Vista para gestionar un evento no es práctico. Por esta razón, Android dispone en todas las clases View de una colección de interfaces con funciones callback que se pueden utilizar con mucha más facilidad. Estas interfaces, que se denominan “Escuchadores de eventos” (Event listeners), permiten controlar la interacción del usuario con la interfaz de usuario. Esto no quiere decir que no podamos extender una clase Vista para crear una clase nueva que herede el comportamiento de la clase anterior y redefinir los eventos de la misma directamente.
2.2.2 Uso de los Event Listeners Un Event Listener es una interfaz de la clase Vista (View) que contiene un único método de tipo callback. Android invoca estos métodos cuando la Vista detecta que el usuario está provocando un tipo concreto de interacción con este elemento de la interfaz de usuario. Existen los siguientes métodos callback: onClick(): de View.OnClickListener. Este método se invoca cuando el usuario toca un elemento con un dedo (modo contacto), hace clic con la bola de navegación (TrackBall) del dispositivo o presiona la tecla "Intro" estando en un objeto. 100
Diseño del interfaz de usuario onLongClick(): de View.OnLongClickListener. Este método se invoca cuando el usuario toca y mantiene el dedo sobre un elemento (modo de contacto), hace clic sin soltar con la bola de navegación (TrackBall) del dispositivo o presiona la tecla "Intro" durante un segundo estando en un elemento. onFocusChange(): de View.OnFocusChangeListener. Se invoca cuando el usuario mueve el cursor hacia una Vista o se aleja de ésta utilizando la bola de navegación (Trackball) o usando las teclas de navegación. onKey(): de View.OnKeyListener. Se invoca cuando el usuario se centra en un elemento y presiona o libera una tecla del dispositivo. onTouch(): de View.OnTouchListener. Se invoca cuando el usuario realiza una acción de tipo contacto con el dedo como presionar o soltar o cualquier gesto de movimiento en la pantalla dentro de la Vista. onCreateContextMenu(): de View.OnCreateContextMenuListener. Se invoca cuando se crea un menú contextual como resultado de una "pulsación larga" sobre un elemento. En la Unidad 3 veremos cómo se usa este tipo de menú.
El siguiente ejemplo muestra cómo especificar los métodos de los eventos sobre un EditText: // Definimos el evento Change del EditText texto.addTextChangedListener(new TextWatcher() {
// Método que se lanza antes de cambiar el texto
public void beforeTextChanged(CharSequence s, int start, int count, int after) { resultado1.setText("Texto antes de cambiar: "+s.toString()); }
// Método que se lanza cuando el texto cambia
public void onTextChanged(CharSequence s, int start, int before, int count) { resultado2.setText("Texto cambiado: "+s.toString()); }
// Método que se lanza cuando el texto cambia. La diferencia con el // método anterior es que la variable s es modificable
public void afterTextChanged(Editable s) {
// En este evento no hacemos nada
101
}
}); // end onChange EditText
En el código anterior fíjate de qué manera se define el Listener de los cambios del texto de una Vista de tipo EditText. Dentro de este Listener establecemos los métodos que vamos a gestionar (escuchar): beforeTextChanged, onTextChanged y afterTextChanged. También es posible definir un método común en toda la Actividad y asignarlo a las Vistas en el fichero de diseño de la interfaz del usuario res\layout\main.xml así:
A continuación, se muestra la implementación del método anterior: // Método onClick que invoca el botón "Calcular" // Este método se define a nivel de Actividad y es común a todas sus vistas public void miClickHandler(View view) {
// Debemos ver la Vista (botón) que ha invocado el método
switch (view.getId()) {
case R.id.boton: // Si se trata del botón "Calcular" // Vemos qué tipo de cálculo debemos hacer
RadioButton kilometrosButton = (RadioButton) findViewById(R.id.radio0);
// Si no se ha escrito nada mostramos un mensaje de error
if (texto.getText().length() == 0) { Toast.makeText(this, "Por favor, introduce un número", Toast.LENGTH_LONG).show();
return; }
102
Diseño del interfaz de usuario
// Definimos el formato del número resultante // Es importante tener bien configurado el AVD para que el // separador decimal sea la coma ","
DecimalFormat formatoNumero = new DecimalFormat("0.00");
// Obtenemos el nº en formato float
float inputValue = Float.parseFloat(texto.getText().toString());
// Convertimos a la unidad correspondiente
if (kilometrosButton.isChecked()) { distancia.setText("Kms son " + formatoNumero.format(inputValue*0.6214) + " Millas"); } else { distancia.setText("Millas son " + formatoNumero.format(inputValue*1.6093) + " Kms"); } break;
} // end switch
} // end miClickHandler
Fíjate en que la función callback miClickHandler() no devuelve ningún resultado (void); sin embargo, otros métodos deben devolver un resultado lógico (boolean) para finalizar su ejecución. A continuación vemos en qué consiste cada evento: onLongClick(): este método devuelve “true” para indicar que se han llevado a cabo las operaciones necesarias para manejar el evento clic, por lo que ya no debe lanzarse cualquier otro método de tipo “clic”. En caso contrario, si el método devuelve el valor “false”, Android puede invocar a continuación otro método diferente de tipo “clic”. onKey(): este método devuelve “true” o "false" para avisar si se han llevado a cabo las operaciones necesarias para manejar el evento de teclado, por lo que ya no debe lanzarse cualquier otro método de tipo “teclado”. onTouch(): en este método ocurre como en los dos casos anteriores, según devuelva "true" o "false" para señalar si Android debe invocar los siguientes métodos.
Recuerda que los eventos de tipo teclado siempre afectan a la Vista que está activa en ese momento.
103
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Eventos) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que hemos utilizado varios métodos de eventos de Android.
Si ejecutas el ejemplo, verás que la aplicación tiene el aspecto siguiente:
2.2.3 Gestores de Eventos (Event Handlers) En otro apartado de esta Unidad vamos a explicar cómo crear un componente (Vista) personalizado. En este caso es posible redefinir varios métodos de tipo callback utilizados como controladores (handlers) de eventos. A continuación vamos a hacer un resumen de los métodos que podemos redefinir en una clase heredada de una Vista de Android:
onKeyDown(int, KeyEvent): se invoca cuando el usuario usa el teclado del dispositivo.
onKeyUp(int, KeyEvent): se invoca cuando el usuario suelta una tecla.
onTrackballEvent(MotionEvent): se invoca cuando el usuario utiliza la bola de navegación (trackball) del dispositivo.
104
Diseño del interfaz de usuario
onTouchEvent(MotionEvent): se invoca cuando el usuario toca con el dedo sobre la pantalla del dispositivo.
onFocusChanged(boolean, int, Rect): se invoca cuando una Vista recibe o pierde el foco del usuario.
Hay otros métodos que, aunque no forman parte de la clase Vista, debes conocer, ya que permiten gestionar eventos de otros componentes de Android. Son los siguientes:
Activity.dispatchTouchEvent(MotionEvent): permite que podamos gestionar el evento que se invoca cuando el usuario toca la pantalla a nivel de Actividad antes de que llegue a alguna Vista de la interfaz de usuario.
ViewGroup.onInterceptTouchEvent(MotionEvent):
permite
a
la
Vista
ViewGroup (es un conjunto de Layout) gestionar eventos antes de que se remitan a los hijos que contiene.
ViewParent.requestDisallowInterceptTouchEvent(boolean): podemos usar este método para indicar que la Vista no debe interceptar los eventos de tipo "toque" de pantalla.
2.2.4 Modo táctil de pantalla Cuando un usuario interacciona con la interfaz de una aplicación Android usando las teclas del dispositivo o una bola de navegación (trackball) es necesario marcar las Vistas como activas (tienen el foco sobre ellas) coloreándolas, para que el usuario sepa que puede utilizarlas. Sin embargo, si el dispositivo tiene una pantalla táctil, el usuario puede interactuar con la interfaz utilizando su propios dedos. En este último caso, ya no es necesario resaltar las Vistas. Este modo de interacción se llama "modo táctil". Cuando un usuario toca la pantalla táctil, el dispositivo cambia a modo táctil, de manera que sólo las Vistas en las que el método isFocusableInTouchMode() devuelve "true" pueden recibir el foco, como ocurre, por ejemplo, en los EditText. Otras Vistas, como los botones, no pueden recibir el foco de la aplicación, sino simplemente inician el método clic cuando se presiona sobre ellos. Cada vez que un usuario pulsa una tecla o desplaza la bola de navegación (trackball), el dispositivo sale del modo táctil y busca una Vista donde activar de nuevo el foco de la aplicación. Así el usuario puede volver a interactuar con la interfaz de usuario sin tocar la pantalla. El estado de modo táctil se mantiene a lo largo de todo el sistema operativo (todas las ventanas y actividades). Para consultar el estado actual, se puede usar el método isInTouchMode(). De esta forma se puede saber si el dispositivo se encuentra en modo táctil.
105
2.2.5 Controlando la Vista con el foco activo El sistema Android cambia la Vista activa (con foco) en respuesta a la interacción del usuario. Las Vistas indican la posibilidad de recibir el foco a través del método isFocusable(). Para establecer si una Vista puede recibir el foco, hay que utilizar el método setFocusable(). Como hemos dicho, en el modo táctil, podemos usar el método isFocusableInTouchMode() y establecer si una Vista puede recibir el foco con setFocusableInTouchMode(). El cambio de foco automático que hace Android se basa en un algoritmo que busca la Vista vecina más cercana en una dirección. Normalmente, este algoritmo establecido por defecto no es el esperado por el usuario. Cuando esto ocurre, es posible definir explícitamente en el archivo de diseño res\layout\main.xml cómo se debe cambiar el foco con los siguientes atributos: nextFocusDown, nextFocusLeft, nextFocusRight y nextFocusUp indicando el id de la Vista al que se debe saltar. Por ejemplo:
Por lo general, en el diseño vertical anterior, intentar ir hacia arriba desde el primer botón no cambia el foco de la aplicación. Lo mismo ocurre con el segundo botón al intentar ir hacia abajo. Sin embargo, al haber definido en el botón superior el nextFocusUp para pasar al segundo botón, el foco cambia al botón de abajo y viceversa. Si queremos indicar que una Vista que por defecto no recibe el foco de la aplicación pueda recibirlo, podemos usar el atributo XML de la Vista android:focusable al diseño la interfaz de usuario. También podemos usar el atributo android:focusableInTouchMode cuando se trate del modo táctil. También desde el código Java podemos usar el método requestFocus() para indicar al sistema que una Vista debe tener el foco.
2.3
USO DEL EMULADOR DE ANDROID
Como ya hemos visto, el entorno de desarrollo de Android incluye un emulador virtual para poder ejecutar en un ordenador las aplicaciones que desarrollamos. El emulador 106
Diseño del interfaz de usuario
permite desarrollar y probar las aplicaciones de Android sin necesidad de disponer de un dispositivo físico. Este Emulador tiene esta apariencia:
El emulador de Android imita todas las características de hardware y software de un dispositivo móvil físico, exceptuando que no puede realizar llamadas a teléfono reales. Dispone de teclas de navegación y control sobre las que se puede hacer clic con el ratón. También muestra una pantalla en la que aparece la aplicación. Para poder diseñar y probar aplicaciones con mayor facilidad, el emulador de Android utiliza dispositivos virtuales (AVD: Android Virtual Device). Estos AVDs permiten establecer algunos aspectos del hardware (memoria) y software (versión de Android) del teléfono virtual. Como el emulador es un sistema operativo completo de Android, la aplicación que desarrollamos puede utilizar los servicios, otras aplicaciones, acceder a la red, reproducir audio y vídeo, almacenar y recuperar datos, hacer notificaciones, etcétera. Además, el emulador incluye la capacidad de depuración y de simulación de interrupciones de las aplicaciones (por ejemplo, cuando llega un mensaje SMS o una llamada telefónica) para simular los efectos en estado de latencia. En la Unidad 1 ya hemos visto cómo se inicia este emulador de Android; ahora vamos a estudiar cómo se utiliza. 107
NOTA: es importante que el dispositivo virtual se encuentre configurado en el idioma Español para que todos los ejemplos del curso funcionen correctamente. Se explica cómo hacerlo en el apartado “Cómo crear un proyecto Android” de la Unidad 1.
2.3.1 Teclado del emulador Se puede interactuar con el emulador como si se tratara de un dispositivo Android real. Para usar el modo táctil de la pantalla hay que usar el puntero del ratón y para escribir con las teclas del dispositivo hay que utilizar las teclas que aparecen a la derecha del emulador. Cuando accedemos por primera vez al emulador, aparece la pantalla de bienvenida del terminal bloqueado:
Para desbloquear la pantalla hay que arrastrar con el ratón el icono "candado" hacia la derecha. La tabla siguiente resume las relaciones entre las teclas del emulador y las teclas del teclado de tu ordenador:
Tecla del Emulador
Inicio 108
Tecla del ordenador INICIO
Función Se vuelve a la pantalla
Diseño del interfaz de usuario
principal de Android.
Menú (tecla izquierda)
Volver
Llamar
Colgar
Buscar
Botón encender/apagar
F2 o RePág
ESC
F3
F4
F5
BLOQUE_NUM_MAS (+), Ctrl+5
Bajar volumen
BLOQUE_NUM_MENOS (-) Ctrl+F6
Cambio orientación
Conexión datos
Pantalla completa
Bola navegación
Entra momentáneamente en modo bola navegación.
Vuelve a la pantalla anterior.
Llamar por teléfono.
Acabar una llamada.
Buscar información.
F7
Subir volumen
Cámara
Muestra un menú desplegable con las opciones de la pantalla actual.
Ctrl+BLOQUE_NUM_5, Ctrl+F3 Inicia la cámara de fotos. BLOQUE_NUM_7, Ctrl+F11 BLOQUE_NUM_9, Ctrl+F12
F8
Alt+Intro
F6
Suprimir
Cambia la orientación del móvil de horizontal (landscape) a vertical (portrait). Habilita y deshabilita la conexión de datos del móvil.
El emulador ocupa la pantalla completa del monitor del ordenador. Inicia el modo de navegación con bola (trackball) Mientras tengamos pulsada la tecla Suprimir
109
Arrastrar izq/arriba/dcha/abajo
BLOQUE_NUM_4/8/6/2
Ten en cuenta que para usar las teclas del bloque numérico es necesario desactivar el bloqueo numérico en tu ordenador. NOTA: es recomendable familiarizarse con el emulador de Android usando las funciones del dispositivo virtual e ir visitando las diferentes opciones del mismo así como utilizar los atajos de teclado de la tabla anterior.
2.3.2 Cómo introducir tildes con el Teclado del Emulador Para que se puedan introducir tildes en las cajas de texto de las aplicaciones es muy importante que el dispositivo virtual se encuentre configurado en el idioma Español. En el apartado “Cómo crear un proyecto Android” de la Unidad 1 se explica cómo hacerlo. Para que podamos introducir tildes en las cajas de texto, hay que pulsar un rato con el ratón en la vocal correspondiente:
Pulsar un rato
También podemos usar de la misma forma el teclado externo del emulador:
110
Diseño del interfaz de usuario
Pulsar un rato
Atención: también es posible pulsar un rato en la tecla de la vocal del teclado del ordenador donde estés trabajando para poder escribir la tilde correspondiente.
2.3.3 Limitaciones del Emulador
No se pueden enviar ni recibir llamadas de teléfono reales, aunque se pueden simular.
No se pueden usar conexiones por USB.
No se puede utilizar la cámara de fotos/vídeo.
No se pueden conectar unos auriculares al dispositivo virtual.
No se puede conectar a una red de telefonía.
No se puede establecer la carga de la batería.
No se puede detectar cuándo se inserta o se quita la tarjeta de memoria SD.
No se pueden establecer conexiones por Bluetooth.
2.3.4 Tamaño ventana emulador Es posible cambiar el tamaño de la ventana de AVD del Emulador para que se vea correctamente en la pantalla de tu ordenador. Para ellos hacemos clic en el menú de Eclipse “Android SDK and AVD Manager”:
111
Después, hacemos clic en el botón “Start”:
En esta ventana podemos ver la escala (Scale) que se aplicará a la ventana del emulador (un número entre 0,1 y 3). Para ello, podemos especificar la densidad del monitor del ordenador en puntos por pulgada (Monitor DPI) y el tamaño en pulgadas de la pantalla del dispositivo Android (Screen Size). Para arrancar el emulador basta con hacer clic en el botón “Launch”.
2.3.5 Otras opciones del Emulador Aunque el Emulador tiene muchas posibles opciones configurables para simular casi todos los casos posibles de un dispositivo real, vamos a describir las que son más importantes o usaremos más adelante en el curso:
Geo Localización: la localización geográfica por GPS es un servicio muy útil en los dispositivos de Android.
112
Diseño del interfaz de usuario
Encendido / Apagado dispositivo: es posible simular el encendido, apagado y bloqueo de la pantalla del teléfono. Incluso se puede simular el estado de la batería del móvil: cargando/descargando.
Llamada de teléfono: el emulador de Android incluye un módem virtual GSM que permite simular las funciones de telefonía. Por ejemplo, se pueden simular llamadas entrantes del teléfono y establecer conexiones de datos. Lo que no podemos usar es el audio en esta versión del Emulador.
Mensajes cortos SMS: se pueden crear mensajes SMS y dirigirlos al dispositivo virtual de Android. Es decir, se simula la recepción de mensajes cortos.
2.3.6 Cómo configurar las opciones del Emulador En Eclipse se incluye una herramienta para depurar de aplicaciones Android que se llama "Dalvik Debug Monitor Server" (DDMS). DDMS funciona tanto con un emulador de Android como con un teléfono real conectado por cable USB. Esta herramienta también se puede utilizar para configurar alguna de las opciones anteriores. Para acceder a esta herramienta en Eclipse hay que hacer clic en la opción del menú principal: Window -> Open Perspective -> DDMS. Una vez abierta la perspectiva de DDMS, en la pestaña "Emulator Control", podemos encontrar los campos necesarios para configurar estas opciones:
113
Nota: no todas las opciones posibles del Emulador están incluidas en la ventana anterior; algunas deben configurarse mediante órdenes en la línea de comandos del sistema operativo de tu ordenador.
En la Unidad 8 veremos cómo usar esta ventana para depurar aplicaciones Android.
2.4
COMPONENTES AVANZADOS
En la Unidad 1 ya hemos indicado que la interfaz de usuario en Android se construye usando componentes o Vistas que están contenidas en paneles de diseño (layout). Los componentes permiten interaccionar con el usuario con la aplicación. Los paneles ordenan la posición de estos elementos en la interfaz del usuario. Android dispone de diversos componentes que permiten al usuario seleccionar una opción dentro de una lista de posibilidades, tales como listas desplegables (Spinner), listas fijas (ListView), tablas (GridView). Además, existen otros componentes específicos de la plataforma, como las galerías de imágenes (Gallery). Este apartado está dedicado a los componentes de tipo selección y vamos a describir un elemento importante y común a todos ellos: los adaptadores.
2.4.1 Qué son los Adaptadores de Android (adapters) Un Adaptador (Adapter) es un objeto que permite definir el modelo de datos que usan todos los componentes de selección de forma unificada. Es decir, todos los componentes de selección acceden a los datos que contienen a través de un adaptador. El Adaptador, además de suministrar datos a los componentes visuales, también es responsable de generar las vistas específicas que se muestran dentro del componente de selección. Por ejemplo, si cada opción de una lista estuviera formada por una imagen y varias etiquetas, el Adaptador se encarga de generar el contenido de todas estas “opciones” diseñadas. Android dispone de varios tipos de adaptadores sencillos, aunque es posible extender fácilmente mediante herencia su funcionalidad para mejorarlos según las necesidades de la aplicación. Los más comunes son los siguientes:
ArrayAdapter: es el más sencillo de todos los adaptadores. Suministra datos a un componente de selección mediante una matriz de objetos de cualquier tipo.
114
Diseño del interfaz de usuario
SimpleAdapter: se usa para definir las opciones de un componente de selección con los datos de un fichero XML.
SimpleCursorAdapter: se utiliza para obtener las opciones de un componente de selección de la respuesta de una consulta a una base de datos.
A continuación vemos cómo crear un Adaptador de tipo ArrayAdapter: // Matriz con las opciones del Spinner final String[] datos = new String[]{"Opción 1","Opción 2","Opción 3", "Opción 4","Opción 5"}; // Adaptador que usamos para indicar al Spinner dónde obtiene las opciones ArrayAdapter adaptador = new ArrayAdapter(this, android.R.layout.simple_spinner_item, datos);
Para crear el adaptador usamos tres parámetros: 1. El contexto se refiere normalmente a la actividad donde se crea el adaptador. 2. El ID de la Vista que va a mostrar la opción seleccionada por el usuario. En este
caso,
usamos
un
ID
de
diseño
predefinido
por
Android
(android.R.layout.simple_spinner_item) y formado por un componente TextView. Podríamos haber escrito cualquier ID de un componente de la interfaz de usuario del proyecto. 3. La matriz de los datos que definen las opciones de la lista. De esta forma, ya hemos creado el adaptador para mostrar las opciones seleccionables y sólo hay que asignarlo al componente de selección adecuado.
Lista desplegable (Spinner) El componente Spinner permite al usuario seleccionar una única opción de un listado desplegable. En el ejemplo siguiente definimos una Lista desplegable: Ejemplo
android:layout_width="fill_parent" android:layout_height="wrap_content" />
Como en otros componentes de Android, podemos utilizar otras propiedades como el color de fondo (android:background), el estilo de la fuente (android:typeface), el color de fuente (android:textcolor), el tamaño de la fuente (android:textSize), etcétera. Para enlazar el adaptador que define las opciones de este tipo de listado y tratar el evento que ocurre cuando el usuario selecciona una de las opciones disponibles, escribimos las siguientes sentencias:
//Indicamos el tipo de Spinner dropdown adaptador.setDropDownViewResource( android.R.layout.simple_spinner_dropdown_item); // Establecemos el adaptador en el Spinner listadoOpciones.setAdapter(adaptador);
// Definimos el evento setOnItemSelected listadoOpciones.setOnItemSelectedListener( new AdapterView.OnItemSelectedListener() { @Override
// Si se selecciona una // actualizamos la etiqueta
opción,
mostramos
un
mensaje
y
public void onItemSelected(AdapterView> adapterView, View view, int position, long id) { Toast.makeText(getBaseContext(), datos[position], 1).show(); resultado.setText("Opción seleccionada: " + (position+1)); }
// Si no se selecciona nada limpiamos la etiqueta. En este tipo de // componentes siempre se selecciona una opción por lo que no se verá // este evento. public void onNothingSelected(AdapterView> adapterView) { resultado.setText(""); }
});
116
Diseño del interfaz de usuario
Para cambiar el aspecto de las opciones de la lista emergente hay que usar el método setDropDownViewResource(ID_layout). En este caso hemos utilizado el diseño predefinido de Android para las listas desplegables android.R.layout.simple_spinner_dropdown_item. Esto provoca que el diseño de la selección y el listado desplegable sean diferentes (fíjate en el cambio de color):
El evento de una Lista desplegable normalmente es onItemSelected, que se invoca cuando el usuario selecciona una opción de la lista. El manejo del evento es similar a otros componentes usando el método setOnItemSelectedListener(). En este evento definimos dos métodos: el primero de ellos es onItemSelected y se invoca cada vez que cambia la selección de la lista desplegable; y el segundo es onNothingSelected, que se invoca cuando no hay ninguna opción seleccionada (esto ocurre si el adaptador no tiene datos).
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Lista desplegable) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el emulador el resultado del programa anterior
117
Lista de selección (List View)
La Lista de selección (o ListView en inglés) permite al usuario hacer clic sobre una lista de opciones seleccionables. Estas opciones se muestran directamente sobre el propio componente; por lo tanto, no se trata de una lista emergente como el Spinner. Si no se visualizan todas las opciones disponibles en la pantalla del dispositivo porque no caben, se puede desplazar el listado con el dedo o los botones de navegación. En el ejemplo siguiente vamos a usar un Adaptador particularizado para que dibuje las opciones del menú personalizadas. Fíjate en el código del siguiente ejemplo que define un ListView en el fichero main.xml de layout de la aplicación:
Ejemplo
Para enlazar el adaptador que define las opciones de este tipo de listado y tratar el evento que ocurre cuando el usuario selecciona una de las opciones disponibles, escribimos las siguientes sentencias: // En este caso definimos la matriz de opciones usando la clase Opcion private Opcion[] datos = new Opcion[32];
... // Definimos 32 Opciones en el ListView for(int i=1; i<=32; i++) datos[i-1] = new Opcion("Opción " + i, "Ésta es la opción " + i);
// Usamos un adaptador para dibujar las opciones de la lista AdaptadorOpciones adaptador = new AdaptadorOpciones(this); // Establecemos el adaptador del Listview listaOpciones.setAdapter(adaptador);
118
Diseño del interfaz de usuario
// Definimos el evento setOnItemClick listaOpciones.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override
// Si se hace clic sobre una opción, mostramos un mensaje public void onItemClick(AdapterView> adapterView, View view, int position, long id) { Toast.makeText(getBaseContext(), "Has hecho clic en '" + datos[position].getTitulo()+"'", 1).show();
} });
Fíjate
que
en
este
caso
hemos
utilizado
un
nuevo
tipo
de
Adaptador:
AdaptadorOpciones, que personaliza el diseño de las opciones de la lista. Este Adaptador se define con el siguiente código: // Definimos el Adaptador que dibuja la opciones del ListView
class AdaptadorOpciones extends ArrayAdapter { Activity contexto;
// Contructor del adaptador usando el contexto de la aplicación actual
AdaptadorOpciones(Activity contexto) {
// Llamamos al constructor de la clase superior super(contexto, R.layout.listitem_opcion, datos); this.contexto = contexto;
} // Método que dibuja la Vista de cada Opción // Se invoca cada vez que haya que mostrar un elemento de la lista.
public View getView(int position, View convertView, ViewGroup parent) {
// Vista que Android indica como reutilizable View item = convertView; // Esta variable se usa para almacenar un objeto dentro // de la Vista que dibuja la opción
119
VistaTag vistaTag; // Si Android indica que no hay una Vista reutilizable para //la opción, la definimos, inflamos el diseño que se define // en el fichero listitem_opcion.xml y establecemos su contenido If (item == null) { // Usamos un Inflater para inflar el diseño // Ahora tenemos una Vista que se asocia al elemento LayoutInflater inflater = contexto.getLayoutInflater(); // Definimos en la vista de vuelta el tipo de diseño item = inflater.inflate(R.layout.listitem_opcion, null); // Definimos el objeto que vamos a almacenar en el //nuevo elemento vistaTag = new VistaTag(); // Obtenemos los punteros a las etiquetas recién infladas vistaTag.titulo = (TextView)item.findViewById(R.id.LblTitulo); vistaTag.subtitulo = (TextView)item.findViewById(R.id.LblSubTitulo); // Guardamos el objeto en el elemento item.setTag(vistaTag); } else { // Si estamos reutilizando una Vista, recuperamos el // objeto interno vistaTag = (VistaTag)item.getTag(); } // Cargamos las opciones de la matriz de datos vistaTag.titulo.setText(datos[position].getTitulo()); vistaTag.subtitulo.setText(datos[position].getSubtitulo()); // Devolvemos la Vista (nueva o reutilizada) que dibuja // la opción
return(item); }
} // end class AdaptadorOpciones
120
Diseño del interfaz de usuario
El método más importante de un Adaptador es getView() ya que es el encargado de mostrar los elementos de la lista. Lo primero que debe hacer este método es “inflar” el diseño layout XML que hemos definido en el fichero res\layout\listitem_opcion.xml. Para ello, hemos utilizando la clase LayoutInflater, que crea la estructura de diseño de los objetos mediante el método inflate(id_layout). Cada vez que es necesario mostrar un elemento de la lista en la pantalla, Android invoca este método para diseñarlo, incluso cuando ya se ha mostrado el elemento y se ha ocultado en la pantalla al desplazar la lista. Esto produce que, dependiendo del tamaño de la lista y de la complejidad del layout, se creen y destruyan muchos objetos aumentando el uso de la CPU y dela memoria y, al final, provocando un mayor consumo de batería. Sin embargo, Android permite reutilizar una Vista que ya hayamos “inflado” con anterioridad y que ya no haga falta por algún motivo; por ejemplo, porque el elemento correspondiente de la lista haya desaparecido de la pantalla al desplazar el listado. Así, se crean únicamente los objetos necesarios que se pueden visualizar en la pantalla del teléfono. Además, todos los componentes de Android tienen una propiedad denominada Tag, que puede almacenar dentro cualquier tipo de objeto. Para asignar y recuperar el objeto almacenado hay que emplear los métodos setTag() y getTag() respectivamente. Esta facilidad de los objetos de Android permite que almacenemos dentro cada Vista convertView las etiquetas que dibujan el diseño de la opción del listado. Para ello, vamos a definir una nueva clase VistaTag donde almacenamos las etiquetas TextView que hemos “inflado” para dicho elemento, de forma que, posteriormente, podamos recuperarlo fácilmente y cambiar su contenido. Por lo tanto, la clase VistaTag sólo contiene una referencia a cada uno de los componentes del diseño que hay que manipular, en este caso, las dos etiquetas de texto. Definimos esta clase de la siguiente forma: // Clase que se // de una opción
usa
para
almacenar
las
2
etiquetas
de
tipo
TextView
static class VistaTag { TextView titulo; TextView subtitulo; }
121
Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Lista de selección) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente ListView.
Lista de rejilla (Grid View)
La Lista de rejilla (o GridView en inglés) permite mostrar al usuario un conjunto de opciones seleccionables distribuidas como una tabla dividida en filas y columnas. Sus propiedades más importantes son:
android:numColumns: establece el número de columnas de la tabla. También podemos escribir “auto_fit” si queremos que este número sea calculado por el propio sistema operativo.
android:columnWidth: establece el ancho de las columnas de la tabla.
android:horizontalSpacing: establece el espacio horizontal entre celdas.
android:verticalSpacing: establece el espacio vertical entre celdas.
android:stretchMode: establece qué hacer con el espacio horizontal sobrante. Si se fija el valor “columnWidth”, este espacio será ocupado a partes iguales por las columnas de la tabla y si se fija el valor “spacingWidth”, será ocupado a partes iguales por los espacios entre celdas.
Fíjate en el código de ejemplo que define un GridView en el fichero main.xml de layout de la aplicación:
Ejemplo
Como en los anteriores ejemplos, para enlazar el adaptador que define las opciones de este tipo de listado escribimos las siguientes sentencias: 122
Diseño del interfaz de usuario
ArrayAdapter adaptador = new ArrayAdapter(this, android.R.layout.simple_list_item_1, datos);
final GridView gridOpciones = (GridView)findViewById(R.id.GridOpciones); gridOpciones.setAdapter(adaptador);
Además, también es posible gestionar el evento que ocurre cuando el usuario selecciona una de las opciones del listado.
Cuadro de texto con sugerencias (AutoComplete TextView)
El Cuadro de texto con sugerencias (o AutoComplet TextView en inglés) permite al usuario la introducción y edición de texto para que lo vea el usuario. Mientras el usuario escribe texto, este componente propone posibles frases. Su propiedad más importante es android:completionThreshold, que indica el número de caracteres mínimo que debe escribir el usuario para que se propongan sugerencias. Fíjate en el código de ejemplo que define un AutoCompleteTextView en el fichero main.xml de layout de la aplicación:
Ejemplo
Como en los anteriores ejemplos, para enlazar el adaptador que define las opciones de este tipo de listado escribimos las siguientes sentencias:
123
// Debemos implemetar los método de TextWatcher para poder detectar los // eventos de abajo public class mesesActivity extends Activity implements TextWatcher { private AutoCompleteTextView miAutoComplete; @Override public void onCreate(Bundle savedInstanceState) { String meses[]={ "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"};
... miAutoComplete = (AutoCompleteTextView)findViewById(R.id.miautocomplete);
ArrayAdapter adaptador = new ArrayAdapter(this, android.R.layout.simple_dropdown_item_1line, meses);
miAutoComplete.addTextChangedListener(this); miAutoComplete.setAdapter(adaptador);
} @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Aquí escribimos el código que se ejecuta antes de que el texto cambie } @Override public void onTextChanged(CharSequence s, int start, int before, int count) {
// Aquí escribimos cambiado }
124
el
código
que
se
ejecuta
cuando
el
texto
ha
Diseño del interfaz de usuario
@Override public void afterTextChanged(Editable s) {
// Aquí escribimos el código que se ejecuta después de que el texto cambie }
}
Además, dispone de las mismas propiedades y eventos que un componente EditText.
Actividad con lista de selección (List Activity)
Una Actividad con lista de selección (en inglés, ListActivity) es una actividad heredada de la clase Activity que ya incluye el componente de selección ListView. El componente ListActivity tiene un diseño predeterminado que consiste en una lista de selección que ocupa toda la pantalla del dispositivo. Sin embargo, es posible personalizar el diseño de esta lista usando la función setContentView() en el evento OnCreate() de la Actividad. Para poder rediseñar esta lista de selección es obligatorio que, en el fichero main.xml de layout de la aplicación, se defina el componente ListView con el id @+id/android:list. Opcionalmente, la pantalla de la aplicación puede contener otro objeto que se mostrará cuando la lista de selección esté vacía. Este componente que se muestra cuando la lista está vacía debe tener el id android:id/empty. El código siguiente muestra un diseño personalizado poco agraciado de la pantalla. Tiene una lista con un fondo verde, rojo y un escueto mensaje "sin datos". Fíjate en el código de ejemplo que define este componente en el fichero main.xml de layout de la aplicación: Ejemplo
125
Para cambiar el diseño de las opciones del ListView interno de la actividad ListActivity hay que definir un nuevo fichero XML de diseño dentro de la carpeta res\layout. Por ejemplo, el fichero se puede llamar fila.xml y contener el siguiente código:
El código anterior crea un diseño de una etiqueta en negrita para definir cada opción del listado. Para acabar, vamos a cargar datos en el objeto ListView de la actividad ListActivity usando un Adaptador de tipo matriz en el fichero Java correspondiente de la aplicación:
public class MiListado extends ListActivity { @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.main);
126
Diseño del interfaz de usuario
// Creamos una matriz de datos que usaremos en la ListActivity String[] nombres = new String[] { "Firefox", "Internet Explorer", "Opera", "Chrome", "Safari" };
// Definimos el adaptador del ListView contenido en el ListActivity. // Hay que indicar el diseño de la opción (R.layout.fila) y el campo // que debemos rellenar (R.id.texto)
this.setListAdapter(new ArrayAdapter(this, R.layout.fila, R.id.texto, nombres)); } }
En el ejemplo anterior podríamos haber utilizado un matriz de objetos en lugar de cadena y extender el Adaptador para dibujar las opciones del listado con el método getView(), tal y como hemos visto en el Ejemplo 4 de esta Unidad.
2.5
COMPONENTES PERSONALIZADOS
2.5.1 Diseño de componentes personalizados Hasta ahora en el curso hemos estudiado los componentes básicos que proporciona Android. Usando estos componentes podemos diseñar interfaces de usuario. A veces, la funcionalidad de la aplicación requiere emplear componentes diseñados por el programador. Como en muchos lenguajes de programación, Android permite crear componentes personalizados de las siguientes maneras: 1. Extendiendo la funcionalidad de un componente ya existente. 2. Combinando varios componentes para formar otro compuesto. 3. Diseñando desde cero un nuevo componente. A continuación, vamos a ver las formas 1 y 2 de crear componentes personalizados.
2.5.2 Cómo crear un componente extendido A continuación, vamos a ver cómo podemos crear un nuevo componente usando un componente básico ya existente de Android. A modo de ejemplo, vamos a extender la Vista
127
EditText (cuadro de texto) para que muestre el número de caracteres que contiene a medida que se escribe en él. Se trata de simular un editor de mensajes cortos SMS del propio sistema operativo que nos avisa del número de caracteres que contiene el mensaje. En nuestro caso, como resultado obtendremos un componente como el que se muestra en la siguiente imagen:
Podemos observar en la esquina superior derecha del cuadro de texto el número de caracteres del mensaje de texto introducido, que se actualiza a medida que modificamos el texto. Lo primero que hay que hacer es crear una nueva clase Java que extienda el componente que utilizamos de partida como base, en este caso EditText. El código de esta nueva subclase es éste:
// El componente EditTextExtendido se extiende de EditText public class EditTextExtendido extends EditText { private Paint pNegro; private Paint pBlanco;
// Hay que reescribiremos siempre los constructores heredados. // En este caso son tres public EditTextExtendido(Context context, AttributeSet attrs, int defStyle){ super(context, attrs, defStyle); pinceles(); }
public EditTextExtendido(Context context, AttributeSet attrs) { super(context, attrs);
128
Diseño del interfaz de usuario
pinceles(); } public EditTextExtendido(Context context) { super(context); pinceles(); }
// Función que inicia las pinceles que usamos para pintar el cuadradito negro private void pinceles() { pNegro = new Paint(Paint.ANTI_ALIAS_FLAG); pNegro.setColor(Color.BLACK); pNegro.setStyle(Style.FILL); pBlanco = new Paint(Paint.ANTI_ALIAS_FLAG); pBlanco.setColor(Color.WHITE); }
// Para modificar el aspecto del EditText hay que reescribir este método @Override public void onDraw(Canvas canvas) {
//Invocamos al método de la superclase (EditText)
super.onDraw(canvas);
//Dibujamos el fondo negro del contador en la parte de arriba derecha
canvas.drawRect(this.getWidth()-30, 5, this.getWidth()-5, 20, pNegro);
//Dibujamos el número de caracteres sobre el contador canvas.drawText("" + this.getText().toString().length(), this.getWidth()-28, 17, pBlanco); }
}
Es importante reescribir siempre los tres constructores heredados. Para modificar el aspecto del componente incluyendo el contador de caracteres tendremos hay que reescribir el método onDraw() que Android invoca cada vez que hay que dibujar el componente en la pantalla del dispositivo. Este método tiene el parámetro Canvas 129
que es el “lienzo” sobre el que podemos dibujar todos los elementos extra necesarios en el componente. La clase Canvas proporciona varios métodos para dibujar líneas, rectángulos, elipses, texto, imágenes, etcétera, sobre el espacio ocupado por el componente. En este caso únicamente vamos a dibujar un rectángulo que sirve de fondo para el contador y el texto con el número de caracteres actual que ha escrito el usuario. Para dibujar el gráfico es necesario definir dos “pinceles” (clase Paint). El primero permite pintar de color negro y con relleno sólido, y el segundo pinta de color blanco. Como sólo es necesario crear estos pinceles una vez, los hemos definido como atributos de la clase y los inicializamos en los tres constructores del componente. Finalmente, dibujamos el fondo y el texto del contador mediante los métodos drawRect() y drawText()del objeto Canvas usando posiciones relativas al espacio ocupado por el componente. Para añadir el nuevo componente a la interfaz de nuestra aplicación hay que incluirlo en fichero res\layout\main.xml que define el diseño de la ventana como cualquier otro componente, teniendo en cuenta que debemos hacer referencia a él con el nombre completo de la nueva clase creada: es.mentor.unidad2.eje5.edittextext.EditTextExtendido. El fichero tiene este aspecto:
Desde Eclipse puedes abrir el proyecto Ejemplo 5 (EditText Extendido) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado un componente extendido.
2.5.3 Cómo combinar varios componentes para crear uno compuesto Android permite la creación de componentes compuestos a partir de varios componentes estándar combinando la funcionalidad de todos ellos en un único componente reutilizable en otras aplicaciones.
130
Diseño del interfaz de usuario
A modo de ejemplo, vamos a crear un componente de identificación (login) formado por varios componentes estándar de Android. Además, en este componente compuesto hemos definido un nuevo evento personalizado. Lo primero que hemos hecho es diseñar la interfaz del componente compuesto a partir de componentes estándar de Android: etiquetas, cuadros de texto y un botón. Para ello, definimos
un
nuevo
layout
XML
en
la
carpeta
\res\layout
con
el
nombre
“componente_login.xml“. En este fichero vamos a establecer la estructura típica de una pantalla que muestra una ventana de login. El fichero es el siguiente:
131
A continuación, creamos la clase Java asociada a este componente compuesto donde se define toda su funcionalidad. Como el diseño está basado en la clase LinearLayout, el nuevo componente debe heredar también esta clase Java de Android. Redefiniremos además los dos constructores básicos:
public class ComponenteLogin extends LinearLayout {
// Componentes que forman el componente compuesto private EditText textoUsuario; private EditText textoPassword; private Button botonLogin; private TextView labelMensaje;
// Evento que se invoca cuando el usuario pulsa el botón Entrar private OnLoginListener listener; // Constructor por defecto public ComponenteLogin(Context context) { super(context); inicializar(); }
// Constructor que define el texto del botón de acceso public ComponenteLogin(Context context, AttributeSet attrs) { super(context, attrs); inicializar();
// Procesamos los atributos XML personalizados TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ComponenteLogin); String textoBoton = a.getString(R.styleable.ComponenteLogin_texto_boton);
// Escribimos el testo del botón botonLogin.setText(textoBoton); // Liberamos memoria a.recycle(); }
132
Diseño del interfaz de usuario
// Este método se usa para dibujar (inflar) el componente compuesto private void inicializar() {
//
Utilizamos el diseño layout 'componente_login' // componente. Primero obtener el inflater
como
interfaz
del
String infService = Context.LAYOUT_INFLATER_SERVICE; LayoutInflater li = (LayoutInflater)getContext().getSystemService(infService);
// Inflamos el componente compuesto definido en el XML li.inflate(R.layout.componente_login, this, true); // Obtenemos las referencias a los distintos componentes internos textoUsuario = (EditText)findViewById(R.id.TextoUsuario); textoPassword = (EditText)findViewById(R.id.TextoPassword); botonLogin = (Button)findViewById(R.id.BotonAceptar); labelMensaje = (TextView)findViewById(R.id.LabelMensaje);
//Asociamos los eventos necesarios asignarEventos(); }
// Establece el listener del componente compuesto public void setOnLoginListener(OnLoginListener l) { listener = l; }
// Define un nuevo evento para el componente compuesto private void asignarEventos() {
// Cuando el usuario hace clic sobre el botón Entrar // entonces se lanza el evento onLogin con los datos // de las cajas de texto botonLogin.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) {
133
listener.onLogin(textoUsuario.getText().toString(), textoPassword.getText().toString()); } }); }
// Permite escribir la etiqueta con el resultado del Login public void setMensaje(String msg) { labelMensaje.setText(msg); } }
Como se puede observar en las sentencias anteriores, el método inicializar() es el encargado de "inflar" el diseño XML que hemos definido, de obtener las referencias a todos los componentes internos y deasignar el evento necesario. Además, se incluye también un método que permite modificar el texto de la etiqueta que muestra un mensaje con el resultado del login. Finalmente, hemos implementado un evento que permite responder cuando el usuario de la aplicación hace clic en el botón "Entrar". Para concretar los detalles de dicho evento, definimos una interfaz Java con su listener. Esta interfaz sólo tiene el método onLogin() que devuelve los dos datos introducidos por el usuario (usuario y contraseña). La interfaz es muy sencilla:
public interface OnLoginListener { void onLogin(String usuario, String password); }
Una interfaz en Java es una colección de métodos abstractos y propiedades. En ella se especifica qué se debe hacer, pero no su implementación. En la clase de la Actividad es donde vamos a implementar esta interfaz que describe la lógica del comportamiento de los métodos:
134
Diseño del interfaz de usuario
// Definimos lo que hace el evento onLogin compLogin.setOnLoginListener(new OnLoginListener() { @Override public void onLogin(String usuario, String password) {
//Validamos el usuario y la contraseña if (usuario.equals("admin") && password.equals("admin")) compLogin.setMensaje("¡El usuario es correcto!"); else compLogin.setMensaje("Error:
el
usuario
o
la
contraseña no son correctos.");
} });
Ahora ya podemos definir el nuevo componente en una actividad como si se tratase de cualquier otro componente predefinido por Android. En el fichero main.xml que define la interfaz de usuario basta con hacer referencia a él utilizando la ruta completa del paquete Java, que en este caso sería es.mentor.unidad2.eje6.logincompuesto:
135
sgo:texto_boton="Entrar" />
Como el componente compuesto es heredado de un LinearLayout, podemos utilizar cualquier atributo permitido en dicho componente; en este caso hemos establecido, por ejemplo, los atributos layout_width, layout_height y background. Además, vamos a definir también atributos xml exclusivos para este componente compuesto. Como ejemplo, definimos el atributo texto_boton que permite establecer el texto del botón de la ventana desde el el fichero anterior de diseño layout xml. Para ello, hay que declarar un nuevo atributo y asociarlo al componente ComponenteLogin. Esto se define en el fichero \res\values\attrs.xml, que incluye la etiqueta asociada al ComponenteLogin donde indicaremos el nombre (name) y el tipo (format) del nuevo atributo:
Este atributo se usa en el segundo constructor de la clase ComponenteLogin mediante el método obtainStyledAttributes() del contexto de la aplicación. Obtenemos el valor del nuevo atributo definido (que es su ID formado por la concatenación del nombre del componente y el nombre del atributo, en nuestro caso ComponenteLogin_texto_boton) y modificaremos el texto por defecto del botón con el nuevo texto. Desde Eclipse puedes abrir el proyecto Ejemplo 6 (Login compuesto) de la Unidad 2. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado un componente extendido.
Si ejecutas este ejemplo verás que el aspecto que presenta es el siguiente:
136
Diseño del interfaz de usuario
Ten en cuenta que en el código de esta aplicación únicamente se da por bueno el usuario y contraseña admin/admin.
137
Una Actividad (Activity) es un componente de Android que ofrece una pantalla con la que los usuarios pueden interactuar en una aplicación. Una aplicación de Android consta de múltiples actividades que están más o menos ligadas entre sí. Una función de llamada (en inglés callback) es una función que se remite a Android cuando se inicia una Actividad para que el sistema operativo la “llame” durante la ejecución de esta Actividad. El ciclo de vida de una Actividad de Android está compuesto de tres estados: Resumed, Paused y Stopped. Como muchos otros entornos de desarrollo, la interacción con el usuario en Android está basada en Controladores de Eventos (Event Handlers). En Android el programador puede capturar los eventos específicos del objeto Vista (View) con la que el usuario interactúa y ejecutar sentencias. Un Event Listener es una interfaz de la clase Vista (View) que contiene un único método de tipo callback que Android invoca cuando detecta que el usuario está provocando un tipo concreto de interacción con este elemento de la interfaz de usuario. Es posible redefinir métodos de tipo callback utilizados, como controladores (handlers) de eventos para ampliar las funcionalidades de las Vistas. El modo táctil permite al usuario interactuar con la interfaz utilizando su propios dedos. El entorno de desarrollo de Android incluye un emulador virtual para poder ejecutar en un ordenador las aplicaciones que desarrollamos sin necesidad de disponer de un dispositivo físico. Un Adaptador (Adapter) es un objeto que permite definir el modelo de datos que usan todos los componentes de selección de forma unificada.
138
Diseño del interfaz de usuario
Android permite crear componentes personalizados de las siguientes maneras:
Extendiendo la funcionalidad de un componente ya existente
Combinando varios componentes para formar otro compuesto
Diseñando desde cero un nuevo componente
139
140
Unidad de Aprendizaje 9
FECTOS DE TRANSICIÓN EMÁS INFORMACIÓN SOBRE Y ANANDROID IMACIÓN
ÍNDICE 3.1
INTRODUCCIÓN .......................................................... 143 3.1.1 Introducción ...................................................................143 3.1.2 Gestión del botón “Hacia atrás” de Android ............143 3.1.3 Definición de una tarea en los proyectos Android...145
3.2
GUARDAR Y RECUPERAR EL ESTADO DE UNA ACTIVIDAD.................................................................... 147
3.3
PROCESOS EN HILOS EN ANDROID ........................ 154 3.3.1 Procesos ........................................................................154 3.3.2 Ciclo de vida de los procesos ....................................155 3.3.3 Hilos de ejecución en Android ....................................157
3.4
HILOS EN SEGUNDO PLANO..................................... 158 3.4.1 Utilización de tareas asíncronas con la clase AsyncTask ..............................................................................159
3.5
MENÚS DE ANDROID ................................................. 162 3.5.1 Ejemplo de Menú Principal y Submenú ....................162 3.5.2 Ejemplo de Menú Contextual......................................167 3.5.3 Ventanas de diálogo en Android ................................172 3.5.3.1 Ventanas de diálogo con mensaje ....................174 3.5.3.2 Ventanas de diálogo con botones ....................174 3.5.3.3 Ventanas de diálogo con selección ..................176 3.5.3.4 Ventanas de diálogo personalizada..................177
Más información sobre Android
3.1
INTRODUCCIÓN
3.1.1
Introducción En esta Unidad vamos a explicar cómo funciona la tecla “Retroceso” de los
dispositivos Android. Además, veremos cómo almacenar el estado de una Actividad de Android. También, detallaremos el uso de Hilos en Android. Finalmente, usaremos Menús y Ventana de diálogo para mejorar la interfaz de usuario con ejemplos de aplicaciones de Android.
3.1.2
Gestión del botón “Hacia atrás” de Android Como ya hemos visto, una aplicación puede contener múltiples actividades. El
programador desarrolla una actividad para que el usuario realice un tipo específico de acción. Además, una actividad puede arrancar otras actividades. Por ejemplo, una aplicación de correo electrónico puede tener una Actividad que muestre una lista con los mensajes nuevos. Así, cuando el usuario seleccione un correo electrónico, se puede abrir una nueva actividad para mostrar su contenido. Una
Actividad
también
puede
arrancar
las
actividades
que
definen
otras
aplicaciones. Por ejemplo, si una aplicación tiene que enviar un correo electrónico, puede usar el objeto “Intención” (en inglés, Intent) de Android para realizar el envío incluyendo alguna información, como la dirección de correo electrónico del destinatario y un mensaje. En la Unidad 5 veremos cómo se usan estas Intenciones ("Intents"). Para que se pueda hacer esto, hay que declarar la Actividad que envía mensajes para que admita este tipo de “intenciones”. En este caso, la “intención” es enviar un correo electrónico, por lo que la aplicación disponible para crear correos electrónicos se inicia (si hubiera varias Actividades que tuvieran la misma “intención”, el sistema permite al usuario seleccionar la que desea utilizar). Finalmente, cuando se envía el mensaje, la actividad inicial se reanuda y “parece” que la Actividad de correo electrónico forma parte de la aplicación general. A pesar de que las actividades pueden pertenecer a diferentes aplicaciones, Android mantiene la sensación de que se trata de una única aplicación juntándolas en una sola Tarea (Task). Una Tarea es un conjunto de actividades con las que un usuario interactúa. Android organiza las actividades en una pila de ejecución (en inglés stack) donde se van apilando las actividades que el usuario va invocando. Cada Tarea tiene asociada su propia pila de ejecución independiente. La ventana siguiente muestra la pantalla principal del dispositivo (HOME):
143
Cuando el usuario toca un icono de esta pantalla, se inicia una tarea asociada a la aplicación. Si es la primera vez que arrancamos la aplicación, el sistema operativo crea una nueva tarea y su Actividad principal ("main") queda almacenada en primer lugar en la pila de Android. Cuando esta Actividad principal arranca otra, la nueva Actividad se añade a la parte superior de la pila y pasa a primer plano. La Actividad anterior permanece en la pila y se detiene manteniendo el estado actual de la interfaz de usuario. Si el usuario pulsa la tecla de retroceso
del teléfono, la Actividad actual se quita de la parte superior de la pila, se
destruye y se reanuda la Actividad anterior en el estado que estuviera. Las actividades en la pila no se pueden reorganizar, se trata de una pila de tipo "último en entrar, primero en salir" ("last in, first out"). En el siguiente esquema visualizamos cómo cambia la pila de Android al ir abriendo Actividades y al pulsar el botón "Volver Atrás":
144
Más información sobre Android Si el usuario continúa presionando "Volver atrás", se extraerá una a una cada actividad de la pila mostrando la anterior, hasta que aparezca la pantalla de inicio u otra aplicación que se haya iniciado antes. Cuando esto ocurre, la tarea se destruye. Una tarea es una unidad compuesta de actividades que puede pasar a un segundo plano cuando los usuarios inician una nueva tarea o van a la pantalla de inicio. Cuando una tarea se encuentra en segundo plano, todas sus actividades se detienen, aunque su pila de ejecución se mantiene intacta hasta que el usuario decida que vuelva al "primer plano". Android denomina a esta manera de guardar el estado de las aplicaciones Multitarea (Multitasking), si bien no se trata de ejecución simultánea de las aplicaciones, sino de la posibilidad de seguir en el estado donde la dejamos cuando ejecutemos otra aplicación y volvamos a la inicial.
Nota: es posible mantener muchas tareas en segundo plano a la vez; sin embargo, si el sistema operativo necesita memoria, puede destruirlas perdiendo sus estados de ejecución.
3.1.3
Definición de una tarea en los proyectos Android Si abrimos el fichero AndroidManifest.xml del Ejemplo 1 de la Unidad 1, veremos que
contiene las siguientes etiquetas:
Aquí podemos establecer una Actividad como punto de entrada de la tarea escribiendo el "android.intent.action.MAIN" como la acción principal. En este caso, la Actividad .BienvenidoActivity está definida como principal.
145
Nota: debido a la nomenclatura de Android, es obligatorio escribir un punto al principio del nombre.
Aunque no es lo normal, es posible definir varios puntos de entrada en un mismo proyecto. Un punto de entrada hace que Android cree un icono y una etiqueta en la pantalla de inicio para la actividad correspondiente, ofreciendo a los usuarios una forma de iniciar la actividad. La categoría de la Actividad android:name="android.intent.category.LAUNCHER es muy importante, ya que permite a los usuarios volver a una tarea que está en segundo plano desde el Lanzador de actividades. Para llegar a este Lanzador debemos hacer clic en el botón:
A continuación aparecen todas las aplicaciones instaladas:
También es posible ver las aplicaciones recientes pulsando un par de segundos el botón "Inicio"
146
para acceder a las ejecutadas anteriormente:
Más información sobre Android
Por esta razón, es muy importante marcar la actividad principal con la categoría "android.intent.category.LAUNCHER", para que el usuario pueda volver a una aplicación que se encuentre en segundo plano y que ya no es visible.
3.2
GUARDAR Y RECUPERAR EL ESTADO DE UNA ACTIVIDAD La configuración de un teléfono puede cambiar mientras un usuario ejecuta una
aplicación. Por ejemplo, la orientación de la pantalla (vertical/horizontal), la disponibilidad de teclado, el idioma, etcétera. Cuando este cambio se produce, Android reinicia la Actividad usando
el
método
OnDestroy()
e
inmediatamente
seguido
por
onCreate().
Este
comportamiento de reinicio está diseñado para que la aplicación se adapte a la nueva configuración de forma automática, por ejemplo, cuando cambia la posición de los componentes. La mejor manera de manejar un cambio de configuración para preservar el estado de la aplicación es usar los métodos onSaveInstanceState() y onCreate(). En la Unidad 2 ya hemos estudiado lo que es una Actividad y su ciclo de vida. En este apartado vamos a describir lo que hace Android cuando una Actividad se destruye o se detiene, cómo se conserva su estado en la memoria del sistema operativo, incluyendo todos los cambios que haya hecho el usuario en la interfaz introduciendo información, seleccionando opciones, etcétera. En el siguiente esquema se muestra los eventos y estados por los que pasa una Actividad cuando se detiene o se destruye.
147
Como se puede ver en el esquema anterior, una Actividad recupera su estado de ejecución si se detiene y vuelve a primer plano; o si se destruye y se vuelve a crear. Para almacenar la información importante sobre el estado de ejecución de una Actividad podemos usar el método de llamada (callback) de Android onSaveInstanceState() y luego restaurar esta información cuando el sistema vuelva a crear la Actividad. El sistema llama a este método justo antes de que la Actividad se destruya y le pasa como parámetro un objeto de tipo Bundle. Este objeto Bundle es donde podemos almacenar la información del estado de la Actividad como pareja nombre-valor, utilizando el método putString(). De esta manera, si Android mata el proceso de la Actividad y el usuario vuelve a ejecutar la misma Actividad, el sistema pasa el objeto Bundle almacenado anteriormente como parámetro en el método onCreate(), para que pueda restaurar el estado de ejecución. Si no hay información sobre este estado, el objeto Bundle es nulo.
Nota: no hay garantías de que Android invoque siempre el método onSaveInstanceState() justo antes de que la Actividad se destruya, ya que hay casos en los que no es necesario guardar el estado de ejecución; por ejemplo, cuando el usuario sale de la aplicación usando la tecla “Volver atrás”
, que indica que desea salir de la Actividad.
Sin embargo, incluso si no lo hacemos en el método onSaveInstanceState(), la aplicación restaura por defecto automáticamente parte del estado de la Actividad. En
148
Más información sobre Android concreto, la implementación por defecto de este método recorre cada componente de la interfaz de usuario, guardando y restaurando automáticamente estos componentes. Por ejemplo, se almacena automáticamente el texto contenido en un componente de tipo EditText o la selección en un componente CheckBox. Lo único que debemos hacer para que Android guarde el estado de los componentes es proporcionar un identificador único (atributo android:id) para cada uno de ellos. Si un componente no tiene un identificador, entonces no se guarda su estado. También es posible indicar de forma explícita que no se almacene automáticamente el estado de un componente de dos formas: en el diseño de la interfaz de usuario con el atributo android:saveEnabled establecido a "false", o usando el método del componente setSaveEnabled(). Por lo general, no se debe desactivar esta opción, salvo que debamos proteger cierta información (contraseñas) o que al restaurar el estado de la interfaz de usuario haya que mostrar la pantalla de manera diferente. Aunque el método onSaveInstanceState() guarda la información útil del estado de la interfaz de usuario, puede ser necesario guardar información adicional, como la orientación del dispositivo (horizontal o vertical). Podemos usar este método y la variable Bundle para almacenar esta información extra. Ten en cuenta que siempre debemos invocar el método de la clase madre antes de guardar el resto de información: super.onCreate(estadoAlmacenado);
Muy importante: debido a que Android no garantiza que siempre se invoque el método onSaveInstanceState(), debe usarse únicamente para almacenar el estado transitorio de la Actividad, es decir, la interfaz de usuario. Nunca debe emplearse para almacenar datos persistentes con preferencias del usuario o datos de la aplicación. En la Unidad 4 veremos cómo se implementa esta funcionalidad.
A continuación, vamos a describir el Ejemplo 1 de esta Unidad para que queden más claros los conceptos anteriores. En el fichero res\layout\main.xml hemos definido la siguiente interfaz de usuario:
149
Se trata de una sencilla pantalla de autentificación de usuario. Fíjate que hemos definido el atributo android:saveEnabled="false" para que no se almacene el campo contraseña.
Después, hemos definido la Actividad así:
150
Más información sobre Android public class GuardarestadoactividadActivity extends Activity { @Override protected void onCreate(Bundle estadoAlmacenado) { // Recuperamos el estado de la Actividad super.onCreate(estadoAlmacenado); setContentView(R.layout.main); if (estadoAlmacenado != null) { // Recuperamos una variable que se almacena con el estado Toast.makeText(this, "Evento onCreate(). Se recupera variable almacenada: " + estadoAlmacenado.getString("VARIABLE_ALMACENADA"), Toast.LENGTH_LONG).show(); } } @Override protected void onSaveInstanceState(Bundle estado) { // Guardamos el estado de la Actividad (sus componentes) super.onSaveInstanceState(estado); // Añadimos una variable que se almacena con el estado estado.putString("VARIABLE_ALMACENADA", "Texto guardado"); Toast.makeText(this, "El estado de la Actividad se ha guardado", Toast.LENGTH_LONG).show(); } @Override protected void onDestroy() { super.onDestroy(); Toast.makeText(this, "El sistema ha terminado la Actividad", Toast.LENGTH_LONG).show(); } }
En el código anterior hemos usado los métodos putString() y getString() del objeto Bundle para almacenar el contenido de una variable extra en el estado de la Actividad.
151
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Guardar estado de una actividad) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que guardamos el estado de ejecución de la Actividad
Para entender mejor la funcionalidad que almacena el estado de la Actividad, inicia este ejemplo en el emulador. Verás la siguiente pantalla:
A continuación, introduce algún texto en los campos que aparecen. Después, haz clic en el botón "INICIO"
del emulador. Aparecerá el mensaje "El estado de la Actividad se ha
guardado" en la pantalla y desaparecerá la aplicación:
Si volvemos a ejecutar la aplicación, veremos que podemos ver tanto los datos tanto de la caja de texto usuario como de la contraseña. Esto ocurre porque Android no ha destruido la Actividad.
152
Más información sobre Android Para que Android pare (kill) la Actividad cuando la dejamos, hay que configurar las herramientas de desarrollo del dispositivo conocidas como "Dev-Tools".
Para acceder a
estas herramientas hay que hacer clic en el icono "Dev Tools" que aparece en el lanzador de aplicaciones. Después, hacemos clic en la opción "Development Settings" y, finalmente, marcamos la opción "Immediately destroy activities":
Si ahora ejecutas de nuevo la aplicación y pulsas otra vez el botón "INICIO", verás el mensaje "El sistema ha terminado la Actividad", que indica que la aplicación se destruye:
Si vuelves a abrir de nuevo la aplicación, observarás que únicamente se recupera el campo nombre de usuario.
Importante: si haces clic en el botón ATRÁS del emulador, no se guarda el estado de la actividad, ya que Android considera que el usuario desea finalizar la ejecución de la aplicación. En este caso, todos los valores de los componentes se perderán.
153
3.3
PROCESOS EN HILOS EN ANDROID Un hilo es una característica de la informática que permite a una aplicación realizar
varias tareas a la vez (concurrentemente). Esencialmente, un hilo es una tarea que se ejecuta en paralelo con otra tarea. Todos los hilos que se ejecutan a la vez pueden compartir una serie de recursos, tales como la memoria, los archivos abiertos, etcétera. Esta técnica permite simplificar el diseño de una aplicación que puede así llevar a cabo distintas funciones simultáneamente. Cuando un usuario ejecuta una aplicación en Android se inicia un nuevo proceso de Linux con un único hilo de ejecución. Por defecto, todos los componentes de una misma aplicación se ejecutan en el mismo hilo principal (en inglés se denomina "main thread"). A este hilo también se lo denomina hilo de la interfaz de usuario (en inglés, "UI thread"). Si se inicia una Actividad de una aplicación que ya se está ejecutando y, por lo tanto, ya existe un proceso asociado, entonces la nueva Actividad se inicia dentro de ese proceso y utiliza el mismo hilo de ejecución. Sin embargo, es posible diseñar aplicaciones que ejecuten sus diferentes componentes de la aplicación en procesos separados y el programador puede crear subprocesos adicionales para cualquier proceso principal. En este apartado vamos a estudiar cómo funcionan los procesos e hilos en una aplicación de Android.
3.3.1
Procesos Ya hemos comentado que, por defecto, todos los componentes de una misma
aplicación se ejecutan en el mismo proceso. La mayoría de las aplicaciones deben ejecutarse de esta manera. Sin embargo, a veces, es necesario controlar en qué proceso se ejecuta un componente
en
particular.
Para
hacer
esto,
debemos
modificar
el
archivo
AndroidManifest.xml del proyecto de Android. En este fichero se definen con etiquetas los componentes de la aplicación:
Actividad: etiqueta
Servicio: etiqueta
Broadcast receiver: etiqueta
Content provider: etiqueta
Dentro de estas etiquetas se puede definir el atributo android:process que establece el proceso que ejecuta ese componente. Se puede establecer este atributo de manera que algunos componentes se ejecutan en su propio proceso y otros lo comparten. Incluso también es posible configurar componentes de aplicaciones diferentes para que compartan el mismo
154
Más información sobre Android proceso, siempre y cuando estas aplicaciones compartan el mismo ID de usuario de Linux y estén firmadas con los mismos certificados del desarrollador. Además, también podemos usar el atributo android:process dentro del elemento para definir el valor por defecto del proceso que aplica a todos sus componentes. Android podría matar un proceso en algún momento si queda poca memoria en el teléfono y el usuario solicita que se ejecuten nuevos procesos al iniciar otra aplicación. Los componentes de la aplicación del proceso destruido desaparecen y Android debe volver a crear el proceso si la aplicación se reinicia. Cuando el sistema Android necesita matar algún proceso para liberar memoria para otro, tiene en cuenta su importancia en relación con el usuario. Por ejemplo, se termina antes un proceso que alberga actividades que ya no son visibles en la pantalla que otro donde sus actividades son visibles. Por lo tanto, la decisión de terminar un proceso depende del estado de los componentes que se ejecutan en ese proceso. Las reglas que emplea Android para acabar con un proceso se discuten a continuación.
3.3.2
Ciclo de vida de los procesos El sistema operativo Android trata siempre de mantener el proceso de una
aplicación el mayor tiempo posible en memoria. Sin embargo, a veces, es necesario eliminar procesos antiguos para liberar memoria e iniciar nuevos
procesos. Para determinar qué
procesos se deben eliminar, el sistema ordena jerárquicamente por importancia cada proceso basándose en qué componentes se están ejecutando dentro de él y su estado. Los procesos de menor importancia se eliminan primero, después los de importancia media y así sucesivamente. Hay cinco niveles en la jerarquía de importancia. La siguiente lista muestra los diferentes tipos de procesos por orden de importancia (el primer proceso es más importante y se destruye el último): 1. Proceso en primer plano (Foreground): es un proceso en el que se ejecutan componentes que está usando el usuario en ese momento. Por lo general, sólo hay unos pocos procesos en primer plano en un momento dado. Sólo se eliminan como último recurso, cuando la memoria del teléfono es tan baja que no puede continuar con la aplicación actual y la pantalla deja de hacer caso al usuario. 2. Proceso visible: es un proceso que no tiene ningún componente en primer plano, pero puede afectar a lo que el usuario ve en la pantalla. Esto puede ocurrir, por ejemplo, si la actividad en primer plano abre una ventana de diálogo que permite ver a la la actividad anterior detrás de ella.
155
3. Proceso de servicio: es un proceso que ejecuta un servicio que se ha iniciado con el método startService() y no corresponde a ninguna de las dos categorías anteriores. Aunque los procesos de servicio no están relacionados directamente con lo que ve el usuario, por lo general, desempeñan tareas importantes para el usuario, como reproducir música o descargar de datos de Internet. 4. Procesos en segundo plano (Background): es un proceso que mantiene una actividad que no se muestra al usuario (ocurre cuando Android invoca el método OnStop() de la actividad). Estos procesos no tienen un impacto directo sobre la experiencia del usuario y el sistema puede eliminarlos en cualquier momento para liberarmemoria. Por lo general, hay muchos procesos ejecutándose en segundo plano, por lo que se reordenan siguiendo el esquema "recientemente menos utilizado" (del inglés, LRU: Least Recently Used) que asegura que se el último proceso utilizado por el usuario se elimina al final. 5. Si una actividad utiliza los métodos de su ciclo de vida de forma correcta guardando su estado actual y Android decide matarla, el usuario no
notará
nada ya que Android recuperará su estado. 6. Proceso vacío: es un proceso que no tiene ningún componente de la aplicación activa. La única razón para mantener este tipo de proceso en memoria es para fines de precarga en la caché y mejorar el tiempo de arranque la próxima vez que se ejecute. Android asigna siempre a un proceso el nivel más alto en base a la importancia de sus componentes internos. Por ejemplo, si un proceso contiene un servicio y una Actividad visible, el proceso se clasifica como un proceso visible, no como un proceso de servicio. Además, la clasificación de un proceso puede aumentar debido a que otros procesos dependen de él. Por ejemplo, un proceso que da servicio a otro proceso no puede ser clasificado nunca con un rango inferior al proceso al que da servicio. Hemos visto que un proceso que ejecuta un Servicio se clasifica con mayor prioridad que un proceso con una Actividad en segundo plano. Si una actividad inicia una operación que dura mucho, es recomendable crear un servicio para esa operación, en lugar de crear un subproceso, especialmente si la operación va a durar más que la propia actividad. Por ejemplo, si una actividad tiene que subir una foto a un sitio Web, es mejor iniciar un servicio para llevar a cabo esta tarea, así la carga pueda continuar en segundo plano incluso si el usuario sale de la actividad. El uso de servicios garantiza de que la operación tendrá, por lo menos, la prioridad de un proceso de servicio, independientemente de lo que ocurra con la actividad. Por esta misma razón, los receptores de mensajes (broadcast receivers) deben emplear servicios en lugar de tareas con un hilo de ejecución.
156
Más información sobre Android 3.3.3
Hilos de ejecución en Android Cuando el usuario ejecuta una aplicación, Android crea un hilo de ejecución que se
denomina principal (main). Este hilo es muy importante porque es el encargado de gestionar los eventos que dispara el usuario a los componentes adecuados e incluye también los eventos que dibujan la pantalla. Por esta razón, a este hilo principal también se le llama hilo de la interfaz de usuario (UI thread). Este modelo de ejecución de las aplicaciones se denomina Modelo de ejecución de un único hilo (en inglés, Single Thread Model). Android no crea un subproceso independiente para cada componente de la aplicación. Todos sus componentes se ejecutan dentro del mismo proceso en el hilo principal. Por lo tanto, los métodos que responden a eventos del sistema, como onKeyDown(), siempre se ejecutan en este hilo principal o de la interfaz de usuario. Por ejemplo, cuando el usuario toca con el dedo un botón de la pantalla del teléfono, el hilo principal invoca el evento correspondiente en el widget apropiado y lo redibuja. Si un usuario interacciona mucho con una aplicación, este modelo de ejecución con un único hilo puede dar lugar a poca fluidez en ella a menos que implementemos adecuadamente la aplicación. Es decir, como todo lo que ejecuta la aplicación se hace en un único hilo principal, llevar a cabo operaciones que van a tardar cierto tiempo, como acceder a Internet o consultar una base de datos, bloquearán la interfaz de usuario. Cuando el hilo principal está bloqueado la pantalla de aplicación se bloquea (ni siquiera se dibuja) y, desde el punto de vista del usuario, la aplicación se bloquea. Además, si el hilo principal está bloqueado durante 5 segundos, Android muestra al usuario una ventana con el mensaje "La aplicación no responde" (en inglés, ANR: Application Not Responding):
Además, la interfaz de usuario de Android no es thread-safe. Una función o método thread-safe puede ser invocado por múltiples hilos de ejecución sin preocuparnos de que los 157
datos a los que accede dicha función (o método) sean corrompidos por alguno de los hilos ya que se asegura la atomicidad de la operación, es decir, la función se ejecuta de forma serializada sin interrupciones Es decir, en Android se puede manipular la interfaz de usuario desde otro subproceso Por lo tanto, hay tener en cuenta dos reglas a la hora de diseñar aplicaciones en Android: 1. No bloquear nunca el hilo principal o de la interfaz de usuario. 2. No acceder a la interfaz de usuario de Android desde un hilo exterior.
3.4
HILOS EN SEGUNDO PLANO
Debido al Modelo de hilo único explicado anteriormente, es vital que no se bloquee el hilo principal (o de interfaz de usuario) para que la interfaz de usuario de la aplicación responda a la interacción del usuario. Si la aplicación tiene que realizar operaciones que no son instantáneas, hay que ejecutarlas en hilos separados en segundo plano. Por ejemplo, a continuación se muestra el código para que se descargue de Internet una imagen cuando el usuario hace clic en un botón usando un hilo separado en segundo plano: public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://pag.es/imagen.png"); miImageView.setImageBitmap(b); } }).start(); }
En este ejemplo hemos usado el estándar de hilos de Java, que se define en el paquete "java.util.concurrent", para iniciar operaciones en segundo plano. A primera vista, este código parece funcionar bien ya que crea un nuevo hilo en segundo plano para descargar la imagen. Sin embargo, no cumple la segunda regla del modelo de un hilo único: no acceder a la interfaz de usuario de Android desde un hilo exterior a esta interfaz de usuario. Esto puede producir un comportamiento inesperado en la aplicación y muy difícil de localizar. Para solucionar este inconveniente, Android ofrece varias formas de acceder al hilo de la interfaz de usuario desde otros hilos en segundo plano. A continuación se muestra la lista de estos métodos:
158
Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long)
Más información sobre Android Por
ejemplo,
para
arreglar
el
ejemplo
anterior
podemos
usar
el
método
View.post(Runnable):
public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap b=loadImageFromNetwork("http://pag.es/imagen.png"); miImageView.post(new Runnable() { public void run() { miImageView.setImageBitmap(b); } }); } }).start(); }
Ahora el código de la aplicación es correcto y la operación se descarga de la imagen se realiza en un hilo en segundo plano, mientras que el componente de tipo ImageView se manipula desde el hilo de la interfaz de usuario. Sin embargo, si aumenta la complejidad de la operación que la aplicación debe realizar, este tipo de codificación puede ser complicada y difícil de mantener por el programador. Para gestionar interacciones más complejas con hilos en segundo plano, es mejor usa un controlador (Handler) en este hilo de sendo plano para procesar los mensajes que se envía al hilo principal de la interfaz de usuario. La mejor solución es extender la clase AsyncTask que simplifica la ejecución de las operaciones en segundo plano.
3.4.1
Utilización de tareas asíncronas con la clase AsyncTask La clase AsyncTask permite realizar operaciones asíncronas en la interfaz de usuario.
Lleva a cabo las operaciones que bloquean la pantalla al usuario en un proceso de segundo plano y devuelve su resultado al hilo principal de la interfaz de usuario de manera sencilla. Para usar esta funcionalidad de Android hay que extender la clase AsyncTask e implementar los siguientes métodos callback:
doInBackground(): inicia los procesos en segundo plano.
onPostExecute(): actualiza la interfaz de usuario desde el hilo principal de la interfaz de usuario. Así, para implementar el ejemplo anterior usando la clase AsyncTask escribimos:
159
// Método asociado al botón "Descargar" public void descargarImagen(View view) { imagen.setVisibility(View.INVISIBLE); cargando.setVisibility(View.VISIBLE);
// Iniciamos la tarea de descarga
TareaDescargaImagen tarea = new TareaDescargaImagen(); tarea.execute(imagenURL);
} // Clase que descarga una imagen de Internet como una tarea asíncrona. // Es decir, podemos seguir usando la interfaz de usuario. private class TareaDescargaImagen extends AsyncTask {
// Método que se ejecuta en segundo plano protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); }
// Cuando la tarea ha acabado se invoca automáticamente este método protected void onPostExecute(Bitmap resultado) { cargando.setVisibility(View.INVISIBLE);
// Cargamos la imagen su se ha podido descargar la imagen de Internet if (resultado!=null)
imagen.setImageBitmap(resultado);
else imagen.setImageResource(R.drawable.error); imagen.setVisibility(View.VISIBLE);
} // end onPostExecute }
Fíjate que para iniciar la tarea hemos usado el método execute().
Para definir la tarea asíncrona hemos extendido la clase así: AsyncTask
El primer parámetro (String) define el tipo de variable que usamos como parámetro al invocar doInBackground(). El segundo parámetro (Void) define el tipo de variable que pasamos como parámetro al invocar onProgressUpdate(). Finalmente, el último parámetro (Bitmap) define el tipo de variable que pasamos como parámetro al invocar onPostExecute()
160
Más información sobre Android Aunque más adelante en el curso veremos cómo se indican los permisos que necesita una aplicación para ejecutarse, para que este ejemplo funcione y se descargue una imagen de Internet es necesario indicar el permiso en el fichero AndroidManifest.xml de la aplicación así:
Es recomendable que el alumno o alumna lea el manual de la clase AsyncTask para conocer más funcionalidad, entre ella se encuentra:
Es posible llamar al método publishProgress() dentro de doInBackground() para invocar onProgressUpdate() y actualizar la pantalla del usuario para que vea cómo progresa la operación.
Es posible cancelar la operación en cualquier momento.
Desde Eclipse puedes abrirse el proyecto Ejemplo 2 (Hilos) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que usamos un proceso en segundo plano
Para entender mejor esta aplicación que usa un proceso en segundo plano, inicia este ejemplo en el emulador. Verás la pantalla siguiente:
161
Si pulsas en el botón "Descargar" comprobarás que puedes seguir usando el resto de Vistas de la pantalla mientras se descarga la imagen. Atención: Puede ocurrir un problema al utilizar un proceso en segundo plano cuando se produce un reinicio inesperado de la Actividad debido a un cambio de configuración en tiempo de ejecución, por ejemplo, cuando el usuario cambia la orientación de la pantalla. Esto puede destruir el proceso en segundo plano y que la operación no finalice correctamente. Además, es posible acceder a un hilo en segundo plano desde el proceso de otra aplicación, para ello hay que desarrollar este hilo de manera thread-safe. Estas funcionalidades forman parte de la programación avanzada en Android y no se describe su uso en este curso.
3.5
MENÚS DE ANDROID
En informática un Menú contiene una serie de opciones que el usuario puede elegir para realizar una determinada tarea. En las aplicaciones de Android podemos utilizar tres tipos de menús diferentes:
Menús Principales: son los usados con más frecuencia. Aparecen en la zona inferior de la pantalla al pulsar el botón Menú
del teléfono.
Submenús: son los menús secundarios que aparecen al elegir una opción de un menú principal.
Menús Contextuales: son muy útiles y se muestran al realizar una pulsación larga sobre algún elemento de la pantalla. Es el equivalente al botón derecho del ratón en un PC. Como es habitual en Android, existen dos formas de crear un menú en una aplicación
Android: definiendo el menú en un fichero XML e "inflándolo" después o creando el menú directamente mediante código Java. En este apartado veremos ambas formas.
3.5.1
Ejemplo de Menú Principal y Submenú Veamos en primer lugar cómo crear un menú principal con un submenú a partir de su
diseño en XML. Estos ficheros XML con el diseño del menú se deben guardar en la carpeta res\menu del proyecto y tienen una estructura de este tipo (archivo menu_principal.xml):
162
Más información sobre Android
-
Podemos ver en el código anterior que la estructura básica del diseño del menú es muy sencilla. Aparece un elemento principal que contiene los elementos - que corresponden con las diferentes opciones del menú. Estos elementos
- tienen a su vez varias propiedades básicas, como su ID (atributo android:id), su texto (atributo android:title) y su icono (atributo android:icon). Los iconos utilizados deben guardarse en las carpetas res\drawable-... del proyecto. Además, hemos definido un submenú (menú secundario) que se muestra al pulsar la opción 3 del un menú principal. Los submenús en Android se muestran en una lista emergente cuyo título contiene el texto de la opción elegida del menú principal. Este submenú tiene dos nuevas opciones secundarias. Para ello, hemos añadido un nuevo elemento
dentro del - correspondiente a la opción 3. De igual forma que otros archivos XML de un proyecto Android, podemos editarlo visualmente haciendo clic en la pestaña “Layout” del archivo que define el menú:
163
Una vez definido el menú en el fichero XML, hay que implementar el método onCreateOptionsMenu() de la Actividad para que se cree en la pantalla. En este método debemos “inflar” el menú de forma parecida a como ya hemos hecho con otro tipo de componentes layouts. Primero obtenemos una referencia al objeto "inflador" mediante el método getMenuInflater() y, después, generamos la estructura del menú usando el método inflate() y pasándole como parámetro el ID del archivo XML de diseño del menú. Finalmente, el método debe devolver el valor true para indicar a la Actividad que debe mostrar el menú.
@Override public boolean onCreateOptionsMenu(Menu menu) {
//Forma 1: definimos el menú inflando el fichero XML con su diseño MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_principal, menu); return true; }
A continuación, vamos a ver cómo implementar el diseño del menú programándolo con sentencias Java. Para ello, redefinimos el método onCreateOptionsMenu() añadiendo las opciones del menú mediante el método add() del objeto Menu, que es un parámetro del primer método. Este método add() se invoca con cuatro parámetros:
164
Más información sobre Android
ID del grupo asociado a la opción: veremos qué es esto en el siguiente ejemplo con un menú contextual, por lo que establecemos el valor Menu.NONE.
ID único para la opción: declaramos unas constantes de la clase.
Orden de la opción: como no queremos indicar ninguno, utilizamos Menu.NONE.
Texto de la opción: texto que aparece en el menú. El icono de cada opción lo establecemos mediante el método setIcon() pasándole el ID
del recurso. Veamos cómo queda el código utilizando esta otra forma de implementarlo que genera un menú exactamente igual al del ejemplo anterior:
private static final int MENU_OP1 = 1; private static final int MENU_OP2 = 2; private static final int MENU_OP3 = 3; private static final int SMENU_OP1 = 31; private static final int SMENU_OP2 = 32; ... @Override public boolean onCreateOptionsMenu(Menu menu) { // Forma 2: definimos el menú con sentencias Java menu.add(Menu.NONE, MENU_OP1, Menu.NONE, "Opción 1") .setIcon(R.drawable.menu_estrella); menu.add(Menu.NONE, MENU_OP1, Menu.NONE, "Opción 2") .setIcon(R.drawable.menu_brujula); SubMenu submenu = menu.addSubMenu(Menu.NONE, MENU_OP1, Menu.NONE, "Opción 3").setIcon(R.drawable.menu_direccion); submenu.add(Menu.NONE, SMENU_OP1, Menu.NONE, "Opción 3.1"); submenu.add(Menu.NONE, SMENU_OP2, Menu.NONE, "Opción 3.2"); return true; }
Para añadir el submenú a la opción 3 utilizamos el método addSubMenu() en lugar de add() y guardamos una referencia al submenú donde insertamos las dos nuevas opciones utilizando una vez más el método add(). Una vez construido el menú, es necesario implementar las sentencias que se ejecutan cuando el usuario selecciona una de las opciones, Para ello, usamos el evento
165
onOptionsItemSelected() de la Actividad. Este evento recibe como parámetro el elemento de menú (MenuItem) que ha sido elegido por el usuario y cuyo ID podemos obtener con el método getItemId(). En función de este ID podemos saber qué opción ha sido pulsada y ejecutar unas sentencias u otras. En nuestro ejemplo, lo único que hacemos es modificar el texto de la etiqueta labelResultado que hemos colocado en la pantalla principal de la aplicación:
// Si el usuario selecciona // seleccionada en la etiqueta
una
opción
del
menú
mostramos
la
opción
public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.MenuOp1: labelResultado.setText("Has pulsado la opción 1"); return true; case R.id.MenuOp2: labelResultado.setText("Has pulsado la opción 2"); return true; case R.id.MenuOp3: labelResultado.setText("Has pulsado la opción 3"); return true; case R.id.SubMenuOp1: labelResultado.setText("Has pulsado la opción 3.1"); return true; case R.id.SubMenuOp2: labelResultado.setText("Has pulsado la opción 3.2"); return true; default: return super.onOptionsItemSelected(item); } } // end onOptionsItemSelected
Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Menús) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que usamos un menú principal y un submenú,
166
Más información sobre Android
Para ver cómo funciona esta aplicación que usa un menú y un submenú inicia este ejemplo en el emulador. Verás la pantalla siguiente:
3.5.2
Ejemplo de Menú Contextual Los menús contextuales siempre están asociados a un componente en concreto de la
pantalla y se muestra cuando el usuario lo pulsa un rato. Normalmente, se suele mostrar opciones específicas para el elemento pulsado. Por ejemplo, en un componente de tipo lista podríamos tener un menú contextual que aparezca al pulsar sobre un elemento en concreto de la lista y que permita editar su texto o eliminarlo de la lista. La creación y utilización de este tipo de menús contextuales son muy parecidas a las de los menús y submenús básicos que hemos visto anteriormente, aunque presentan algunas particularidades que vamos a tratar a continuación. Vamos a partir del ejemplo 4 de esta Unidad, al que vamos a añadir un menú contextual que aparece al pulsar sobre la etiqueta de texto donde mostramos la opción seleccionada y un ListView con elementos sobre los que pulsar seguidamente y mostrar opciones de edición. Lo primero que debemos hacer es indicar en el método onCreate() de la Actividad que la etiqueta y el listado tienen asociado un menú contextual. Esto se hace usando la función registerForContextMenu():
167
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
//Obtenemos las referencias a los componentes
labelResultado = (TextView)findViewById(R.id.labelResultado); listadoPrincipal = (ListView)findViewById(R.id.ListadoPrincipal);
//Rellenamos la lista con datos de ejemplo
adaptador = new ArrayAdapter(this, android.R.layout.simple_list_item_1, datos); listadoPrincipal.setAdapter(adaptador);
//Asociamos los menús contextuales a los componentes
registerForContextMenu(labelResultado);
registerForContextMenu(listadoPrincipal); } // end onCreate
A continuación, de igual forma que hicimos con los menús básicos para crear las opciones disponibles con el método onCreateOptionsMenu(), vamos a construir los menús contextuales asociados a los diferentes componentes de la aplicación con el método onCreateContextMenu(). A diferencia del método onCreateOptionsMenu() Android invoca este método cada vez que es necesario mostrar un menú contextual. Este método lo implementaremos de misma forma que los menús básicos, inflándolo con un archivo de diseño XML o creándolo con sentencias Java. En este ejemplo hemos decidido diseñar los menús en XML. El menú contextual que aparece en la etiqueta se define en el fichero menu_context_etiqueta.xml:
168
Más información sobre Android El menú contextual que aparece en el ListView se define en el fichero menu_context_lista.xml:
Para implementar el método onCreateContextMenu() hay que tener en cuenta que definimos varios menús contextuales en la misma Actividad, por lo que hay que construir un menú distinto dependiendo del componente asociado. Para hacerlo, obtenemos el ID del componente al que se va a ir asociado el menú contextual, que se recibe en el parámetro (View v) del método onCreateContextMenu() utilizando el método getId() de dicho parámetro:
// Método donde definimos el menú contextual cuando se despliega public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); //Inflador del menú contextual MenuInflater inflater = getMenuInflater(); // Si el componente que vamos a dibujar es la etiqueta usamos // el fichero XML correspondiente if(v.getId() == R.id.labelResultado) inflater.inflate(R.menu.menu_context_etiqueta, menu); // Si el componente que vamos a dibujar es el ListView usamos // el fichero XML correspondiente else if(v.getId() == R.id.ListadoPrincipal) { AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo;
169
// Definimos la cabecera del menú contextual menu.setHeaderTitle( listadoPrincipal.getAdapter().getItem(info.position).toString()); // Inflamos el menú contextual inflater.inflate(R.menu.menu_context_lista, menu); } }
En el caso del menú contextual para el listado hemos personalizado el título del menú contextual mediante el método setHeaderTitle(), para que muestre el texto del elemento seleccionado en el listado. Para hacer esto es necesario conocer la posición del elemento seleccionado en el listado mediante el último parámetro menuInfo. Este parámetro contiene información adicional del componente sobre el que el usuario ha pulsado para mostrar el menú contextual. En este caso en particular del componente ListView contiene la posición del elemento pulsado. Para obtenerlo, hacemos un cambio de formato (typecasting) del parámetro menuInfo a un objeto del tipo AdapterContextMenuInfo y accedemos a su propiedad position. Por último, para implementar las acciones que hay que ejecutar cuando el usuario selecciona una opción determinada del menú contextual vamos a implementar el método onContextItemSelected() de manera similar a cómo hacíamos con onOptionsItemSelected() para los menús básicos:
// Si el usuario selecciona una opción del menú contextual mostramos // la opción seleccionada en la etiqueta public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
switch (item.getItemId()) { // Se selecciona la opción 1 de menú contextual de la etiqueta case R.id.ContextLabelOp1: labelResultado.setText("Etiqueta: Opción 1"); return true;
170
Más información sobre Android
// Se selecciona la opción 2 de menú contextual de la etiqueta case R.id.ContextLabelOp2: labelResultado.setText("Etiqueta: Opción 2"); return true;
// Se selecciona la opción "Editar texto opción" de menú contextual // de la etiqueta case R.id.EditTextOp: labelResultado.setText("Opción " + info.position + " listado: Cambio de texto"); // Cambiamos el contenido de la matriz de datos datos[info.position]="Opción "+info.position+" listado modificado"; // Reiniciamos el adaptador para que recargue los datos y // actualice el ListBox adaptador.notifyDataSetChanged(); return true; // Se selecciona la opción "Reiniciar texto opción" de menú // contextual de la etiqueta case R.id.ReiniciaTextOp: labelResultado.setText("Opción " + info.position + " listado: Reinicio texto"); datos[info.position]="Opción "+info.position+" listado"; adaptador.notifyDataSetChanged(); return true; default: return super.onContextItemSelected(item); } }
Fíjate en el código anterior que se puede mantener pulsado el dedo sobre la etiqueta azul o sobre una opción del listado y seleccionar una de las opciones del menú contextual.
En el código anterior también hemos
utilizado
la
información
del objeto
AdapterContextMenuInfo para saber qué elemento de la lista se ha pulsado aunque, en esta
171
ocasión, lo obtenemos llamando al método getMenuInfo() de la opción de menú MenuItem recibida como parámetro. Además,
para
modificar
la
opción
del
listado
hemos
usado
el
método
notifyDataSetChanged() del adaptador para que se recarguen los elementos del listado una vez hemos modificado el texto de uno de ellos.
Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Menú contextual) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que usamos un menú contextua.
Para ver cómo funciona esta aplicación que usa un menú contextual inicia este ejemplo en el emulador. Verás la pantalla siguiente:
3.5.3
Ventanas de diálogo en Android El programador utiliza a menudo pequeñas ventanas de diálogo que para que el
usuario tome una decisión o reciba un mensaje informativo. A diferencia de una ventana completa, estas pequeñas ventanas de diálogo tienen la particularidad de que pueden aparecer flotando sobre la pantalla de fondo, que queda inactiva. Android define cuatro tipos diferentes de ventanas de diálogo: 172
Más información sobre Android 1. AlertDialog: puede contener hasta tres botones (incluso ninguno) o mostrar una lista de elementos seleccionables como CheckBox o RadioButton. Es recomendable utilizar este tipo de ventana de diálogo en la mayoría de las aplicaciones. Se hereda de la clase Dialog de Android. 2. ProgressDialog: muestra una barra de progreso o la típica rueda de progreso. Se hereda de la clase AlertDialog y, por lo tanto, también se pueden incluir botones. 3. DatePickerDialog: este diálogo permite seleccionar una fecha. 4. TimePickerDialog: este diálogo permite seleccionar una hora. Una ventana de diálogo siempre se crea y se muestra como parte de una Actividad. Normalmente debemos crear la ventana de diálogo en el método onCreateDialog() de la Actividad para asociarla a ésta. En el caso de que creemos una ventana de diálogo fuera del método anterior, debemos indicar qué Actividad es la que alberga la ventana de diálogo mediante setOwnerActivity(Activity). Para mostrar un diálogo desde cualquier parte del código, hay que usar el método showDialog() indicando como parámetro un entero único en la actividad que identifica el diálogo que queremos mostrar. En el ejemplo 5 de esta unidad definimos estos identificadores así:
private static final int DIALOGO_MENSAJE = 1; private static final int DIALOGO_DOS_BOTONES = 2; private static final int DIALOGO_TRES_BOTONES = 3; ...
Desde Eclipse puedes abrir el proyecto Ejemplo 5 (Ventanas de diálogo) de la Unidad 3. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que usamos varios tipos de ventanas de diálogo
A continuación, explicamos algunos tipos de ventanas de diálogo que aparecen en el ejemplo 5:
173
3.5.3.1
Ventanas de diálogo con mensaje
Es una ventana de diálogo que obliga a que el usuario vea un mensaje bloqueando la pantalla hasta que pulse la tecla volver
. Sus métodos más importantes son éstos:
setTitle(): establece el título de la ventana de diálogo.
setMessage(): indica el mensaje que contiene la ventana de diálogo.
setIcon():establece la propiedad Icon con una de las imágenes predefinidas del sistema.
show(): muestra la ventana de diálogo.
Si hacemos clic sobre el primer botón del ejemplo del curso, veremos que aparece el siguiente mensaje:
El código fuente es el siguiente: ventana = new AlertDialog.Builder(this); ventana.setTitle("Atención"); ventana.setMessage("Tienes un mensaje nuevo. Pulsa el botón Atrás para volver a la pantalla principal."); ventana.setIcon(android.R.drawable.ic_dialog_email); ventana.show();
3.5.3.2
Ventanas de diálogo con botones
En esta ventana, a diferencia de la anterior, se obliga al usuario a pulsar uno de los botones creados mediante los métodos setPositiveButton(), setNegativeButton() o setNeutralButton(). Además, con el método setCancelable() podemos inhabilitar la tecla de escape, para evitar que el usuario cierre la ventana sin tomar una decisión. Dispone además, de los mismos métodos de la ventana anterior.
174
Más información sobre Android Si hacemos clic sobre el segundo o el tercer botón del ejemplo del curso, veremos que aparecen los siguientes mensajes:
El código fuente es el siguiente: ventana = new AlertDialog.Builder(this); ventana.setIcon(android.R.drawable.ic_dialog_info); ventana.setTitle("Encuesta"); ventana.setMessage("¿Te gusta la música clásica?"); ventana.setCancelable(false); ventana.setPositiveButton("Sí", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { /* Sentencias si el usuario pulsa Sí */ Toast.makeText(getApplicationContext(),
"Has
pulsado
el
botón
'Sí'",
1).show(); } }); ventana.setNegativeButton("No", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { /* Sentencias si el usuario pulsa No */ Toast.makeText(getApplicationContext(), "Has pulsado el botón 'No'",1).show(); } }); ventana.setNeutralButton("A veces", new
DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int boton) { /* Sentencias si el usuario pulsa A veces */ Toast.makeText(getApplicationContext(),
"Has
pulsado
el
botón
'A
veces'", 1).show(); }
175
}); ventana.show();
3.5.3.3
Ventanas de diálogo con selección
Esta ventana permite al usuario seleccionar una única opción de un listado de RadioButton. Dispone de los métodos vistos anteriormente, salvo setMessage(), ya que vamos a usar el método setSingleChoiceItems() para definir las posibles opciones del listado. Si hacemos clic sobre el sexto botón del ejemplo del curso, veremos que aparece el siguiente mensaje:
El código fuente es el siguiente: ventana = new AlertDialog.Builder(this); ventana.setIcon(android.R.drawable.ic_dialog_info); ventana.setTitle("Selecciona un modelo de teléfono");
// ¡¡ No se puede incluir un mensaje dentro de este tipo de diálogo!!! final CharSequence[] telefonos = {"iPhone", "Nokia", "Android"}; ventana.setSingleChoiceItems(telefonos, 0, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int opcion) { // Evento que ocurre cuando el usuario selecciona una opción Toast.makeText(getApplicationContext(), "Has seleccionado " +telefonos[opcion], Toast.LENGTH_SHORT).show(); } });
176
Más información sobre Android ventana.setPositiveButton("Aceptar", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { Toast.makeText(getApplicationContext(), "Has pulsado el botón 'Aceptar'", 1).show(); } }); ventana.setNegativeButton("Cancelar", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { Toast.makeText(getApplicationContext(), "Has pulsado el botón 'Cancelar'", 1).show(); } }); ventana.show();
3.5.3.4
Ventanas de diálogo personalizada
Esta ventana permite al programador diseñar mediante archivos XML la ventana de diálogo que queremos mostrar al usuario. Dispone de los métodos vistos anteriormente, salvo setMessage(), ya que vamos a usar el método setView() para definir el contenido de la ventana. Si hacemos clic sobre el octavo botón del ejemplo del curso, veremos que aparece el siguiente mensaje:
El código fuente es el siguiente: // Primero preparamos el interior de la ventana de diálogo inflando su fichero XML String infService = Context.LAYOUT_INFLATER_SERVICE;
177
LayoutInflater li =(LayoutInflater)getApplicationContext().getSystemService(infService); // Inflamos el componente compuesto definido en el XML View inflador = li.inflate(R.layout.dialogo_entrada_texto, null); // Buscamos los componentes dentro del Diálogo final TextView nombreEdit = (TextView) inflador.findViewById(R.id.nombre_edit); final TextView passwordEdit = (TextView) inflador.findViewById(R.id.password_edit); ventana = new AlertDialog.Builder(this); ventana.setTitle("Indica usuario y contraseña"); // Asignamos previamente)
el
contenido
dentro
del
diálogo
(el
que
hemos
inflado
ventana.setView(inflador); // ¡¡ No se puede incluir un mensaje dentro de este tipo de diálogo!!! ventana.setPositiveButton("Aceptar", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { Toast.makeText(getApplicationContext(), "Has escrito el nombre '"+ nombreEdit.getText().toString() +"', la contraseña '"+ passwordEdit.getText().toString() +"' y has pulsado el botón 'Aceptar'", 1).show(); } }); ventana.setNegativeButton("Cancelar", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int boton) { Toast.makeText(getApplicationContext(), "Has pulsado el botón 'Cancelar'", 1).show(); } }); ventana.show();
Nota: es muy importante no usar el método setMessage() cuando usemos ventanas de diálogo cuyo interior se define mediante listas de selección o un archivo de diseño XML, ya que no se mostrará bien la ventana si lo hacemos.
En el Ejemplo 5 de esta Unidad puedes ver otros tipos de ventanas de diálogo muy
178
Más información sobre Android similares a las estudiadas en este apartado. Recomendamos al alumno o alumna que estudie a fondo el código fuente de este ejemplo.
179
Una aplicación puede estar compuesta de una o varias Actividades. Una Tarea es un conjunto de actividades con las que un usuario interactúa. Android organiza las actividades en una pila de ejecución (en inglés stack) donde se van apilando las actividades que el usuario invoca. Es posible mantener muchas tareas en segundo plano a la vez; sin embargo, si el sistema operativo necesita memoria, puede destruirlas perdiendo sus estados de ejecución. La configuración de un dispositivo Android puede cambiar mientras se ejecuta una aplicación; por ejemplo, pueden cambiar la orientación de la pantalla (vertical/horizontal), la disponibilidad de teclado, el idioma, etcétera. Cuando esto ocurre la Actividad se destruye. Para almacenar la información importante sobre el estado de ejecución de una Actividad podemos usar el método de llamada onSaveInstanceState() y después restaurar esta información cuando el sistema vuelva a crear la Actividad. No
hay
garantías
de
que
Android
invoque
siempre
el
método
onSaveInstanceState() justo antes de que la Actividad se destruya, ya que hay casos en los que no es necesario guardar el estado de ejecución; por ejemplo, cuando el usuario sale de la aplicación usando la tecla “Volver atrás. Debido a que no se garantiza que Android invoque siempre el método onSaveInstanceState(), debe usarse únicamente para almacenar el estado transitorio de la Actividad, es decir, la interfaz de usuario. Nunca debe emplearse para almacenar datos persistentes con preferencias del usuario o datos de la aplicación. Un hilo es una tarea que se ejecuta en paralelo con otra tarea. Todos los componentes de una misma aplicación Android se ejecutan en el mismo hilo principal (en inglés se denomina "main thread"). A este hilo también se lo denomina hilo de la interfaz de usuario (en inglés "UI thread").
180
El sistema operativo Android sigue el modelo de ejecución de aplicaciones denominado Modelo de ejecución de un único hilo (en inglés Single Thread Model). Hay tener en cuenta dos reglas cuando se diseñan aplicaciones en Android: 1. No bloquear nunca el hilo principal ni el de la interfaz de usuario. 2. No acceder a la interfaz de usuario de Android desde un hilo exterior.
Si una aplicación Android tiene que realizar operaciones que no son instantáneas y llevan cierto tiempo, hay que ejecutarlas en hilos separados en segundo plano. Un Menú es una serie de opciones que el usuario puede elegir para realizar una determinada tarea. En las aplicaciones Android podemos utilizar tres tipos de menús diferentes: 1. Menús Principales: aparecen en la zona inferior de la pantalla al pulsar el botón Menú del teléfono. 2. Submenús: menús secundarios que aparecen al elegir una opción de un menú principal. 3. Menús Contextuales: se muestran al realizar una pulsación larga sobre algún elemento de la pantalla. Es el equivalente al botón derecho del ratón en un PC. Como es habitual en Android, existen dos formas de crear un menú en una aplicación Android: mediante un fichero XML e "inflándolo" después o creando el menú directamente mediante código Java. Las ventanas de diálogo son pequeñas ventanas que el programador utiliza a menudo para que el usuario tome una decisión o reciba un mensaje informativo.
181
Unidad de Aprendizaje 9
EFECTOS TRABAJANDO DE TRANSICIÓN CON Y FICHEROS ANIMACIÓN
ÍNDICE 4.1 FICHEROS EN ANDROID .......................................................... 185 4.1.1 Introducción ..................................................................... 185 4.1.2 Gestión de información en Android ................................. 185 4.1.3 Gestión del sistema de archivos en Android ................... 185 4.1.4 Clase Fichero File ............................................................. 186 4.1.4.1 Constructores más importantes .............................. 186 4.1.4.2 Métodos más importantes....................................... 187 4.1.5 Ficheros en la memoria interna del diapositivo ............... 188 4.1.6 Fichero de recurso de la aplicación ................................. 190 4.1.7 Fichero en almacenamiento externo................................ 191 4.1.8 Añadir datos a un fichero ................................................. 196 4.1.9 Gestionando las excepciones en la gestión de ficheros . 196 4.2 PREFERENCIAS DE UNA APLICACIÓN ANDROID ................. 197 4.2.1 Preferencia de las aplicaciones Android.......................... 197 4.2.2 Pantallas de opciones ...................................................... 200 4.3 RECURSOS DE LAS APLICACIONES ANDROID ..................... 207 4.3.1 Tipos de recursos............................................................. 208 4.3.2 Crear literales en ficheros de recursos con Eclipse......... 208 4.3.3 Recursos de tipo Cadena (String Resources) .................. 209 4.3.4.1 Cadena (String) ....................................................... 210 4.3.4.2 Matriz de cadenas (String Array)............................. 211 4.3.4.3 Cadenas de cantidad (Quantity Strings) ................. 212 4.3.4 Formatear y cambiar el estilo de las cadenas de recursos213 4.3.4.1 Escapando comillas simples y dobles.................... 213 4.3.4.2 Formatear cadenas de texto................................... 214 4.3.4.3 Cambio de estilo ..................................................... 214 4.4 ACCESO A INTERNET CON ANDROID ..................................... 215 4.4.1 Ejemplo de conexión a Internet........................................ 216 4.4.2 Conexión a través de proxy ............................................. 220 4.5 QUÉ SON JSON (JavaScript Object Notation? ........................ 221 4.5.1 Cómo usar JSON en nuestras aplicaciones a Android.... 222 4.5.2 Cómo escribir ficheros en formato JSON ........................ 225
4.1
4.1.1
FICHEROS EN ANDROID
Introducción En esta Unidad vamos a explicar cómo gestionar ficheros en Android, tanto en la
memoria interna del teléfono como en su tarjeta de memoria externa SD. Además, veremos cómo almacenar las preferencias de una Aplicación de Android. También detallaremos el uso de ficheros JSON en Android. Finalmente, accederemos a Internet desde aplicaciones Android y veremos lo que son los Recursos (Resources).
4.1.2
Gestión de información en Android En Android existen tres formas de almacenar información, para poder usarla en las
aplicaciones:
Preferencias de la aplicación
Ficheros locales en el sistema de archivos del sistema operativo
Base de datos SQLite
En esta Unidad 4 trataremos las dos primeras formas, y en la Unidad 6 veremos las bases de datos.
4.1.3
Gestión del sistema de archivos en Android Como en cualquier sistema operativo, en Android también podemos manipular
ficheros de forma muy parecida a como se hacen en Java.
185
En Android, por defecto, los ficheros son privados y únicamente puede acceder a ellos la aplicación que los crea. Para compartir información de estos ficheros se utilizan los Content Providers que veremos en la Unidad 7. Lo primero que hay debemos tener en cuenta es dónde queremos almacenar estos ficheros y la manera en que vamos a acceder a ellos: lectura o escritura. Podemos leer y escribir ficheros localizados en: 1. La memoria interna del dispositivo: como fichero o como recurso de la aplicación. 2. La tarjeta SD externa, si existe, también denominada almacenamiento externo.
4.1.4
Clase Fichero File La clase File de Android se usa para identificar y gestionar archivos y directorios del
sistema operativo. Un archivo en Android puede estar identificado por su ruta absoluta, relativa al directorio raíz del sistema de archivos, o por su ruta relativa, que es directorio actual en el que se ejecuta la aplicación. Esta clase File está basada en la clase de Java. El acceso a los ficheros es similar al Java estándar: se deben crear inputs y outpus streams. La clase File puede hacer referencia a un archivo que ya exista o a uno que vayamos a crear. Aunque el nombre de esta clase sea File, también puede, referirse a un directorio o un enlace (link) de Linux. Esta clase proporciona una funcionalidad limitada para obtener y establecer permisos del archivo, cambiar el tipo de archivo o establecer su fecha de última modificación.
4.1.4.1
Constructores más importantes File(File dir, String nombre): crea un fichero usando el nombre y el directorio especificados como parámetros.
File(String dir): crea un fichero en el directorio especificado.
File(URI uri): crea un fichero usando el camino especificado en el parámetro URI (del inglés Uniform Resource Identifier), que es un identificador uniforme de recurso. Se trata de una cadena de caracteres corta que identifica inequívocamente un recurso (servicio, página, documento, dirección de correo electrónico, enciclopedia, etcétera.).
186
4.1.4.2
Métodos más importantes
boolean canExecute(): comprueba si la aplicación puede ejecutar el fichero.
boolean canRead(): indica si el fichero se puede leer.
boolean canWrite(): comprueba si la aplicación puede escribir datos en el fichero.
boolean createNewFile(): crea un archivo nuevo y vacío en el sistema de archivos de acuerdo con la ruta almacenada.
static File createTempFile(String prefijo, String sufijo, File directorio): crea un archivo temporal vacío en el directorio establecido usando un prefijo y un sufijo para formar el nombre del mismo.
static File createTempFile(String prefix, String suffix): crea un archivo temporal vacío usando un prefijo y un sufijo para formar el nombre del mismo.
boolean delete(): borra el fichero.
boolean exists(): indica si existe un fichero.
String getAbsolutePath(): obtiene el camino absoluto del fichero.
long getFreeSpace(): indica el número de bytes libres en la partición actual (donde se encuentra el fichero).
String getName(): devuelve el nombre del archivo o del directorio que representa al archivo.
String getPath(): devuelve el camino del fichero.
long getTotalSpace(): indica el tamaño total en bytes de la partición actual (donde se encuentra el fichero).
boolean isAbsolute(): indica si el camino del fichero es absoluto.
boolean isDirectory(): indica si la clase File contiene un directorio.
boolean isFile(): indica si la clase File contiene un archivo de datos.
boolean isHidden(): indica si el fichero es de tipo oculto en el sistema operativo.
long lastModified(): devuelve la fecha de la última modificación del archivo medido en número de milisegundos desde el 1 de enero 1970.
long length(): indica el tamaño del fichero en bytes.
String[] list(): devuelve un listado con los nombres de todos los ficheros y subdirectorios contenidos en un directorio.
File[] listFiles(): devuelve un listado de tipo File con todos los ficheros y subdirectorios contenidos en un directorio.
boolean mkdir(): crea un subdirectorio en el directorio actual.
187
4.1.5
boolean mkdirs(): crea recursivamente un subdirectorio incluyendo todos los subdirectorios superiores que completan el camino.
boolean renameTo(File nuevoNombre): renombra un fichero.
boolean setExecutable(boolean executable, boolean ownerOnly): Cambia los permisos del fichero indicando si es un ejecutable y si únicamente lo puede ejecutar el usuario que lo ha creado.
boolean setLastModified(long modificación del fichero.
boolean setReadable(boolean readable, boolean ownerOnly): cambia los permisos del fichero indicando si se puede leer y si únicamente lo puede leer el usuario que lo ha creado.
boolean setWritable(boolean writable, boolean ownerOnly): cambia los permisos del fichero indicando si se puede escribir en él y si únicamente puede escribir en el mismo el usuario que lo ha creado.
URI toURI(): devuelve el identificador URI del fichero.
time):
establece
la
última
fecha
de
Ficheros en la memoria interna del diapositivo Al almacenar archivos en la memoria interna debemos tener en cuenta las limitaciones
de espacio que tienen muchos dispositivos, por lo que no deberíamos abusar de este espacio guardando ficheros de gran tamaño. Crear ficheros en la memoria interna es muy sencillo. Android dispone del método openFileOutput(), que recibe como parámetros el nombre del fichero y el modo de acceso al mismo. Este modo de acceso puede ser: o
MODE_PRIVATE: es el valor por defecto, que únicamente permite el acceso privado desde nuestra aplicación.
o
MODE_APPEND: permite añadir datos a un fichero ya existente.
o
MODE_WORLD_READABLE: permite a otras aplicaciones leer el fichero.
o
MODE_WORLD_WRITABLE: permite a otras aplicaciones escribir en el fichero. El método openFileOutput() devuelve una referencia a un objeto de tipo stream de
Java asociado al fichero (más en concreto, se trata de un objeto FileOutputStream), a partir del cual podemos utilizar los métodos de manipulación de ficheros tradicionales del lenguaje Java. En el siguiente ejemplo convertimos este stream al tipo OutputStreamWriter para escribir una cadena de texto en el fichero: try {
188
OutputStreamWriter fileout= new OutputStreamWriter(openFileOutput("fichero_interno.txt", Context.MODE_PRIVATE)); fileout.write("En un lugar de la Mancha..."); fileout.close(); } catch (Exception excepcion) { Log.e("Fichero", "Error al escribir fichero en memoria interna"); }
Android almacena este archivo de texto en la memoria interna del dispositivo, en un directorio determinado y siempre el mismo, sin que el programador pueda cambiarlo. Sigue este patrón: /data/data/paquete_java/files/nombre_del_fichero En el ejemplo anterior se almacena en: /data/data/es.mentor.unidad4.eje1.ficheros/files/fichero_interno.txt Si ejecutamos el Ejemplo 1 de esta Unidad podemos comprobar en el DDMS cómo se crea el fichero correctamente en el directorio indicado. Para acceder a esta herramienta dentro de Eclipse, hay que hacer Clic en la opción del menú principal: Window -> Open Perspective -> DDMS:
189
Leer ficheros desde la memoria interna es igual de sencillo. Procedemos de forma análoga utilizando esta vez el método openFileInput(), para abrir el fichero y usamos los métodos de lectura mostrados anteriormente para leer el contenido. try { BufferedReader filein = new BufferedReader( new InputStreamReader(openFileInput("fichero_interno.txt")));
String texto = filein.readLine(); filein.close(); } catch (Exception ex) { Log.e("Ficheros", "Error al leer fichero de memoria interna"); }
4.1.6
Fichero de recurso de la aplicación Otra forma de almacenar ficheros en la memoria interna del dispositivo es incluirlos
como un recurso de la propia aplicación. Aunque este método es muy útil, únicamente debemos utilizarlo cuando no necesitemos realizar modificaciones sobre el fichero, ya que el acceso al mismo es de sólo lectura. Para incluir un fichero como recurso de la aplicación debemos colocarlo en la carpeta /res/raw del proyecto Android. Esta carpeta no suele estar creada por defecto, por lo que debemos crearla manualmente en Eclipse. Una vez creada la carpeta raw, podemos añadir en ella cualquier fichero que necesitemos incluir con la aplicación en tiempo de compilación en forma de recurso. En el Ejemplo 1 de esta Unidad hemos incluido el fichero de texto “prueba_raw.txt“. Posteriormente, en tiempo de ejecución, podemos acceder a este fichero, sólo en modo de lectura, de una forma similar a la que ya hemos visto anteriormente para el resto de ficheros en la memoria interna del dispositivo. Estos ficheros de tipo recurso también pueden ser binarios: por ejemplo, imágenes, vídeos, etcétera. En primer lugar, para acceder a este fichero, obtenemos los recursos de la aplicación con 190
el
método
getResources()
y
sobre
éste
utilizamos
el
método
openRawResource(id_del_recurso) para abrir el fichero en modo lectura. Este método devuelve un objeto de tipo InputStream que podemos manipular. En el ejemplo del curso, convertimos el InputStream en un objeto BufferedReader para leer el texto contenido en el fichero, tal y como haríamos en Java. A continuación, mostramos el código fuente: try { InputStream ficheroraw = getResources().openRawResource(R.raw.prueba_raw); BufferedReader brin = new BufferedReader(new InputStreamReader(ficheroraw)); while (true) { texto = brin.readLine(); // Si ya no hay más líneas que leer hemos acabado de leer el fichero if (texto==null) break; resultado.append("\n"+Html.fromHtml(texto)); } // end while ficheroraw.close(); } catch (Exception ex) {
Log.e("Ficheros", "Error al leer fichero de recurso de aplicación"); }
4.1.7
Fichero en almacenamiento externo La mayoría de los dispositivos Android disponen de una tarjeta SD externa para que el
sistema tenga un espacio extra de almacenamiento de información. Normalmente en esta tarjeta se guardan las fotos, los vídeos y, en las versiones últimas de Android, incluso se instalan aplicaciones. A diferencia de la memoria interna, la memoria externa no tiene por qué estar presente en el dispositivo o puede que el sistema no reconozca su formato. Por lo tanto, antes de usar ficheros en la memoria externa, hay que comprobar que dicha memoria está presente y disponible para leer y/o escribir en ella. Para esto, Android proporciona en la clase Environment el método estático getExternalStorageStatus(), que indica si la memoria externa está disponible y si se puede leer y escribir en ella. Este método devuelve una serie de valores que señalan el estado de la memoria externa. Entre ellos, los más importantes son los siguientes:
191
MEDIA_MOUNTED: indica si la memoria externa está disponible y es posible leer y escribir en ella.
MEDIA_MOUNTED_READ_ONLY: indica que la memoria externa está disponible, pero únicamente podemos leer información.
Otros valores que indicarán que existe algún tipo de problema y que, por lo tanto, no podemos usar la memoria externa (MEDIA_UNMOUNTED, MEDIA_REMOVED, etcétera). Se puede consultar todos estos valores en la documentación oficial de la clase Environment. Teniendo en cuenta esto, en el Ejemplo 1 realizamos comprobaciones previas del
estado de la memoria externa del dispositivo con el siguiente método:
// Método que comprueba si el almacenamiento externo está activo y si se puede escribir en la tarjeta. private void compruebaAlmacenamientoExt(){
// Obtenemos el estado del almacenamiento externo del teléfono String estado = Environment.getExternalStorageState(); // La tarjeta está activa y se puede escribir en ella if (Environment.MEDIA_MOUNTED.equals(estado)) { hayAlmacenamientoExt = almacenamientoExtEscritura = true; resultado.append("\n\nEl
teléfono dispone de almacenamiento externo conectado y se puede escribir en él.");
} else
// Sólo se puede leer el almacenamiento externo if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(estado)) { hayAlmacenamientoExt = true; almacenamientoExtEscritura = false; resultado.append("\n\nEl
teléfono dispone de almacenamiento externo conectado pero no se puede escribir en él.");
} else {
// No se puede leer el almacenamiento externo hayAlmacenamientoExt = almacenamientoExtEscritura = false; resultado.append("\n\nEl
} if (hayAlmacenamientoExt) {
192
teléfono
no
tiene
ningún almacenamiento externo conectado.");
// Mostramos el directorio donde está el almacenamiento externo
File dir = android.os.Environment.getExternalStorageDirectory(); dirAlmacExt=dir.getAbsolutePath(); resultado.append("\n\nDirectorio almacenamiento externo: "+dir);
} } // end compruebaTarjetaSD
Una vez comprobado el estado de la memoria externa y dependiendo del resultado obtenido, podemos leer o escribir en ella cualquier tipo de fichero. Hemos aprovechado el método anterior para obtener la ruta al directorio raíz de esta memoria. Para ello utilizamos el método getExternalStorageDirectory() de la clase Environment, que devuelve un objeto File con la ruta de dicho directorio. Empecemos por la funcionalidad de escritura en la memoria externa. Lo primero que hemos hecho ha sido crear el directorio ejemplo_curso_Mentor dentro de la tarjeta SD usando el método mkdirs de la clase File. Después, para escribir un fichero en ese directorio de la tarjeta SD creamos un nuevo objeto File que combina ambos elementos. Para acabar, sólo hay que encapsularlo en algún objeto de escritura de ficheros de la API de Java y escribir algún dato. En este caso lo convertimos de nuevo a un objeto OutputStreamWriter para escribir un archivo de texto. El código fuente es el siguiente: //Si la memoria externa está disponible y se puede escribir if (hayAlmacenamientoExt && almacenamientoExtEscritura) { try { // Creamos un directorio de prueba File directorio = new File (dirAlmacExt + "/ejemplo_curso_Mentor"); directorio.mkdirs(); resultado.append("- Creamos el directorio " + dirAlmacExt + "/ejemplo_curso_Mentor"); // Abrimos in fichero en el raíz de la tarjeta SD File fichero = new File(directorio, "prueba_sd.txt"); OutputStreamWriter fout = new OutputStreamWriter(new FileOutputStream(fichero)); resultado.append("\n\n- Abrimos fichero '" + dirAlmacExt + "/ejemplo_curso_Mentor/fichero_externo.txt' para escritura en memoria externa");
193
fout.write("Caminante no hay camino se hace camino al andar..."); resultado.append("\n\n- Escribimos los datos"); fout.close(); resultado.append("\n\n- Cerramos fichero"); } catch (Exception ex) { Log.e("Ficheros", "Error al escribir fichero en memoria externa"); resultado.append("Error al escribir fichero en memoria externa"); } }
else
resultado.append("No
hay
almacenamiento externo disponible o no se puede escribir en él.");
Hay que tener en cuenta que es preciso que especificar en el fichero AndroidManifest.xml que la aplicación necesita el permiso de escritura en la memoria externa. Para "manifestar" que la aplicación necesita este permiso usamos la cláusula utilizando el valor android.permission.WRITE_EXTERNAL_STORAGE. El fichero queda así:
194
Si ejecutamos ahora el Ejemplo 1 y abrimos al explorador de archivos del DDMS podemos comprobar que se ha creado correctamente el fichero en el directorio especificado de la tarjeta SD:
La forma de leer un fichero de la tarjeta SD es muy parecida a si estuviera almacenado en la memoria interna del teléfono; únicamente cambia el directorio del que leemos el fichero. El código fuente tiene este aspecto: try { File fichero = new File(dirAlmacExt + "/ejemplo_curso_Mentor", "prueba_sd.txt"); BufferedReader fin = new BufferedReader(new InputStreamReader( new FileInputStream(fichero))); resultado.append("- Abrimos fichero '"+ dirAlmacExt + "/ejemplo_curso_Mentor/fichero_externo.txt' para lectura de memoria externa"); String texto = fin.readLine(); resultado.append("\n\n- Leemos el contenido del fichero:\n"); resultado.append(texto); fin.close(); resultado.append("\n\n- Cerramos fichero");
195
} catch (Exception ex) { Log.e("Ficheros", "Error al leer fichero de memoria externa"); }
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Ficheros) de la Unidad 4. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado los métodos de manipulación de archivos
4.1.8
Añadir datos a un fichero Por defecto, Android sobrescribe siempre un fichero si éste ya existe perdiendo los
datos almacenados anteriormente. Esto ocurre en los ejemplos anteriores. Para poder añadir información a un fichero existente podemos cambiar el constructor FileOutputStream(File fichero) por el constructor FileOutputStream (File fichero, Boolean append), que permite indicar si queremos agregar la información al final del fichero con el parámetro append =true.
4.1.9
Gestionando las excepciones en la gestión de ficheros Fíjate en que todo el código fuente que gestiona el fichero está contenido dentro de un
bloque try-catch. De esta manera se controlan los errores de ejecución de las sentencias mediante el manejo de excepciones estándar de Java. Por ejemplo, Android puede lanzar las siguientes excepciones cuando ocurre un error al manipular un fichero: 1. Si el fichero indicado en el parámetro del constructor FileOutputStream(File) no existe, se lanza la excepción FileNotFoundException.
2. Si ocurre algún error al cerrar un fichero con el método close(), entonces Android lanza la excepción IOException. El programador debe gestionar esas Excepciones para poder mostrar al usuario un mensaje del sistema operativo que entienda, en lugar de en inglés. La forma estándar de hacerlo tiene el siguiente aspecto:
196
try { //
Sentencias excepciones
que
queremos
ejecutar
y
que
pueden
lanzar
. . } catch (ExceptionType excepcion1) { // Sentencias que gestiona la excepción 1 . . } catch (ExceptionType excepcion2) { // Sentencias que gestiona la excepción 2 . . }
En el caso del Ejemplo 1 del curso hemos capturamos todas las excepciones en un único bloque para mostrar al usuario y en la consola de Eclipse un mensaje sencillo de error:
catch (Exception ex) { Log.e("Ficheros", "Error al escribir fichero en memoria externa"); resultado.append("Error al escribir fichero en memoria externa");
}
4.2
4.2.1
PREFERENCIAS DE UNA APLICACIÓN ANDROID
Preferencia de las aplicaciones Android Las preferencias de una aplicación son datos que una aplicación guarda y
recupera para personalizar la experiencia del usuario. Por ejemplo, se debe almacenar información personal, configuración de la aplicación, opciones de presentación, etcétera. En el apartado anterior hemos visto cómo almacenar información en ficheros. Las preferencias de una aplicación se podrían almacenar utilizando este método, aunque Android proporciona otro método alternativo diseñado específicamente para administrar este tipo de datos: las Preferencias compartidas (shared preferences en inglés). 197
Cada preferencia se almacena siguiendo la estructura clave-valor. Es decir, cada una de ellas está compuesta por un identificador único (por ejemplo, “email”) y un valor asociado a dicho identificador (por ejemplo, “[email protected] ”). Estos datos se guardan en un fichero XML. Para gestionar las preferencias de una aplicación hay que usar la clase SharedPrefences. Una misma aplicación Android puede gestionar varias colecciones de preferencias que se diferencian por un identificador único. Para obtener la referencia a una colección determinada utilizamos el método getSharedPrefences() indicando el identificador de la colección y el modo de acceso a la misma. El modo de acceso indica qué aplicaciones tienen acceso a esta colección de preferencias y qué operaciones podrán realizar sobre ellas. Existen tres tipos de permisos principales: o
MODE_PRIVATE: únicamente la aplicación tiene acceso a las preferencias.
o
MODE_WORLD_READABLE: todas las aplicaciones pueden leer las preferencias, aunque únicamente la matriz puede modificarlas.
o
MODE_WORLD_WRITABLE: todas las aplicaciones pueden leer y modificar las preferencias. Por
ejemplo,
para
obtener
la
referencia
a
la
colección
de
preferencias
“MisPreferencias”, con modo de acceso exclusivo,para la aplicación que las gestiona, escribimos la siguiente sentencia:
SharedPreferences preferencias = getSharedPreferences("MisPreferencias",Context.MODE_PRIVATE);
Una vez hemos creado el objeto que nos permite acceder a la colección de preferencias, podemos leer, insertar o modificar claves de preferencias utilizando los métodos get() o put() correspondientes al tipo de dato de cada preferencia. Por ejemplo, para obtener el valor de la preferencia “email” de tipo String escribimos la siguiente setencia:
String correo = preferencias.getString("email", "[email protected] ");
Al método getString() le pasamos como parámetros el nombre de la preferencia que queremos leer y el valor por defecto (por si no contiene nada o no existe). A parte del método getString(), existen métodos análogos para el resto de tipos de datos básicos de Java. Por ejemplo, getInt(), getLong(), getFloat(), getBoolean(), etcétera. 198
Actualizar o añadir nuevas claves de preferencias es muy parecido. En lugar de usar el objeto SharedPreferences, utilizaremos SharedPreferences.Editor para editar preferencias. Accedemos a este objeto mediante el método edit() de la clase SharedPreferences. Una vez obtenida la referencia al editor, utilizamos los métodos put() oportunos en función del tipo de dato de cada clave de preferencia para actualizar o insertar su valor. Por ejemplo, putString(clave, valor) actualiza una preferencia de tipo String. De igual forma, existen métodos get() para todos los tipos de datos básicos de Java: putInt(), putFloat(), putBoolean(), etcétera. Para acabar, una vez actualizados/insertados todos las claves de preferencias necesarias invocamos el método commit() para confirmar los cambios. Fíjate en el siguiente ejemplo sencillo:
SharedPreferences preferencias = getSharedPreferences("MisPreferencias", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferencias.edit(); editor.putString("email", "[email protected] "); editor.putString("nombre", "Pedro del Cielo Lindo"); editor.commit();
Las preferencias se almacenan en un fichero XML dentro de un directorio con el siguiente patrón: /data/data/paquetejava/shared_prefs/nombre_coleccion.xml En este caso encontraremos el fichero de preferencias en el directorio: /data/data/es.mentor.unidad4.eje2.preferencias/shared_prefs/MisPreferencias.xml Si descargamos este fichero desde el DDMS y lo abrimos con cualquier editor de texto, veremos el contenido siguiente:
Pedro del Cielo Lindo [email protected]
199
En este fichero XML observamos cómo se almacenan las dos preferencias del ejemplo anterior con sus claves y valores correspondientes.
4.2.2
Pantallas de opciones Si abrimos cualquier pantalla de preferencias estándar de Android, veremos que todas
comparten una interfaz común, similar a la siguiente imagen (Preferencias de "Ajustes de sonido" de Android):
Si nos fijamos en la imagen anterior, vemos que las distintas opciones se organizan dentro de una pantalla de opciones que incluye varias categorías (“General”, “Llamadas entrantes“, etcétera). Dentro de cada categoría, aparecen varios tipos de opciones. Por ejemplo, de tipo CheckBox (“Modo Silencio“) o de tipo lista de selección (“Vibrar“). Android permite mejorar y simplificar la gestión de las preferencias de una aplicación mediante el uso de una pantalla de opciones. Para definir la pantalla de opciones vamos a usar un fichero de diseño (layout) XML, que guardamos en la carpeta /res/xml/pantallapreferencias.xml del proyecto. El contenedor principal de nuestra pantalla de preferencias será el elemento que aparece en el fichero opciones.xml. Este elemento representa la pantalla de opciones en sí, dentro de la cual incluiremos el resto de componentes de la interfaz de usuario. Dentro de esta pantalla podemos incluir una lista de opciones organizadas por categorías, que se representan mediante el elemento , al que 200
añadimos un texto descriptivo utilizando el atributo android:title. Dentro de cada categoría podemos incluir varias opciones. Éstas pueden ser de distintos tipos, por ejemplo:
CheckBoxPreference: caja seleccionable.
EditTextPreference: caja de texto.
ListPreference: lista con valores seleccionables (sólo uno).
MultiSelectListPreference: lista con valores varios seleccionables. A continuación, vamos a describir los diferentes atributos de estos tipos de opciones:
CheckBoxPreference
Se usa para introducir una opción de preferencia que sólo puede tener dos valores: activada (marcada) o desactivada (desmarcada). Es el equivalente al componente de tipo CheckBox. En este caso, hay que especificar los atributos: nombre interno de la opción (android:key),
texto
que
muestra
(android:title)
y
descripción
de
la
opción
(android:summary). Veamos un ejemplo:
EditTextPreference
Se utiliza para introducir una opción de preferencia que contiene una cadena de texto. Al pulsar sobre una opción de este tipo, se muestra un cuadro de diálogo sencillo que solicita al usuario un texto. Para este tipo de opción, además de los tres atributos comunes a la opción anterior, también hay que indicar el texto que aparece en el cuadro de diálogo mediante el atributo android:dialogTitle. Por ejemplo:
ListPreference
Se emplea para que el usuario seleccione una única opción de preferencia de una lista de valores predefinida. Además de los cuatro atributos anteriormente comentados, hay que 201
añadir dos más: uno para indicar la lista de valores que se visualizan en la lista, y otro para señalar los valores internos que guardaremos para cada uno de los valores de la lista anterior. Por ejemplo, al usuario podemos mostrarle una lista de buscadores con el texto “Google” y “Bing”, pero internamente almacenarlos como “www.google.es” y “www.bing.com”. Esta lista de valores la definimos en el fichero XML /res/xml/opciones.xml con los tipos de recursos necesarios. En este caso son dos: uno para la lista de valores visibles y otro para la lista de valores internos que se guardan en el fichero de preferencias. Este fichero tiene este aspecto:
- Google
- Bing
- Yahoo
- www.google.es
- www.bing.com
- www.yahoo.es
En
la
opción
de
preferencia
utilizamos
los
atributos
android:entries
android:entryValues para hacer referencia a estas listas, como vemos a continuación:
202
y
MultiSelectListPreference
Se emplea para que el usuario seleccione una o varias opciones de preferencia de una lista de valores predefinida. Los atributos que debemos establecer son, por lo tanto, los mismos que para el tipo ListPreference. Vemos un ejemplo a continuación:
A continuación, mostramos el fichero completo pantallapreferencias.xml que usamos en el Ejemplo 2 de esta Unidad:
203
android:summary="Indica el buscador por defecto" android:dialogTitle="Selecciona buscador" android:entries="@array/nombre" android:entryValues="@array/url" />
Una vez está definida la estructura de la pantalla de opciones, hay que implementar una nueva Actividad que la llamaremos cuando queramos mostrar la pantalla de preferencias, y que se encargará internamente de gestionar todas las opciones, guardarlas, modificarlas, etcétera, a partir de la definición XML. Android facilita el trabajo al programador ofreciendo la clase PreferenceActivity, que se encarga de gestionar todas las operaciones internamente. Únicamente hay que crear la nueva Actividad PantallaPreferencias que se extiende de esta clase e implementar su método onCreate() para invocar el método addPreferencesFromResource() indicando el fichero XML en el que hemos definido la pantalla de opciones. El código fuente se encuentra en el fichero PantallaPreferencias.java:
public class PantallaPreferencias extends PreferenceActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.pantallapreferencias); Toast.makeText(getBaseContext(), "Pulsa la tecla 'Volver atrás' para guardar las preferencias y volver a la aplicación.", 1).show(); } }
La nueva actividad, al descender de la clase PreferenceActivity, se encarga automáticamente de crear la interfaz de usuario con la lista de opciones siguiendo el diseño del fichero XML y se ocupa de mostrar, modificar y guardar estas opciones cuando sea necesario. Para que la aplicación pueda llamar a esta nueva Actividad, hay que incluirla en el fichero AndroidManifest.xml como otra actividad más de la aplicación:
204
Para acabar la aplicación, hay que añadir algún mecanismo que nos permita mostrar la pantalla de preferencias. Esta opción suele estar en un menú, pero para simplificar, en el Ejemplo 2 vamos a incluir el botón preferenciasBtn en la interfaz del usuario, para mostrar la ventana de preferencias. Al pulsar este botón se muestra la ventana de preferencias mediante el método startActivity() al que pasamos como parámetros el contexto de la aplicación y la clase de la ventana de preferencias (PantallaPreferencias.class).
preferenciasBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity(new Intent(PreferenciasActivity.this, PantallaPreferencias.class)); } });
En la Unidad 5 trataremos en detalle cómo usar Intents para iniciar Actividades de nuestra aplicación o de otra externa. Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Preferencias) de la Unidad 4. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior.
Si ejecutamos la aplicación en el emulador y pulsamos el botón de preferencias, aparece la siguiente pantalla de opciones:
205
Podemos marcar o desmarcar directamente la primera opción pulsando sobre el CheckBox. Si pulsamos sobre la segunda opción de tipo texto, se muestra una pequeña ventana que permite introducir el valor de la opción:
Por último, la tercera opción de tipo lista muestra una ventana emergente con la lista de valores posibles, donde únicamente podemos seleccionar uno:
Una vez hayamos establecidos los valores de las preferencias, podemos salir de esta ventana de opciones pulsando el botón Atrás del dispositivo o del emulador
. La Actividad
PantallaPreferencias se habrá ocupado por nosotros de guardar correctamente los valores de las opciones. Para
comprobarlo
vamos
a
añadir
a
la
interfaz
de
usuario
el
botón
obtenerOpcionesBtn, que recupera el valor actual de las tres preferencias y las muestra en la pantalla. Para acceder a las preferencias compartidas de la aplicación usaremos el método getDefaultSharedPreferences(). Como hemos hecho anteriormente, usamos los distintos métodos get() para recuperar el valor de cada opción dependiendo de su tipo:
206
obtenerPreferenciasBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { SharedPreferences prefe = PreferenceManager.getDefaultSharedPreferences(PreferenciasActivity.this); lblResultado.setText(""); Log.i("", "Opción 1: " + prefe.getBoolean("opcion1", false)); lblResultado.append("Búsqueda automática: " + prefe.getBoolean("opcion1", false)); Log.i("", "Opción 2: " + prefe.getString("opcion2", "")); lblResultado.append("\n\nTexto por defecto: " + prefe.getString("opcion2", "")); Log.i("", "Opción 3: " + pref.getString("opcion3", "")); lblResultado.append("\n\nBuscador: " + prefe.getString("opcion3", "")); } });
Si ejecutamos ahora la aplicación, establecemos las preferencias y pulsamos el botón de consulta de las preferencias, veremos la siguiente pantalla:
4.3
RECURSOS DE LAS APLICACIONES ANDROID
Los Recursos de Android son archivos almacenados en el directorio /res del proyecto. Estos ficheros de recursos pueden ser de tipo audio, vídeo, imágenes, texto, XML, etcétera, y se pueden integrar en la aplicación. ¿Por qué usar ficheros con recursos?
207
1. Los recursos se integran en la aplicación de manera transparente y se pueden cambiar sin necesidad de modificar el código fuente en Java. 2. Android genera un identificador único (ID) para cada archivo de recurso para acceder a ellos directamente desde el código fuente en Java. Todos estos identificadores de recursos se incluyen automáticamente en el archivo /gen/nombre_paquete/R.Java. 3. Android trata automáticamente los archivos de recursos de tipo XML de forma que se puede acceder a los valores definidos en ellos directamente. 4. El uso de Recursos es muy útil en la Localización e Internacionalización de la aplicación para desarrollar aplicaciones en varios idiomas. Así, se modifican los textos de las etiquetas, su alineación, imágenes, direcciones de Internet o cualquier tipo de archivo en función del idioma del dispositivo Android.
4.3.1
Tipos de recursos
Cadenas (String), colores, matrices (arrays) y dimensiones. Se definen en el directorio res/values/ del proyecto. Son muy útiles para internacionalizar la aplicación.
Las imágenes e iconos se almacenan en el directorio res/drawable.
Las animaciones se guardan en el directorio res/anime/.
Los ficheros XML se encuentran en el directorio res/xml/.
Diseños (Layout) de las pantallas o de elementos visibles (como una opción de un ListView) de la aplicación. Se definen en el directorio res/layout/ del proyecto.
Definición de menús principales y contextuales en el directorio res/menu/.
En el curso ya hemos usado recursos en las aplicaciones anteriores. En este apartado vamos a explicar en detalle cómo se incorporan éstos dentro de las mismas. Sobre todo, centraremos la explicación en la Cadenas (String), ya que es la funcionalidad más compleja y es muy útil para el programador.
4.3.2
Crear literales en ficheros de recursos con Eclipse Usando Eclipse se pueden definir fácilmente atributos en los ficheros de Recursos de
los proyectos Android, como cadenas, colores, etcétera. Para ello, hay que hacer doble clic en el fichero de recursos que queramos editar; por ejemplo, en el fichero res/values/string.xml y hacer clic en el botón "Add": 208
Después, basta con seleccionar el tipo de recursos que necesitamos dar de alta y rellenar el nombre y el valor que debe contener:
4.3.3
Recursos de tipo Cadena (String Resources)
Un recurso de tipo Cadena (String) permite definir cadenas de texto para usarlas en la aplicación Android: Incluso podemos cambiar su estilo y formato. Hay tres recursos de tipo Cadena:
String: recurso que incluye una única cadena de texto.
String Array: recurso que incluye una matriz de cadenas de texto.
Quantity Strings (Plurals): recurso que incluye dos cadenas según una cantidad sea singular o plural.
209
Más adelante veremos cómo se puede modificar el estilo y formatear el contenido de todos estos recursos de tipo cadena.
4.3.4.1 Cadena (String) Se trata de una Cadena de texto simple que se puede utilizar dentro del código fuente Java o en otro fichero (layout) de diseño XML, como los que se usan para definir las pantallas. Para definir recursos de cadenas de texto debemos usar la siguiente sintaxis: texto de la cadena Si abrimos el fichero res/values/strings.xml del Ejemplo 3, veremos que en él aparece la cadena:
Este texto se carga de res/layout/main.xml
El atributo name de se usa para identificar la cadena unívocamente. Por lo tanto, es el identificador (ID) de esta cadena. Dentro de la etiqueta incluimos el literal al que hace referencia. Para hacer referencia a este recurso de cadena debemos escribir lo siguiente:
En el código fuente Java: R.string.etiqueta1
En un fichero XML (de diseño o de recursos): @string/etiqueta1
En el Ejemplo 3 de esta Unidad puedes ver cómo se hace referencia a esta etiqueta desde el fichero XML que define el diseño de la pantalla principal main.xml:
También podemos usar el recurso de tipo cadena en el código fuente Java de la aplicación: etiqueta2.setText(getString(R.string.etiqueta2));
210
4.3.4.2 Matriz de cadenas (String Array) Es un recurso similar al de tipo Cadena, pero en este caso almacenamos varias cadenas en una matriz. Se puede utilizar, igualmente, dentro del código fuente Java o en otro fichero (layout) de diseño XML, como los que usan para definir las pantallas. Para definir recursos de tipo matriz debemos usar la siguiente sintaxis:
- texto elemento 1
- texto elemento 2
...
Si abrimos el fichero res/values/matrices.xml del Ejemplo 3, veremos que aparece la matriz:
- Cancer
- Capricornio
- Aries
- Leo
- Libra
El atributo name de se usa para identificar la matriz unívocamente. Por lo tanto, es el identificador (ID) de esta matriz. Dentro de la etiqueta incluimos varias etiquetas - con los literales que forman los elementos de la matriz. Para hacer referencia a este recurso de cadena en el código fuente Java debemos escribir R.array.horoscopo. En el Ejemplo 3 de esta Unidad puedes ver cómo se usa este recurso de tipo matriz para cargar las opciones de una caja de selección: Spinner s = (Spinner) findViewById(R.id.spinner); s.setPrompt("Elige el horóscopo"); ArrayAdapter
adapter = ArrayAdapter.createFromResource(this, R.array.horoscopo, android.R.layout.simple_spinner_item);
211
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) ; s.setAdapter(adapter);
Para obtener los elementos de la matriz desde el código fuente en Java podemos usar la función getStringArray:
Resources res = getResources(); String[] horoscopo = res.getStringArray(R.array.horoscopo);
4.3.4.3 Cadenas de cantidad (Quantity Strings) Cada idioma tiene diferentes reglas de concordancia gramatical con las cantidades. En español por ejemplo, escribimos "un libro" y, para cualquier otra cantidad, escribimos "3 libros". Esta distinción entre el singular y el plural es muy común en los idiomas. Incluso hay idiomas que hacen distinciones más sutiles. Android incluye un conjunto completo y distingue entre cero (zero), uno (one), dos (two), pocos (few), muchos (many) y otros (other). Las reglas de concordancia pueden ser muy complejas dependiendo del idioma, por lo que Android dispone del método getQuantityString(), que selecciona el recurso apropiado según la cantidad numérica que estemos tratando. Para definir recursos de tipo cantidad debemos usar la siguiente sintaxis:
- texto del literal
Si abrimos el fichero res/values/strings.xml del Ejemplo 3, veremos que aparece el elemento cantidad: - Se ha encontrado un contacto.
- Se han encontrado %d contactos.
El atributo name de se usa para identificar la cadena de cantidad unívocamente; por lo tanto, es su identificador (ID). Dentro de la etiqueta incluimos varias etiquetas - con los literales que forman las distintas opciones de plurales. Podemos escribir los siguientes tipos de plurales en el atributo quantity:
212
Valor
Descripción
zero
Cuando el idioma requiere un tratamiento especial del número 0 (como el árabe).
one
Cuando el idioma requiere un tratamiento especial del número 1 (como el español, el inglés, etcétera).
two
Cuando el idioma requiere un tratamiento especial del número 2 (como el galés).
few
Cuando el idioma requiere un tratamiento especial de pequeñas cantidades (como los números 2, 3 y 4 en checo).
many
Cuando el idioma requiere un tratamiento especial de los números grandes (como los números que terminan en 11-99 del maltés).
other
Valor por defecto del resto de cantidades.
Para hacer referencia a este recurso de cadena en el código fuente Java debemos escribir R.plurals.numeroDeContactos. En el Ejemplo 3 de esta Unidad puedes ver cómo se usa este recurso de tipo matriz para cambiar el literal en función del número que escribe el usuario en una caja de texto: //Obtenemos los recursos de la aplicación Resources res = getResources(); int total = Integer.parseInt(s.toString()); // Cambiamos el texto de la etiqueta plurales en función del contador String contactosStr = res.getQuantityString(R.plurals.numeroDeContactos, total, total); txtPlurales.setText(contactosStr);
Hemos usado la función getQuantityString() para obtener el literal que corresponde a la cantidad total y, además, volvemos a pasar como tercer parámetro total, para que lo use para formatear el literal en caso necesario. A continuación, veremos cómo formatear las cadenas de recursos,
4.3.4
Formatear y cambiar el estilo de las cadenas de recursos 4.3.4.1 Escapando comillas simples y dobles A la hora de escribir comillas simples o dobles dentro del literal de una cadena de
texto debemos "escaparlas", para que Android no las interprete como parte del fichero XML y muestre errores de sintaxis.
213
Es necesario incluir siempre las comillas simples dentro de las dobles o usar el carácter "\" para escaparlas. Además, no se permite incluir entidades de HTML para los caracteres singulares como "á". A continuación, mostramos algunos ejemplos que funcionan y otros que no:
name="buen_ejemplo_1">"Esto 'está' bien" name="buen_ejemplo_2"> Esto \'está\' bien name="ejemplo_incorrecto_1">Esto 'no' funciona name="ejemplo_incorrecto_2">Esto no funcionará
4.3.4.2 Formatear cadenas de texto Podemos también formatear cadenas usando la función
String.format(String,
Object...) incluyendo en ésta los argumentos que sean necesarios para formar el literal. Por ejemplo, la siguiente cadena de recurso con formato ¡Hola %1$s! Tienes %2$d mensajes nuevos
tiene dos argumentos: %1$s, que es una cadena y %2$d, que es un número decimal. En el Ejemplo 3 de esta Unidad hemos usado así esta cadena con formato para formatear un literal: String strFormat=getString(R.string.FormatoCadena); String texto=String.format(strFormat, "Usuario", 4); etiqueta3.setText(texto);
4.3.4.3 Cambio de estilo
Es posible definir una cadena de recurso que contenga cambios de estilo HTML en el texto. Así, en el Ejemplo 3 de esta Unidad definimos: Esto es un ejemplo de Recurso con Diseño
Las etiquetas HTML que se pueden usar son:
para texto en negrita.
para texto en cursiva.
para subrayar un texto.
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Recursos) de la Unidad 4. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior.
214
Si ejecutamos la aplicación en el emulador, veremos que aparece la siguiente pantalla:
Para completar este ejemplo hemos incluido también un recurso de tipo imagen en el directorio drawable y hemos definido un color para cambiar el aspecto de una de las etiquetas. Importante: Al definir recursos de tipo cadena hay que tener cuidado en no escribir caracteres que Android no sepa interpretar. Cuando esto ocurra Eclipse mostrará el siguiente mensaje de error y no permitirá compilar el proyecto:
4.4
ACCESO A INTERNET CON ANDROID
Android incluye la biblioteca Apache de cliente HTTP (Apache HttpClient library) que permite a las aplicaciones conectar con servidores Web de Internet.
215
Android también permite usar la librería estándar Red de Java Java Networking API (paquete java.net), aunque, si usamos este paquete java.net, Android utiliza internamente la librería de Apache. A partir de Android 2.2 también se puede utilizar la clase AndroidHttpClient. Para construir un objeto a partir de esta clase hay que usar el constructor newInstance(), que permite especificar el nombre de navegador (agente) que usa para conectar a una página de Internet. La clase AndroidHttpClient incluye el protocolo seguro SSL y métodos GZIP para comprimir y descomprimir los datos recibidos. Para que una aplicación Android acceda a Internet, es necesario declararlo en el fichero AndroidManifest.xml, que requiere el permiso "android.permission.INTERNET".
4.4.1
Ejemplo de conexión a Internet A continuación, vamos a mostrar mediante un ejemplo cómo conectar una aplicación
Android a Internet para descargar la página de un servidor Web. Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Acceso Internet) de la Unidad 4. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior. Si ejecutamos la aplicación en el emulador y pulsamos el botón "Descargar página web", aparece la siguiente pantalla:
En este ejemplo hemos usado también preferencias en la aplicación para guardar la dirección de la página Web que el usuario escribe en esta pantalla. Hemos implementado esta funcionalidad con las siguientes sentencias: 216
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Cargamos las preferencias y mostramos la última dirección en el EditText cargaPreferencias(); pagWeb.setText(ultimaUrl); ... } // Carga las preferencias de la aplicación private void cargaPreferencias() { SharedPreferences preferences = getSharedPreferences(PREFERENCIAS, Activity.MODE_PRIVATE); // Obtenemos la última dirección sobre la que se ha descargado ultimaUrl = preferences.getString(URL, "http://www.google.es"); }
@Override protected void onPause() { super.onPause(); // En el caso de que la aplicación se pause, guardamos las preferencias SharedPreferences preferences = getSharedPreferences(PREFERENCIAS, Activity.MODE_PRIVATE); Editor preferenceEditor = preferences.edit(); preferenceEditor.putString(URL, pagWeb.getText().toString()); // No hay que olvidar nunca hacer el commit preferenceEditor.commit(); }
Fíjate que en el código anterior definimos el evento onPause() de la Actividad, para que se guarden las preferencias en el caso de que el ciclo de vida pase por este estado. Así, nos aseguramos de que se guardará siempre la dirección de la página Web. Para descargar la página de Internet hemos usado una tarea asíncrona que hemos estudiado en la Unidad 3. En este caso hemos definido el método onProgressUpdate() para ir actualizando la pantalla según se va descargando la página de Internet:
217
// Método onClick del botón Descargar public void onClickHandler(View view) { switch (view.getId()) { case R.id.descargPagWeb: ResultadoLabel.setText(""); if (! hayConexionInternet()) ResultadoLabel.setText("ERROR: no hay conexión a Internet"); else { cargando.setVisibility(View.VISIBLE); // Iniciamos la tarea de descarga TareaDescargaPaginaWeb tarea = new TareaDescargaPaginaWeb(); tarea.execute(pagWeb.getText().toString()); } break; } } // end onClick
// Clase que descarga una página de Internet como una tarea asíncrona. // Es decir, podemos seguir usando la interfaz de usuario. private class TareaDescargaPaginaWeb extends AsyncTask { // Método que se ejecuta en segundo plano protected String doInBackground(String... urls) { try { HttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(urls[0]); HttpResponse respuesta = client.execute(request); // Obtenemos la respuesta BufferedReader rd = new BufferedReader(new InputStreamReader( respuesta.getEntity().getContent())); String linea = ""; String resultado = ""; // Mientras podamos leer una línea de la página Web while ((linea = rd.readLine()) != null) {
218
resultado+=linea; if (resultado.length()>1024) { publishProgress(resultado); resultado=""; } } // end while }
catch (Exception e) { System.out.println("Error al descargar la página."); return "ERROR al descargar la página: "+e.getMessage(); } return null; } /** Actualiza la etiqueta al ir descargando la página */ protected void onProgressUpdate(String... values) { ResultadoLabel.append(values[0]); } /** Cuando la tarea ha acabado, se invoca automáticamente este método */ protected void onPostExecute(String resultado) { if (resultado !=null) ResultadoLabel.append(resultado); cargando.setVisibility(View.INVISIBLE); } // end onPostExecute }
Para descargar la página de Internet hemos usado la clase DefaultHttpClient y llamado al método HttpGet(). Después, usamos la clase InputStreamReader para leer los datos como si se tratara de un fichero más. Es evidente que un dispositivo Android no tiene siempre conexión a Internet. Por esto, es bueno comprobar que tiene acceso a Internet a través del siguiente código:
public boolean hayConexionInternet() { // Comprobamos si hay conexión a Internet ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
219
NetworkInfo networkInfo = cm.getActiveNetworkInfo(); // Si la variable networkInfo<> null, entonces tenemos conexión a Internet if (networkInfo != null && networkInfo.isConnected()) { return true; } return false; }
En el código anterior usamos la clase ConnectivityManager para conocer el estado de la conexión a Internet. Además, para poder hacerlo, hay que declarar en el fichero Manifest de la aplicación el permiso correspondiente:
4.4.2
Conexión a través de proxy Si estás programando aplicaciones Android y te conectas a Internet a través de un
Proxy, es interesante configurar el emulador de Android para que acceda a Internet a través de ese Proxy. Para configurarlo, haz clic en el icono "Ajustes" y establece las siguientes opciones: Conexiones inalámbricas->Redes móviles ->APN->Telkila-> Proxy/Puerto
220
4.4
QUÉ SON JSON (JavaScript Object Notation?
JSON es el acrónimo en inglés de JavaScript Object Notation; es un formato ligero para el intercambio de datos en aplicaciones Web. JSON tiene la ventaja de que no requiere el uso de XML. La simplicidad de JSON ha provocado la generalización de su uso; es una buena alternativa, especialmente, al formato XML. JSON es más fácil de utilizar como formato de intercambio de datos que XML, porque es mucho más sencillo escribir un analizador semántico de JSON. El formato JSON se basa en los tipos de datos y sintaxis del lenguaje JavaScript. Es compatible con cadenas, números, boolean y valores nulos. También se pueden combinar valores en matrices y objetos. Los objetos en JSON son simplemente conjuntos desordenados de parejas nombre/valor, donde el nombre es siempre una cadena y el valor es cualquier tipo de datos válido para JSON, incluso otro objeto. A continuación, se muestra un ejemplo simple de definición de los datos de un producto usando JSON: { "producto": { "nombre": "Widget", "compania": "ACME, Inc", "numero": "7402-129", "precios": [ { "cantMin": 1, "precio": 12.49 }, { "cantMin": 10, "precio": 9.99 }, { "cantMin": 50, "precio": 7.99 } ] } }
221
Puedes ver más ejemplos en el siguiente enlace: json.org/example.html. Twitter es una fuente muy grande que usa el formato JSON. En el Ejemplo 5 de esta Unida vamos a cargar el Twitter de Mentor en una aplicación Android. La dirección es la siguiente: http://twitter.com/statuses/user_timeline/MinisterioEduc.json Fíjate en que la cuenta de la que obtenemos los datos se llama #MinisterioEduc.
4.5.1
Cómo usar JSON en nuestras aplicaciones a Android Android incluye la biblioteca JSON, que permite tratar este formato de dato. Las
clases más importantes de este paquete son:
JSONArray: permite cargar y tratar una matriz de elementos en formato JSON.
JSONObject: permite tratar un único elemento en formato JSON.
Desde Eclipse puedes abrir el proyecto Ejemplo 5 (JSON) de la Unidad 4. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior. Si ejecutamos la aplicación en el emulador, veremos la siguiente pantalla:
222
Vamos a mostrar con un ejemplo práctico cómo usar una fuente de datos en formato JSON en una aplicación Android. Si abrimos el fichero de código fuente del Ejemplo 5, veremos las siguientes sentencias:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Definimos la matriz que vamos a usar de adaptador en la ListActivity ArrayList datosAdaptador = new ArrayList(); // Leemos los datos de la cuenta de Twitter String datosCuentaTwitter = leeCuentaTwitter(); // Leemos el contenido interno del fichero obtenido if (datosCuentaTwitter!= null) try { // Usamos una matriz de JSON JSONArray matrizJSON = new JSONArray(datosCuentaTwitter); TextView nNoticias = (TextView) findViewById(R.id.nNoticias); nNoticias.setText("Número de noticias: " + matrizJSON.length()); // Recorremos ahora todos los elementos de la matriz for (int i = 0; i < matrizJSON.length(); i++) { // Leemos cada objeto y lo añadimos a la matriz de datos JSONObject jsonObjeto = matrizJSON.getJSONObject(i); datosAdaptador.add(jsonObjeto.getString("text")); } // end for } catch (Exception e) { e.printStackTrace(); } // Indicamos el adaptador de la ListActivity setListAdapter(new ArrayAdapter(this, android.R.layout.simple_list_item_1, datosAdaptador)); }
// Método que lee las noticias de una cuenta de Twitter public String leeCuentaTwitter() {
223
// String que permite ir añadiendo líneas StringBuilder builder = new StringBuilder(); // Usamos un cliente HTTP para obtener el fichero JSON HttpClient cliente = new DefaultHttpClient(); // Definimos un objeto para hacer una petición HTTP GET HttpGet httpGet = new HttpGet( "http://twitter.com/statuses/user_timeline/MinisterioEduc.json"); try { // Ejecutamos la petición GET HttpResponse respuesta = cliente.execute(httpGet); // Obtenemos la respuesta del servidor StatusLine statusLine = respuesta.getStatusLine(); // Y su código de estado asociado int statusCode = statusLine.getStatusCode(); // 200 el servidor responde con datos if (statusCode == 200) { // Obtenemos un puntero a la respuesta HttpEntity entity = respuesta.getEntity(); // Obtenemos el contenido de la respuesta InputStream contenido = entity.getContent(); // Leemos el contenido como si fuera un fichero BufferedReader reader = new BufferedReader(new InputStreamReader(contenido)); String line; while ((line = reader.readLine()) != null) { builder.append(line); } } else { Log.e(JSONActivity.class.toString(), "No se puede descargar el fichero"); } } catch (ClientProtocolException e) { e.printStackTrace(); } catch (IOException e) {
224
e.printStackTrace(); } return builder.toString(); } // end leeCuentaTwitter
En el código anterior hemos usado la clase DefaultHttpClient para descargar los datos en formato JSON. Esta clase ya la hemos visto con anterioridad en otros ejemplos. Una vez hemos obtenido los datos JSON del servidor, únicamente hay que crear un objeto de la clase JSONArray cuyo parámetro sean los datos anteriores. Después, sólo hay que recorrer esta matriz de elementos de tipo JSONObject y usar el método getString() para obtener el contenido del mismo.
NOTA: Para que una aplicación Android acceda a Internet es necesario declararlo en el fichero AndroidManifest.xml, que requiere el permiso "android.permission.INTERNET".
4.5.2
Cómo escribir ficheros en formato JSON Escribir datos en formato JSON es muy sencillo. Basta con crear un objeto del tipo
JSONObject o del tipo JSONArray y utilizar el método toString() para transformar este objeto en datos JSON. Veamos un ejemplo práctico:
public void escribirJSON() { JSONObject objeto = new JSONObject(); try { objeto.put("nombre", "Pedro del Cielo Lindo"); objeto.put("edad", new Integer(33)); objeto.put("altura", new Double(1,77)); objeto.put("ciudad", "Avila"); } catch (JSONException e) { e.printStackTrace(); } System.out.println(objeto); }
225
En Android existen tres formas de almacenar información para usarla en las aplicaciones: o
Preferencias de la aplicación (sólo lectura).
o
Ficheros locales en el sistema de archivos del sistema operativo.
o
Base de datos SQLite.
Android también permite manipular ficheros de forma similar a como se hacen en Java. En Android, por defecto, los ficheros son privados y únicamente puede acceder a ellos la aplicación que los crea. Podemos leer y escribir ficheros localizados en: o
La memoria interna del dispositivo: como fichero (lectura y escritura) o como recurso de la aplicación (sólo lectura).
o
La tarjeta SD externa, si existe, también denominada almacenamiento externo.
La clase File de Android se usa para identificar y gestionar archivos y directorios del sistema operativo. Es necesario “manifestar” que una aplicación necesita el permiso de escritura en la memoria externa en su arhivo Manifest.xml. Las preferencias de una aplicación son datos que una aplicación guarda y recupera para personalizar la experiencia del usuario. Para gestionar las preferencias de una aplicación hay que usar la clase SharedPrefences, donde cada preferencia se almacena siguiendo la estructura clave-valor.
226
Las preferencias se almacenan en formato XML en un fichero en la memoria del dispositivo. Android permite simplificar la gestión de las preferencias de una aplicación mediante el uso de una pantalla de opciones dividida en categorías que tienen opciones de diversos tipos. Los Recursos de Android son archivos contenidos en el proyecto de tipo audio, vídeo, imágenes, texto, XML, etcétera, que se pueden usar en la aplicación. Estos recursos son de sólo lectura.. Un recurso de tipo Cadena (String) permite definir cadenas de texto para usarlas en la aplicación Android; incluso podemos cambiar su estilo y formato. Son muy útiles para el programador, pues facilita la Internacionalizar las aplicaciones. Android incluye las bibliotecas necesarias que permiten a las aplicaciones conectar con servidores Web de Internet. Es necesario “manifestar” que una aplicación necesita el permiso de acceso a Internet en su arhivo Manifest.xml. JSON es el acrónimo en inglés de JavaScript Object Notation; es un formato ligero para el intercambio de datos en aplicaciones Web. JSON tiene la ventaja de que no requiere el uso de XML. Android incluye la biblioteca JSON que permite leer y escribir este formato de dato.
227
INTENTS EN ANDROID
ÍNDICE 5.1 INTENTS EN ANDROID.......................................................... 231 5.1.1 Introducción ...........................................................................231 5.1.2 Intenciones (Intents) ..............................................................231 5.1.3 Ficheros Manifest ...................................................................232 5.1.4 Declarar capacidades de los componentes de las aplicaciones233 5.1.5 Uso de intenciones .................................................................234 5.1.6 Arranque explícito de una actividad.......................................234 5.1.7 Arranque implícito de una actividad ......................................235 5.1.7.1 Ejecutar subactividades .....................................235 5.1.8 Filtros de intenciones .............................................................239 5.1.9 Resolución de intenciones implícitas......................................240 5.1.10 Uso de intenciones para extender aplicaciones ....................241 5.2 USO DE INTENTS ................................................................... 243 5.2.1 Uso de Intents ........................................................................243 5.2.2 Invocación Explícita ................................................................243 5.2.3 Invocación Implícita................................................................249 5.2.4 Registro Acción para Invocación Implícita..............................252 5.2.5 Detectar Acciones de Intents..................................................255 5.3 PERMISOS Y SEGURIDAD EN ANDROID ........................... 256 5.3.1 Arquitectura de seguridad de Android ...................................256 5.3.2 Firma de aplicación.................................................................256 5.3.3 ID de usuario y Acceso a ficheros ...........................................257 5.3.4 Permisos de aplicaciones........................................................257 5.3.5 Autoprotección de aplicaciones Android ...............................261 5.3.6 Asignar permisos de componentes internos de la aplicación 262 5.3.7 Cómo obtienes permisos estas aplicaciones ..........................263 5.3.8 Notas sobre seguridad en Android .........................................264 5.4 Tab.Layout .............................................................................. 264 5.4.1 Pantallas con pestañas con Tab Layout ..................................264
2
Intents
5.1
5.1.1
INTENTS EN ANDROID
Introducción En esta Unidad vamos a explicar cómo usar Intenciones (Intents) en Android para
arrancar Actividades o servicios. Además, veremos cómo definir los permisos de una Aplicación de Android. Finalmente, diseñaremos pantallas de aplicaciones con pestañas con el componente Tab Layout.
5.1.2
Intenciones (Intents) Las Intenciones (Intents) permiten a las aplicaciones de Android expresar la intención
de que se desea ejecutar una acción sobre unos datos usando algún componente de ésta o de otra aplicación. Las intenciones permiten interconectar componentes de la misma o de distintas aplicaciones mediante mensajes. De los cuatro componentes de Android, las Actividades, los Servicios y los Receptores de mensajes de difusión se activan con un mensaje asíncrono que se denomina Intención. Los Proveedores de contenidos quedan excluidos. Para crear una Intención hay que usar el objeto Intent de Android. Las intenciones se utilizan para arrancar componentes de dos formas:
Explícita: invocando la clase Java del componente que queremos ejecutar. Normalmente, esto se usa para invocar componentes de una misma aplicación.
Implícita: invocando la acción y los datos sobre los que aplicar dicha acción. Android selecciona, en tiempo de ejecución, la actividad receptora que cumple mejor con la acción y los datos solicitados.
Para las Actividades y Servicios, una intención define la acción que queremos realizar (por ejemplo, "ver" o "enviar" algo) y puede especificar el identificador URI de los datos que va a utilizar esa acción. Por ejemplo, una intención podría hacer una petición para arrancar una actividad que muestre una imagen o abra una página Web. En algunos casos, se puede iniciar una subactividad para recibir un resultado, en cuyo caso esta subactividad devuelve el resultado en otra nueva intención. Por ejemplo, se puede arrancar un Intent para que el usuario elija un contacto del teléfono y lo devuelta a la Actividad principal (esta intención de respuesta se devuelve también como un identificador URI que apunta al contacto seleccionado).
231
Las Intenciones son mensajes asíncronos entre componentes de aplicaciones que se usan para realizar acciones e intercambiar datos, tanto en la petición como en la respuesta,. De esta manera el usuario tiene la sensación de estar usando una única aplicación cuando, en realidad, son componentes de varias. A continuación, vemos un esquema que muestra cómo funciona un Intent:
Las Intenciones de difusión (en inglés Broadcast Intents) se envían a múltiples destinatarios del sistema y pueden ser procesadas por cualquier receptor de mensajes de difusión (Broadcast Receiver). Por ejemplo, el sistema genera este tipo de intenciones de difusión para anunciar situaciones diversas, como que la ’batería del teléfono se agota’, que llega una llamada de teléfono o un mensaje SMS, etcétera. Cualquier componente puede registrar un receptor de mensajes de difusión para que esté informado de estos eventos. El otro tipo de componente de Android, el Proveedor de contenido (Content Provider), no se activa mediante intenciones, sino mediante una solicitud de un ContentResolver. En la Unidad 7 de este curso veremos cómo funciona este procedimiento. Uno de los usos principales de las intenciones es arrancar, parar y cambiar entre las actividades y los servicios de una aplicación.
5.1.3
Ficheros Manifest Para que Android pueda iniciar un componente de una aplicación, el sistema debe
conocer que existe este componente. Ya hemos visto que, para ello, se declaran los componentes de una aplicación en el fichero AndroidManifest.xml. Este fichero se encuentra en el directorio raíz del proyecto Android. 232
Intents
Por ejemplo, para declarar una Actividad debemos escribir: ...
En el elemento usamos el atributo android:name para especificar el nombre completo de clase de la actividad y el atributo android:label especifica la cadena de texto visible que se muestra al usuario cuando la utiliza. Hay que declarar todos los componentes de la aplicación de esta forma usando las siguientes etiquetas:
: Actividades
: Servicios
: Receptores de mensajes de difusión (Broadcast receivers)
: Proveedores de contenido (Content providers)
Es obligatorio incluir en este fichero "AndroidManifest" todas las actividades, los servicios y los proveedores de contenido, ya que, si no lo hacemos, no son visibles para el sistema y, en consecuencia, no se pueden ejecutar. Los receptores de mensajes de difusión pueden declarase en este fichero "AndroidManifest" o bien podemos crearlos de forma dinámica en el código fuente Java.
5.1.4
Declarar capacidades de los componentes de las aplicaciones Tal y como se ha mencionado anteriormente, si queremos iniciar los componentes de
una aplicación hay que utilizar una intención para ejecutar actividades, servicios y receptores de mensajes de difusión. Se puede hacer de forma explícita indicando el nombre de la clase del componente destino. Sin embargo, el potencial de uso de las intenciones radica en el concepto de Acción mediante las invocaciones implícitas. En una Acción el programador sólo tiene que describir el tipo de acción que desea realizar y, opcionalmente, los datos sobre los que desea realizar esa acción. Si hay varios componentes que pueden llevar a cabo esta acción de la intención, entonces el usuario del dispositivo puede seleccionar cuál aplicar.
233
Android identifica qué componentes pueden responder a una Acción determinada buscándola en los filtros de intención (intent filters) que se declaran en el archivo "AndroidManifest" de todas las aplicaciones del dispositivo. Cuando se incluye un componente en el fichero "AndroidManifiest" de una aplicación, se pueden especificar filtros de intención que declaren la capacidad de este componente para que pueda responder a las Acciones de otras aplicaciones. Para incluir un filtro de intención de un componente, hay que añadir el elemento dentro de la declaración del componente. Veamos un ejemplo: una aplicación de correo electrónico con una Actividad que componga un nuevo correo electrónico puede declarar el filtro de intención ACTION_SEND que responda a la intención "send" (enviar) que, lógicamente, envía un mensaje. Una actividad de otra aplicación puede entonces simplemente iniciar una intención con la acción (ACTION_SEND) que provocará que Android busque la actividad o actividades que concuerdan con esta acción en iniciar la intención "send". Es decir, el programador no tiene que conocer el nombre de la intención, únicamente, debe invocar su acción. Más adelante veremos otro ejemplo.
5.1.5
Uso de intenciones Para arrancar una Actividad sin esperar una respuesta de la subactividad iniciada,
debemos usar la siguiente función: startActivity(anIntent);
Para arrancar una Actividad y esperar una respuesta de la subactividad iniciada, debemos usar la siguiente función: startActivityForResult(anIntent, INTENT_COD);
Ambos métodos se pueden usar tanto en las invocaciones explícitas como implícitas. La diferencia radica en que el primero inicia la subactividad y no espera respuesta de ésta; el segundo método espera recibir una respuesta de la ejecución de la subactividad.
5.1.6
Arranque explícito de una actividad Para iniciar explícitamente una Actividad hay que especificar en la intención el
contexto de la aplicación que la invoca y la clase de la actividad que se quiere arrancar: Intent intent = new Intent(MiActividad.this, MiOtraActividad.class); startActivity(intent);
234
Intents
En la Unidad 4, en el apartado de pantallas de preferencias, ya hemos usado esta manera de invocar una Intención explícitamente.
5.1.7
Arranque implícito de una actividad Una intención implícita especifica la acción requerida y los datos sobre los que actúa.
Es importante insistir en que las aplicaciones de Android deben publicar las acciones que ofrecen. Por ejemplo, la aplicación dialer de marcado de llamadas de teléfono de Android ofrece la acción Intent.ACTION_DIAL que se invoca así:
if (...) { Intent intent = new Intent(Intent.ACTION DIAL, Uri.parse("tel:91-6666")); startActivity(intent); }
El código anterior inicia una llamada de teléfono al número indicado como parámetro en la intención. Como hemos usado el método startActivity(), no trataremos la respuesta de su ejecución.
5.1.7.1
Ejecutar subactividades
Se puede arrancar una actividad como subactividad de
otra actividad principal.
Cuando termina esta subactividad, se invoca al método onActivityResult de la actividad principal desde la que se inició. Cualquier actividad registrada en el fichero "AndroidManifiest" puede ser invocada como
subactividad.
Para
arrancar
una
subactividad
hay
que
usar
el
método
startActivityForResult(), pasando como parámetro la intención y un código de petición, que se utiliza para identificar a qué subactividad corresponde la respuesta. Veamos un ejemplo sencillo que inicia una subactividad desde una actividad principal:
private static final int SUBACTIVIDAD = 1; Intent intent = new Intent(this, MiOtraActividad.class);
// Añadimos un dato extra en la intención que pasa a la subactividad intent.putExtra("nombre", "Nadie"); startActivityForResult(intent, SUBACTIVIDAD);
235
En la Intención hemos incluido una colección de datos Extras con información adicional. Para ello, usamos el método putExtra() de la clase Intent para incorporar la información adicional. Veamos ahora un ejemplo del código de una subactividad: public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(R.layout.subactividad); // Obtenemos el intent que invoca esta actividad Intent intent = getIntent(); // Obtenemos el contenido de los datos del intent Uri datos = intent.getData(); // Obtenemos el contenidos de la variable extras Bundle extra = intent.getExtras(); // Usamos los datos que hemos obtenido if (extra == null) return; // Leemos los contenidos de los datos de invocación y variables extra String valor1 = extra.getString("nombre"); // Formamos la URL que debemos cargar URL url = new URL(datos.getScheme(), datos.getHost(), datos.getPath()); }
Button okButton = (Button) findViewById(R.id.ok_button); okButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { Uri dato = Uri.parse("content://dato/" + id_dato_seleccionado); Intent resultado = new Intent(null, dato); resultado.putExtra(TODO_CORRECTO, correcto); resultado.putExtra(DATO_SELECCIONADO, datoSeleccionado); setResult(RESULT_OK, resultado); finish(); } });
236
Intents
Button cancelarButton = (Button) findViewById(R.id.cancel_button); cancelarButton.setOnClickListener(new View.OnClickListener() { public void onClick(View view) { setResult(RESULT_CANCELED, null); finish(); } });
A continuación, vamos a explicar los métodos más importantes:
getIntent(): obtiene la intención que ha invocado la actividad. Normalmente este método se usa en el constructor de la Actividad con el método onCreate(). Una vez obtenida la intención, podemos conseguir más información contenida en ella mediante los métodos: o
getData(): obtiene el dato contenido en la intención en formato Uri. Para leer los contenidos internos de este dato podemos usar los métodos getScheme() (esquema del dato), getUserInfo() (información de usuario), getHost()(dirección del servidor), etcétera.
o
getExtras(): obtiene los datos extras contenidos en la intención en formato Bundle. Para leer los campos de este tipo de dato podemos usar los métodos getString("nombre"), getInteger("nombre"), etcétera,
setResult(): acepta dos parámetros: un código de respuesta y una intención con los datos devueltos. o
Código de resultado: Activity.RESULT_OK, Activity.RESULT_CANCELED o cualquier número entero.
o
Intención con datos devueltos: identificador URI a los contenidos devueltos (un contacto, un número de teléfono, una imagen, etcétera) y una colección de datos Extras con información adicional. Para ello creamos una clase Intent y usamos el método putExtra() para incorporar la información adicional.
finish():
devuelve a la actividad principal el resultado de la ejecución de la
subactividad. En el código Java de la subactividad se debe invocar siempre el 237
método setResult() antes que el método finish(), para devolver los resultados de la ejecución de ésta.
Para recuperar los resultados devueltos a la actividad principal debemos definir el método onActivityResult(). Veamos un ejemplo del código de una actividad principal que trata la respuesta de una subactividad:
private static final int SUB_ACTIVIDAD_UNA = 1; private static final int SUB_ACTIVIDAD_DOS = 2; @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch(requestCode) { case (SUB_ACTIVIDAD_UNA) : { if (resultCode == Activity.RESULT_OK) { Uri dato = data.getData(); // Obtenemos el campo TODO_CORRECTO con el valor por defecto false boolean correcto = dato.getBooleanExtra(TODO_CORRECTO,false); String datoSeleccionado = dato.getStringExtra(DATO_SELECCIONADO); } break; } case (SUB_ACTIVIDAD_DOS) : { if (resultCode == Activity.RESULT_OK) { // Tratar el resultado de la Subactividad 2 } break; } } }
El método onActivityResult() recibe los siguientes parámetros:
238
Intents
Código de petición: código que se utiliza para iniciar la subactividad.
Código
de
resultado:
código
que
devuelve
la
subactividad,
Activity.RESULT_OK, Activity.RESULT_CANCELED o cualquier número entero.
Intención con datos devueltos: identificador URI con la intención con los resultados devueltos por la subactividad. Esta segunda actividad reciben datos de la primera a través de la clase Bundle que pueden ser recuperados a través de dos formas: o
método data.getExtras() en combinación con cualquiera de sus métodos getString("nombre_dato"), getBoolean("nombre_dato"), getInteger("nombre_dato"), etcétera.
o
métodos getData() en combinación con cualquiera de los métodos getStringExtra("nombre_dato"), getBooleanExtra("nombre_dato"), getIntegerExtra("nombre_dato"), etcétera. En estos métodos se puede definir un valor por defecto en el caso de que no esté definido el dato.
Podemos comprobar con el método data.hasExtra("nombre_dato") si la intención devuelta contiene el dato extra nombre_dato antes de tratar esta información.
5.1.8
un servicio nativo o de otra aplicación. Los filtros de intenciones registran Actividades, Servicios y Receptores de mensajes de difusión (broadcast receivers) como potenciales receptores de acciones sobre cierto tipo de datos. En el fichero "Manifest" se utiliza la etiqueta en la sección del componente de la aplicación que recibe la acción, especificando la acción, la categoría y los datos para invocarla. Estos parámetros se usan para especificar lo siguiente:
Acción: atributo android:name, que especifica la acción que se sirve a la misma aplicación o a otras aplicaciones . Debemos definir una cadena única, por lo que se debe utilizar la notación de paquetes (es.mentor.)
Categoría: atributo android:category, que especifica bajo qué circunstancias se debe servir la acción. Es posible especificar varias categorías. Este atributo añade información adicional sobre la acción que se debe ejecutar. Por ejemplo, CATEGORY_LAUNCHER indica que la acción debe aparecer en la pantalla de Inicio (Launcher) como una aplicación, mientras que CATEGORY_ALTERNATIVE 239
indica que debe incluirse en una lista de acciones alternativas que el usuario puede aplicar a un conjunto de datos.
Datos: permite especificar mediante atributos los tipos de datos sobre los que puede actuar el componente. Por ejemplo, android:host, android:mimetype, android:path, etcétera.
Veamos un ejemplo del código de la actividad EjemploActividad que declara la acción VER_TEXTO al sistema operativo para que otra Actividad la invoque:
Resolución de intenciones implícitas Puede ocurrir que exista más de una actividad que haya registrado un filtro de
intención (Acción) con el mismo nombre. Por ejemplo, el dispositivo Android puede disponer de varios programas para navegar por Internet; si el usuario solicita abrir una página HTML, entonces se le muestra un listado con las opciones posibles que completan las acciones. Android elige la intención implícita que se resuelve con varias actividades posibles así: 1. Android genera una lista interna en el sistema operativo con todos los filtros de intenciones posibles incluyendo los de las aplicaciones nativas preinstaladas. 2. Elimina los filtros que no coinciden con la acción solicitada. 3. Eliminan las categorías que no coinciden con la categoría invocada. 4. Cada parte de la URI de los datos de la intención que se invoca se compara con la etiqueta del filtro. Los parámetros especificados en el filtro han de coincidir con los de la intención. Es decir, no podemos hacer una llamada 240
Intents
de teléfono con un fichero de música, sino que debemos usar un número de teléfono. 5. Si después de los pasos anteriores, existe más de un filtro candidato, se escoge el de mayor prioridad o se permite al usuario que seleccione la Intención. Con los métodos getIntent(), getAction() y getData() el componente elegido por el proceso de resolución puede saber qué acción tiene que ejecutar y el dato sobre el que ejecutarla. Fíjate en este ejemplo:
@Override public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(R.layout.main); Intent intent = getIntent(); String action = intent.getAction(); Uri data = intent.getData(); }
Con el método startNextMatchingActivity() un componente puede derivar el procesamiento de una intención implícita a otro componente cuando no quiere atenderla; por ejemplo, cuando no es la adecuada para realizar esa acción. Fíjate en el siguiente ejemplo:
Intent intent = getIntent(); if (!noHayConexionInternet) startNextMatchingActivity(intent);
De esta manera, un componente de una aplicación Android puede decidir si responde a una acción o cede el trabajo a otro componente más apropiado.
5.1.10
(acciones) de nuevos componentes de aplicaciones desde menús de opciones de aplicaciones compiladas, antes de que exista la acción en el sistema.
241
El método addIntentOptions() de la clase Menu permite especificar los datos sobre los que puede operar una acción futura. Se especifican únicamente los datos, no la acción en sí. Cuando Android resuelve la intención y devuelve una lista de acciones apropiadas para el dato, se crea una nueva opción en el menú de la aplicación. Muchas aplicaciones del sistema operativo emplean este mecanismo para extender su funcionalidad a medida que nuevas actividades van implementando las acciones previstas. Para declarar nuevas acciones susceptibles de ser invocadas desde menús preexistentes en otras actividades, hay que exportarlas en los filtros de intenciones. La etiqueta del filtro debe ser ALTERNATIVE y/o SELECTED ALTERNATIVE. La etiqueta aparecerá en la opción del menú correspondiente. Por ejemplo:
El método addIntentOptions() del objeto menú recibe como parámetro una intención que especifica los datos para los que se quiere proporcionar una acción. Se invoca este método desde los métodos onCreateOptionsMenu() o onCreateContextMenu(), que ya hemos
estudiado.
La
CATEGORY_ALTERNATIVE
intención o
sólo
especifica
los
datos
y
la
CATEGORY_SELECTED_ALTERNATIVE.
No
especificar ninguna acción ya que es lo que buscamos.
@Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); // Creamos un intent que se usa para resolver que acciones aplican a // ese tipo de datos. Intent intent = new Intent();
242
categoría se
debe
Intents
intent.setData(Dato.CONTENT_URI); intent.addCategory(Intent.CATEGORY_SELECTED_ALTERNATIVE); // Buscamos el menú que coincide con esos datos. menu.addIntentOptions( R.id.intent_group, // Menu group donde añadimos las nuevas opciones 0, // ID único de la opción (ninguno) 0, // Orden de la opción (ninguno) this.getComponentName(), // Nombre Actividad actual null, // Opciones que se colocan al principio (ninguna) intent, // Intent creado con los datos y la categoría 0, // Parámetro adicional (ninguno) null); // Matriz de MenuItems que correla opciones (ninguna)
return true; }
5.2
USO DE INTENTS
5.2.1
se usan, tanto implícita como explícitamente. 5.2.2
actividades. Para ello, vamos a invocar intenciones explícitamente entre dos actividades. La primera actividad llama a la segunda, también llamada subactividad, a través de una intención explícita. Esta segunda actividad recibe datos de la primera a través de la clase Bundle, que pueden ser recuperados a través de intent.getExtras(). La segunda actividad (o subactividad) puede finalizar mediante el botón de retroceso del teléfono o por una acción del usuario en ésta, por ejemplo, un clic en un botón o la selección de una opción en un listado. En este caso se lanza el método finish(), en el que se pueden transferir algunos datos como respuesta de la ejecución a la actividad principal inicial. En esta actividad principal se utiliza el método startActivityForResult() para recibir estos datos de la subactividad. A continuación, vamos a ver cómo funciona la invocación explícita de Intenciones en Android mediante un ejemplo sencillo que simula una aplicación que gestiona un único contacto. En este ejemplo debemos prestar atención al código Java que interrelaciona ambas Actividades. Lo primero que vamos a hacer es definir el fichero XML que incluye los diseños (layout) de ambas actividades. Puedes encontrar este diseño en los ficheros del proyecto Android: 243
res/layout/main.xml: actividad principal.
res/layout/segundaactividad.xml: actividad secundaria o subactividad.
Después, creamos el código fuente Java para las dos actividades. La segunda actividad, que se invoca desde la primera, muestra los datos recibidos e indica si devuelve datos a la actividad principal. Veamos el código fuente de la actividad principal: public class IntencionexplicitaActivity extends Activity { private static final int COD_PETICION = 10; private String nombre, apellidos; private TextView resultadoLbl; private Button ModContactoBtn, AltaContactoBtn;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Subrayamos la etiqueta que hace de título TextView tituloLbl = (TextView) findViewById(R.id.tituloLbl); SpannableString contenido = new SpannableString(tituloLbl.getText()); contenido.setSpan(new UnderlineSpan(), 0, contenido.length(), 0); tituloLbl.setText(contenido); resultadoLbl = (TextView) findViewById(R.id.resultadoLbl); // Al arrancar la aplicación como no hay un contacto no se puede modificar ModContactoBtn = (Button) findViewById(R.id.ModContactoBtn); AltaContactoBtn = (Button) findViewById(R.id.AltaContactoBtn); ModContactoBtn.setEnabled(false); resultadoLbl.setText("No has dado de alta ningún contacto"); // Cargamos las variables con el dato nombre=""; apellidos=""; } public void onClick(View view) { Intent i = new Intent(this, ActividadDos.class);
244
Intents
switch (view.getId()) { case R.id.AltaContactoBtn: i.putExtra("operacion", "alta"); i.putExtra("nombre", ""); i.putExtra("apellidos", ""); break; case R.id.ModContactoBtn: i.putExtra("operacion", "modifica"); i.putExtra("nombre", nombre); i.putExtra("apellidos", apellidos); break; } // Invocamos explícitamente la actividad con los datos "i" startActivityForResult(i, COD_PETICION); }
@Override // Este método se invoca cuando la subactividad finaliza protected void onActivityResult(int requestCode, int resultCode, Intent data) { // Si el Cód. petición no coincide (otra aplicación está usando esta // actividad también) no tratamos la información if (requestCode != COD_PETICION) return; // Si la subactividad responde OK if (resultCode == RESULT_OK) { resultadoLbl.setText(""); if (data.hasExtra("nombre")) { resultadoLbl.setText("Nombre: " + data.getExtras().getString("nombre")+"\n"); nombre= data.getExtras().getString("nombre"); } if (data.hasExtra("apellidos")) { resultadoLbl.append("Apellidos: " + data.getExtras().getString("apellidos")+"\n"); apellidos= data.getExtras().getString("apellidos");
245
} if (!nombre.isEmpty() || !apellidos.isEmpty()) { AltaContactoBtn.setEnabled(false); ModContactoBtn.setEnabled(true); String auxStr = "modificado"; String operacion = data.getExtras().getString("operacion"); if (operacion.equals("alta")) auxStr = "dado de alta"; Toast.makeText(this, "Has " + auxStr + " el contacto correctamente", Toast.LENGTH_SHORT).show(); } else { AltaContactoBtn.setEnabled(true); ModContactoBtn.setEnabled(false); }
} else Toast.makeText(this, "Has salido de la subactividad sin pulsar el botón 'Aceptar'", Toast.LENGTH_SHORT).show(); } }
En el código anterior es importante fijarse en cómo se invoca la subactividad mediante el método startActivityForResult() y cómo se trata la respuesta de ésta en el método onActivityResult() de la actividad principal. Veamos ahora el código fuente de la actividad secundaria o subactividad:
public class ActividadDos extends Activity { private EditText nombre, apellidos; private TextView tituloLbl; private String operacion; @Override public void onCreate(Bundle bundle) { super.onCreate(bundle); setContentView(R.layout.segundaactividad); // Buscamos los componentes de la UI
246
Intents
nombre = (EditText) findViewById(R.id.nombre); apellidos = (EditText) findViewById(R.id.apellidos); tituloLbl
= (TextView) findViewById(R.id.tituloLbl);
// Obtenemos el contenidos de la variable extras Bundle extra = getIntent().getExtras(); // Si no hay variable extra no ejecutamos las siguientes sentencias if (extra == null) return; // Leemos los contenidos de las variables extra String valor1 = extra.getString("nombre"); String valor2 = extra.getString("apellidos"); operacion = extra.getString("operacion"); if (valor1 != null && valor2 != null && operacion != null) { nombre.setText(valor1); apellidos.setText(valor2); // Cambiamos el título de la pantalla en función de la operación if (operacion.equals("alta")) tituloLbl.setText("Alta de contacto. Indica el nombre y los apellidos."); else tituloLbl.setText("Modificar contacto. Cambia el nombre y los apellidos."); } } // Cuando el usuario pulsa aceptar devolvemos la información de nuevo public void onClick(View view) { Intent datos = new Intent(); datos.putExtra("nombre", nombre.getText().toString()); datos.putExtra("apellidos", apellidos.getText().toString()); datos.putExtra("operacion", operacion); // Indicamos OK en el resultado setResult(RESULT_OK, datos); finish(); } @Override // Método de la actividad que se invoca cuando ésta finaliza public void finish() {
247
super.finish(); } }
En el código anterior es importante fijarse en cómo se usa la clase Bundle en el método onCreate() para leer los datos extra que incorpora la actividad principal al invocar la actividad secundaria y cómo se devuelve la respuesta de ésta con los métodos setResult() y finish() a la actividad principal. Finalmente, declaramos la actividad secundaria en el fichero "AndroidManifest.xml":
248
Intents
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Intención explícita) de la Unidad 5. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Intenciones invocadas de manera explícita.
Si ejecutamos la aplicación, vemos que al hacer clic en cualquiera de los botones de la pantalla siguiente:
se inicia la actividad secundaria para hacer una tarea en concreto:
5.2.3
dos actividades mediante Acciones. Para ello, vamos usar actividades de aplicaciones que el sistema operativo instala por defecto. Lo primero que vamos a hacer es definir el fichero XML que incluye el diseño (layout) de la actividad principal. Puedes encontrar este diseño en el fichero res/layout/main.xml del proyecto Android. Para poder usar Intents con componentes de Android hay que añadir los siguientes permisos a la aplicación en el fichero "AndroidManifest.xml":
249
Después, creamos el código fuente Java para la actividad principal: // Método que usan los botones de la pantalla principal public void invocaIntent(View view) { Intent intent = null; // Invocamos un Intent con una Acción y con un dato de información para la // acción: un teléfono, una dirección Internet, etcétera. switch (view.getId()) { case R.id.navegadorBtn: intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.es/")); startActivity(intent); break; case R.id.llamarTfnoBtn: intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:(+34)12345789")); startActivity(intent); break; case R.id.marcarTfnoBtn: intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:(+34)12345789")); startActivity(intent); break; case R.id.contactosBtn: intent = new Intent(Intent.ACTION_VIEW, Uri.parse("content://contacts/people/")); startActivity(intent); break; case R.id.selContactoBtn:
250
Intents
intent = new Intent(Intent.ACTION_PICK, Uri.parse("content://contacts/people/")); // Ejecutamos este Intent indicando que esperamos la respuesta startActivityForResult(intent, SELEC_CONTACTO); break; } // end switch }
@Override // Método que se lanza cuando un Intent acaba su tarea. En este caso sólo la // acción que selecciona un contacto devuelve información public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && requestCode == SELEC_CONTACTO) { // En la Unidad 7 veremos los Content Providers. En este caso usamos los contactos del teléfono Uri contactoData = data.getData(); Cursor c = managedQuery(contactoData, null, null, null, null); if (c.moveToFirst()) { String nombre= c.getString(c.getColumnIndexOrThrow( ContactsContract.Contacts.DISPLAY_NAME)); Toast.makeText(this, "Nombre seleccionado: "+ nombre, Toast.LENGTH_LONG).show(); } } // end RESULT_OK }
En el código fuente anterior podemos ver que algunas actividades se inician con el método startActivity() porque no queremos saber el resultado de la ejecución de la subactividad. Sin embargo, el segundo botón "Seleccionar contacto" se inicia con el método startActivityForResult() para obtener el resultado de la ejecución de la subactividad en el método onActivityResult(). En la práctica lo que hacemos es obtener el contacto seleccionado mediante un identificador URI de la base de datos de contactos del teléfono.
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Intención implícita) de la Unidad 5. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado Intenciones invocadas de manera implícita.
251
Si ejecutamos la aplicación, veremos que al hacer clic en cualquiera de los botones de la pantalla se inicia una nueva Actividad:
Fíjate que en el código fuente no especificamos la aplicación que tiene que lanzarse para esa Acción, simplemente dejamos que Android decida qué aplicación es más apropiada para la tarea solicitada.
5.2.4
dispone de componentes que pueden ser utilizados por otras aplicaciones mediante Acciones. El Ejemplo 2 de este Unidad muestra cómo definir filtros de intenciones (Filter Intens) de una Actividad interna de la aplicación para que pueda ser invocada implícitamente desde otra aplicación mediante su Acción correspondiente. En este caso, hemos definido en el sistema operativo un nuevo navegador de Internet. Por simplificación, esta nueva actividad del navegador de Internet lo vamos a iniciar desde la misma aplicación, si bien esta opción aparecerá también en otras aplicaciones del sistema operativo que deseen navegar por Internet. Para que Android sepa que hay una nueva Acción disponible en el sistema operativo, tenemos
que
añadir
"AndroidManifest.xml": 252
la
Actividad
con
la
Acción
correspondiente
en
el
fichero
Intents
En este caso, la acción es visualizar (VIEW) el protocolo HTTP (esquema del dato). Hemos usado la etiqueta para definir la nueva acción. Por la nomenclatura de Android, es muy importante definir el punto "." en el nombre de la actividad: android:name=".NavegadorActivity". Lo primero que vamos a hacer es definir el fichero XML que incluye el diseño (layout) de la actividad secundaria (Navegador de Internet). Puedes encontrar este diseño en el fichero res/layout/navegador.xml del proyecto Android.
Después, creamos el código fuente Java para la nueva actividad:
// Clase sencilla que simula un navegador cargando una dirección de Internet public class NavegadorActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.navegador); // Usamos una etiqueta para mostrar la página descargada TextView textoResultado = (TextView) findViewById(R.id.textView); // Obtenemos el intent que invoca esta actividad Intent intent = getIntent(); // Obtenemos el contenido de los datos del intent Uri datos = intent.getData(); try { // Formamos la URL que debemos cargar
253
URL url = new URL(datos.getScheme(), datos.getHost(), datos.getPath()); // Leemos los datos de la página web y los vamos cargando en la etiqueta BufferedReader rd = new BufferedReader(new InputStreamReader( url.openStream())); String line = ""; while ((line = rd.readLine()) != null) { textoResultado.append(line); }
} catch (Exception e) { e.printStackTrace(); } } }
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Intención implícita) de la Unidad 5. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos definido una nueva Acción mediante Filtros de Intenciones.
Si ejecutamos la aplicación, veremos que al hacer clic en el botón "Iniciar el navegador" de la pantalla podemos seleccionar el navegador con el que queremos abrir la página Web. Si hacemos clic en "Navegador curso Mentor", se inicia la nueva Actividad que hemos definido anteriormente:
254
Intents
5.2.5
una determinada Acción en un Intent. Por ejemplo, puede ocurrir que ampliemos la funcionalidad de una aplicación usando una actividad de otra aplicación opcional que el usuario puede haber instalado. Esto se puede hacer usando la clase PackageManager de Android y el método queryIntentActivities(), para consultar si algún componente instalado en el teléfono responde a esa acción. El siguiente código comprueba si una Acción existe. Así, es muy fácil cambiar el comportamiento de una aplicación, como mostrar u ocultar opciones de la misma.
public boolean existeAccion(Context context, String accion) { final PackageManager packageManager = context.getPackageManager(); final Intent intent = new Intent(accion); List resolveInfo = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); if (resolveInfo.size() > 0) { return true; } return false; }
255
5.3
PERMISOS Y SEGURIDAD EN ANDROID
Android es un sistema operativo que separa privilegios entre aplicaciones que se ejecutan simultáneamente usando identidades diferentes del sistema, es decir, con un ID de usuario de grupo de Linux diferente (cada aplicación se ejecuta con un usuario distinto). Determinadas partes del sistema operativo también se separan usando diferentes identidades. Linux aísla así las aplicaciones entre sí y del sistema operativo. Además, se proporcionan características adicionales de seguridad más específicas a través del mecanismo de "permiso" que impone restricciones a las operaciones concretas que un proceso en particular puede realizar. Nota: por coherencia, en este apartado tratamos también la seguridad de Content Providers, Servicios y Receptores de mensajes de difusión (Broadcast Receivers), que estudiaremos en la Unidad 7. Puedes buscar información en esta Unidad para conocer los conceptos básicos.
5.3.1
Arquitectura de seguridad de Android La filosofía del diseño de la arquitectura de seguridad de Android consiste en
que, por defecto, una aplicación no tiene permisos para realizar cualquier operación que pueda afectar negativamente a otras aplicaciones, al sistema operativo o al usuario. Esto incluye la lectura o escritura de datos privados del usuario (como los contactos o correos electrónicos), leer o escribir archivos de otra aplicación, acceder a Internet, etcétera. Debido a que Android separa la ejecución de las aplicaciones en cajas de arena (del inglés sandbox), las aplicaciones deben compartir recursos y datos de manera explícita. Un sandbox es un sistema de aislamiento de procesos, mediante el cual se pueden ejecutar distintos programas con seguridad y de manera separada. Para compartir recursos entre aplicaciones, éstas deben declarar al sistema Android los permisos adicionales que necesitan para funcionar correctamente y ampliar su funcionalidad básica. Las aplicaciones declaran los permisos que necesitan y el sistema Android solicita al usuario su consentimiento cuando instala la aplicación. Android no tiene ningún mecanismo para dar permisos de forma dinámica (en tiempo de ejecución).
5.3.2
Firma de aplicación Todas las aplicaciones Android (archivos .apk) deben estar firmadas con un certificado
que identifica al autor de la aplicación. El propósito de los certificados de Android es distinguir a los autores de las aplicaciones. Esto permite al sistema conceder o denegar solicitudes de acceso para compartir la identidad de Linux que tenga otra aplicación. Es decir, si el desarrollador es el mismo, es posible ejecutar ambas aplicaciones con el mismo usuario de Linux y que compartan el mismo sandbox. 256
Intents
5.3.3
ID de usuario y Acceso a ficheros Cuando se instala una aplicación, Android asigna a cada paquete un identificador
único de usuario de Linux. Esta identidad no cambia mientras el paquete está instalado en el dispositivo. En un dispositivo diferente el mismo paquete puede tener un ID de usuario distinto; así pues, lo importante es que cada paquete siempre tiene otro ID de usuario en un dispositivo cualquiera. Debido a que la seguridad en Android se hace a nivel de proceso, el código de dos paquetes cualquiera no se puede ejecutar en el mismo proceso por defecto, ya que funcionan con diferentes usuarios de Linux. Sin
embargo,
se
puede
utilizar
el
atributo
sharedUserId
del
fichero
AndroidManifest.xml para asignar a cada paquete el mismo ID de usuario. De esta manera, los dos paquetes se tratan como si fueran la misma aplicación con el mismo ID de usuario y los permisos de archivo. Es importante tener en cuenta que, para poder hacer esto, las dos aplicaciones deben estar firmadas con el mismo certificado de autor. A los datos almacenados por una aplicación también se les asigna el ID de usuario y normalmente otras aplicaciones no pueden acceder a ellos. Cuando se crea un nuevo archivo con funciones del estilo getSharedPreferences(), openFileOutput(), etcétera, se puede usar los parámetros MODE_WORLD_READABLE y MODE_WORLD_WRITEABLE para permitir que cualquier otra aplicación lea o escriba en el archivo. Aunque se establezcan estos indicadores, el archivo sigue siendo propiedad de la aplicación original, si bien se permite su lectura y escritura globalmente, de manera que cualquier otra aplicación puede acceder a esta información almacenada. Por seguridad, no es recomendable usar la compartición de información de esta manera; es mejor usar Proveedores de contenidos (Content Provider) para ello.
5.3.4
Permisos de aplicaciones Una aplicación básica de Android no tiene permisos asociados, por lo que no puede
hacer nada que afectara al usuario o a cualquier aplicación instalada en el dispositivo. Para hacer uso de las características protegidas del dispositivo, se debe incluir en el fichero AndroidManifest.xml del proyecto una o más etiquetas que declaren los permisos que necesita la aplicación. En alguno de los ejemplos anteriores del curso ya hemos usado esta etiqueta para poder ampliar la funcionalidad de la aplicación y acceder a determinados recursos del sistema operativo.
257
Podemos escribir una o varias etiquetas en el archivo AndroidManifest.xml. El elemento requiere que definamos el atributo android:name, dentro del cual indicamos el nombre del permiso que requiere la aplicación. Por ejemplo: ...
En este caso, estamos manifestando que esta aplicación necesita poder recibir mensajes cortos SMS para funcionar. Como hemos comentado, los permisos se asignan en el momento en que se instala la aplicación en el dispositivo. Para ello, se pide al usuario su consentimiento para que la aplicación pueda acceder a los recursos solicitados. Por lo tanto, a la hora de programar una aplicación Android, es importante seleccionar sólo los permisos que realmente necesita esa aplicación y justificar la petición al usuario. Ten en cuenta que cuando programes y pruebes aplicaciones en el emulador de tu ordenador, este mensaje no aparece; solamente aparece al instalar las aplicaciones en un dispositivo real. La pantalla que informa de los permisos tiene este aspecto:
El programador puede definir sus propios permisos internos de aplicación para que otra aplicación los utilice. Más adelante veremos cómo se hace. Todos los permisos del sistema comienzan con el texto android.permission y se pueden ver en la documentación oficial de Android para la clase Manifest.permission. Como 258
Intents
hemos dicho, las aplicaciones de terceros pueden tener sus propios permisos. De forma general, resumimos algunos de los permisos más utilizados:
ACCESS_WIFI_STATE: permite a la aplicación acceder a la información de las conexiones WI-FI.
INTERNET: permite a la aplicación acceder a Internet.
READ_CALENDAR, READ_CONTACTS: todos los permisos con el prefijo READ_ permiten a la aplicación leer información de un Content provider de Android. En este caso, estos permisos otorgan acceso de lectura al calendario y a los contactos del teléfono.
WRITE_CALENDAR, WRITE_CONTACTS: todos los permisos con el prefijo WRITE_
permiten a la aplicación modificar información con un Content
provider de Android. En este caso, estos permisos otorgan acceso de escritura al calendario y a los contactos del teléfono. Es posible que una aplicación no reciba la autorización para hacer algo porque nos hayamos olvidado de declarar el permiso necesario en el fichero AndroidManifest. En este caso podemos usar la excepción de tipo SecurityException, que indica el permiso que falta. Existe otra forma de comprobar los permisos de nuestra aplicación en tiempo de ejecución: podemos utilizar el método Context.checkPermission(permiso, paquete) de la clase PackageManager para comprobar si una aplicación tiene concedido un permiso en concreto. Este método devuelve el valor PERMISSION_GRANTED o PERMISSION_DENIED para indicar si la aplicación tiene el permiso concedido o denegado respectivamente. En el Ejemplo 3 de esta Unidad hemos usado este método.
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Permisos de aplicaciones) de la Unidad 5. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos comprobado si la aplicación tiene los permisos adecuados.
Si nos olvidáramos de declarar los permisos necesarios de una aplicación en el fichero AndroidManifest y no controlamos en tiempo de ejecución los permisos que tiene asignados, al ejecutar esta aplicación pulsando en su botón "Vibración simple", por ejemplo, veríamos el siguiente mensaje y la aplicación finalizaría:
259
Para que funcione bien esta aplicación, debemos añadir la siguiente etiqueta en el fichero AndroidManifest:
Este permiso posibilita a la aplicación que el teléfono vibre. Para comprobar que la aplicación tiene asignado este permiso, hemos escrito las siguientes sentencias en la Actividad:
// Cargamos el objeto de gestor de paquetes PackageManager p = this.getPackageManager(); // Buscamos si el paquete de este ejemplo tiene el permiso para vibrar el teléfono if (p.checkPermission("android.permission.VIBRATE", "es.mentor.unidad5.eje3.permisos")==PackageManager.PERMISSION_DENIED) { ...
Así, si borramos la etiqueta que solicita el permiso de vibración en el fichero AndroidManifest.xml, veremos que la aplicación muestra un error y no permite al usuario utilizarla:
260
Intents
En el caso de que la aplicación tenga los permisos adecuados, el usuario puede utilizarla:
5.3.5
forma en que podemos proteger el acceso de otras aplicaciones a nuestras aplicaciones. Existen dos formas complementarias de enfocar la seguridad en aplicaciones Android: 1. Seguridad externa: la aplicación está formada con Actividades e indicamos los permisos necesarios para utilizar recursos de otras aplicaciones. 2. Seguridad interna (autoprotección): la aplicación utiliza Content Providers, Servicios o Receptores de mensajes de difusión (Broadcast Receivers). En este caso hay que controlar qué aplicaciones pueden acceder a la información interna y cómo acceden a ella. El primer procedimiento ya lo hemos visto anteriormente. En el segundo procedimiento de autoprotección de nuestras aplicaciones hay que definir, igualmente, los permisos dentro del archivo AndroidManifest.xml.
261
En este caso, en lugar de escribir la etiqueta , utilizamos la etiqueta para declarar un permiso al sistema. De igual forma, podemos definir uno o varios permisos. La declaración de un permiso es un poco más compleja que usar un permiso. Para ello, debemos proporcionar tres datos:
Nombre simbólico del permiso: el nombre del permiso no puede coincidir con el de otra aplicación. Es recomendable utilizar nombres al estilo de las clases Java de la aplicación. Por ejemplo: es.mentor.ejemplo.VER_LISTADO
Etiqueta del permiso: nombre corto que ve el usuario.
Descripción del permiso: texto más extenso con la descripción del permiso.
Ejemplo de permiso:
Esta definición de permiso sólo informa al sistema operativo de un posible permiso que pueden usar otras aplicaciones; posteriormente, la aplicación debe decidir cómo gestionar estos permisos.
5.3.6
de
las
actividades
y
los
servicios
podemos
definir
el
atributo
android:permission, que indica el nombre del permiso necesario para acceder a ese elemento de la aplicación. Fíjate en este ejemplo:
De esta forma, únicamente las aplicaciones que hayan solicitado el permiso indicado podrán acceder al elemento de forma segura. En este contexto, el “acceso” significa lo siguiente: 262
Intents
Las actividades no pueden ejecutarse sin el permiso.
Los servicios no pueden arrancarse, detenerse o vincularse a una actividad sin el permiso.
Los Receptores de mensajes de difusión ignorarán los mensajes enviados o recibidos a menos que el remitente tenga el permiso.
Los Proveedores de contenidos (Content providers) ofrecen dos atributos diferentes: readPermission y writePermission. Fíjate en este ejemplo:
En el ejemplo anterior, el atributo android:readPermission controla el acceso para hacer consultas al Content provider y android:writePermission controla el acceso para actualizar o borrar su información.
5.3.7
externas están solicitando los permisos necesarios para iniciar un componente de nuestra aplicación:
Los
servicios
pueden
checkCallingPermission().
verificar Este
los
permisos método
usando devuelve
el
método también
PERMISSION_GRANTED o PERMISSION_DENIED dependiendo de si la aplicación tiene el permiso o no. Como parámetro se indica el nombre del permiso que se quiere comprobar.
También cuando invocamos el método sendBroadcast() para enviar un mensaje de difusión (Broadcast) podemos incluir un permiso. Así, sólo los receptores de mensajes de difusión que tengan este permiso podrán recibir el mensaje enviado. Por ejemplo, Android incluye el permiso RECEIVE_SMS para saber cuándo se ha recibido en el teléfono un mensaje SMS nuevo.
263
5.3.8
permisos que necesita ésta. Es decir, si no asignamos bien los permisos en el fichero AndroidManifest, veremos los errores en tiempo de ejecución. Por esto, debemos prestar especial cuidado a la hora de declarar los permisos correctos y probar la aplicación en el emulador de Android. Además,
si
definimos
en
la
aplicación
permisos
internos
a
determinados
componentes, es importante documentar bien estos permisos para que otros desarrolladores puedan aprovechar las capacidades de nuestra aplicación.
5.4
5.4.1
Tab.Layout
Android mediante el uso de diversos componentes de tipo Layout, como los lineales, los absolutos, los relativos, etcétera. Los Layouts son elementos organizativos básicos de la interfaz, pero, teniendo en cuenta el poco espacio disponible en las pantallas de los teléfono o, simplemente, por cuestiones de organización, a veces es interesante dividir la distribución de los componentes en varias pantallas. Una de las formas clásicas de hacerlo en programación es mediante la distribución de los componentes en pestañas (en inglés tabs). Android también permite diseñar este tipo de interfaces de usuario, si bien lo hace de una forma característica, ya que la implementación depende de varios elementos que deben estar distribuidos y estructurados de una forma determinada. Adicionalmente, no es suficiente con definir la interfaz en el fichero XML de diseño que hemos utilizado en otros ejemplos, sino que también es necesario completarlo con algunas sentencias de código. A continuación, en el Ejemplo 4 de esta Unidad, vamos a ver paso a paso cómo implementar este tipo de interfaces. El elemento principal de Android de un conjunto de pestañas es el componente TabHost. Éste es el contenedor principal del conjunto de pestañas y debe tener obligatoriamente como id el valor @android:id/tabhost.
264
Intents
Dentro de éste vamos a incluir un LinearLayout, que se usa para distribuir verticalmente las secciones principales del TabHost, con las pestañas en la parte superior y la sección con el contenido de cada pestaña en la parte inferior. La parte de pestañas se representa mediante un elemento TabWidget, que debe tener el valor id @android:id/tabs. La parte inferior que alberga el contenido de las pestañas se define con el elemento FrameLayout y con el id obligatorio @android:id/tabcontent. Por último, dentro del FrameLayout incluimos el contenido de cada pestaña. Normalmente, cada contenido se define dentro de su propio layout principal (en este ejemplo hemos utilizado un LinearLayout), con un id único que permita, posteriormente, hacer referencia a él fácilmente. En el ejemplo hemos utilizado los identificadores pest1 y pest2. A continuación, se puede ver gráficamente la estructura descrita anteriormente:
Si abrimos el fichero XML del layout, veremos el siguiente código que corresponde a esta estructura:
265
266
Intents
Como puedes observar en el código anterior, por simplificación, en el contenido de las pestañas únicamente hemos añadido la etiqueta de texto “Contenido de la Pestaña Nº Pestaña“. Con el diseño anterior únicamente está montada toda la estructura de componentes necesarios para la nueva pantalla con pestañas. Sin embargo, como ya indicamos al principio de este apartado, esto no completa la funcionalidad. Es necesario asociar cada pestaña con su contenido de forma que el componente de pestañas funcione correctamente al cambiar de pestaña. Esto hay que hacerlo con sentencias Java en la actividad principal. Como viene siendo habitual, lo primero que hacemos es obtener una referencia al componente principal TabHost y lo inicializamos preparándolo para su configuración invocando su método setup(). Después, creamos un objeto de tipo TabSpec para cada una de las pestañas que queramos añadir mediante el método newTabSpec(), al que pasamos como parámetro una etiqueta identificativa de la pestaña; en el ejemplo usamos mipestania1 y mipestania2. Además, también asignamos el Layout del contenido correspondiente a la pestaña en particular con el método setContent(). Finalmente, indicamos el texto y el icono que se muestra en la pestaña mediante el método setIndicator(texto, icono). Para acabar, añadimos la nueva pestaña al componente TabHost mediante el método addTab(). Veamos el código completo: // Obtenemos la referencia al componente TabHost TabHost tabs=(TabHost)findViewById(android.R.id.tabhost); // Preparamos su configuración tabs.setup(); // Preparamos un objeto con referencia a la pestaña 1 TabHost.TabSpec pestania=tabs.newTabSpec("mipestania1"); //Establecemos el contenido de la pestaña 1 pestania.setContent(R.id.pest1); // Definimos la pestaña 1 en el TabHost
267
pestania.setIndicator("Pestaña 1", res.getDrawable(android.R.drawable.ic_menu_agenda)); //Añadimos la pestaña 1 al TabHost tabs.addTab(pestania); // Preparamos un objeto con referencia a la pestaña 2 pestania=tabs.newTabSpec("mipestania2"); //Establecemos el contenido de la pestaña 2 pestania.setContent(R.id.pest2); // Definimos la pestaña 2 en el TabHost pestania.setIndicator("Pestaña 2", res.getDrawable(android.R.drawable.ic_menu_directions)); //Añadimos la pestaña 2 al TabHost tabs.addTab(pestania); // Indicamos la pestaña activa por defecto tabs.setCurrentTab(0);
Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Pestañas) de la Unidad 5. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado el componente TabHost.
Si ejecutamos la aplicación en el emulador de Eclipse, veremos la siguiente pantalla:
268
Intents
Normalmente no se suelen usar los eventos disponibles del componente TabHost, aunque, a modo de ejemplo, vamos a ver el más interesante de ellos, que ocurre cuando el usuario cambia de pestaña. El evento se denomina OnTabChanged e informa de la nueva pestaña que ha seleccionado el usuario. Este
evento
los
podemos
implementar
mediante
el
método
setOnTabChangedListener() de la siguiente manera:
// Definimos el evento OnTabChanged (cuando el usuario ha cambiado de pestaña) tabs.setOnTabChangedListener(new OnTabChangeListener() { @Override public void onTabChanged(String tabId) { Toast.makeText(getBaseContext(), "Pestaña seleccionada: " + tabId, 1).show(); } });
El método onTabChanged() recibe como parámetro el identificador de la pestaña, no su ID, identificador que asignamos al crear el objeto TabSpec con la pestaña correspondiente. En el ejemplo del curso hemos detectado el cambio de pestaña y mostrado un mensaje informativo con el identificador de la nueva pestaña seleccionada. Al cambiar a la segunda pestaña vemos el mensaje “Pestaña seleccionada: mipestania2“:
269
Las Intenciones (Intents) permiten a las aplicaciones de Android expresar la intención que desea ejecutar una acción sobre unos datos usando algún componente de ésta o de otra aplicación. Para crear una Intención hay que usar el objeto Intent de Android. Las Intenciones son mensajes asíncronos que se usan para realizar acciones e intercambiar datos, tanto en la petición como en la respuesta, entre componentes de las aplicaciones. Las intenciones se invocan de dos formas: o Explícita: invocando la clase Java del componente que queremos ejecutar. Normalmente, se hace para invocar componentes de una misma aplicación. o Implícita: invocando la acción y los datos sobre los que aplicar dicha acción. Android selecciona, en tiempo de ejecución, la actividad receptora que cumple mejor con la acción y los datos solicitados. Para que Android pueda iniciar un componente de una aplicación, debe conocer que existe; para ello, se declaran los componentes de una aplicación en el fichero AndroidManifest.xml. La forma en que Android identifica qué componentes pueden responder a una Acción es buscándola en los filtros de intención (intent filters) que se declaran en el archivo "AndroidManifest" de todas las aplicaciones. Es posible emplear intenciones implícitas para proporcionar funcionalidad (acciones) de nuevos componentes de aplicaciones desde los menús de opciones de aplicaciones compiladas y antes de que existiera la acción en el sistema. Android es un sistema operativo que separa privilegios entre aplicaciones que se ejecutan simultáneamente. Una caja de arena, en inglés sandbox, es un sistema de aislamiento de procesos, mediante el cual se pueden ejecutar distintos programas con seguridad y de manera separada. 270
Intents
Para compartir recursos entre aplicaciones, éstas deben declarar al sistema Android los permisos adicionales que necesitan para funcionar correctamente y ampliar su funcionalidad básica. Es posible compartir el mismo sandbox entre dos aplicaciones siempre y cuando estén firmadas con el mismo certificado del autor. Hay dos formas complementarias de enfocar la seguridad en aplicaciones Android: 1. Seguridad externa: indicamos
los
la aplicación está formada con Actividades e
permisos
necesarios
para
utilizar
recursos
de
otras
aplicaciones. 2. Seguridad interna (autoprotección): la aplicación utiliza Content Providers, Servicios o Receptores de mensajes de difusión (Broadcast Receivers). En este caso hay que controlar qué aplicaciones pueden acceder a la información interna y cómo acceden a ella. No existe un mecanismo que detecte automáticamente al compilar los permisos que necesita una aplicación. Por esto, debemos prestar especial cuidado a la hora de declarar los permisos correctos y probar la aplicación en el emulador de Android. Android también permite diseñar interfaces de usuario con pestañas mediante la clase TabHost.
271
BASES DE DATOS Y XML
ÍNDICE 6.1 BASES DE DATOS .................................................................. 275 6.1.1 Introducción ...........................................................................275 6.1.2 Teoría sobre Bases de Datos ..............................................275 6.1.3 Ventajas de las bases de datos ..........................................279 6.1.4 Bases de datos relacionales................................................281 6.1.5 Diseño de bases de datos ...................................................283 6.2 SQLite en Android .................................................................. 284 6.2.1 Gestión de la información en Android ........................... 284 6.2.2 Gestión de la Base de Datos SQLite en Android........... 285 6.2.3 Creación de Bases de datos SQLite.............................. 286 6.2.4 Modificación de la información de BD SQLite............... 292 6.2.4.1 Método insert() .....................................................293 6.2.4.2 Método update() y método delete() ..................294 6.2.5 Uso de parámetros en los métodos SQLite................... 294 6.3 Consultas SQLite en Android ............................................... 295 6.3.1 Selección y recuperación de Consultas de BD SQLite ..295 6.3.2 Ejemplo práctico de BD SQLite con Android ................ 298 6.3.3 Acceso y creación de la Base de datos......................... 299 6.3.4 Recursos de diseño XML ............................................... 303 6.3.5 Actividades..................................................................... 304 6.3.6 Fichero Androidmanifest.xml ......................................... 310 6.4 GESTIÓN DE FICHEROS XML .............................................. 311 6.4.1 SAX es el modelo clásico en Android ............................ 316 6.4.2 SAX simplificado en Android.......................................... 323 6.4.3 DOM en Android ............................................................ 327 6.4.4 StAX en Android............................................................. 331
2
Introducción al entorno Android
6.1
6.1.1
BASES DE DATOS
Introducción En esta Unidad vamos a repasar, como preámbulo , la teoría general sobre bases de
datos. Después, explicaremos cómo gestionar bases de datos SQLite en Android dentro de las aplicaciones. Finalmente, detallaremos el uso de ficheros XML en Android.
6.1.2
Teoría sobre Bases de Datos En
este
apartado
vamos
a
explicar
brevemente
algunos
conceptos
fundamentales sobre las bases de datos. Suponemos que el alumno o alumna ya tiene conocimientos suficientes sobre las mismas y, por tanto, abordaremos sólo algunos conocimientos más relacionados con la forma en que Android accede a las bases de datos y trata su información. El término base de datos es informático, pero puede aplicarse también a la forma como se almacena, ordena y utiliza manualmente la información. Por ejemplo, ya hemos visto en la Unidad 4 cómo leer y almacenar información en ficheros de preferencias o archivos en la memoria del dispositivo Android. La información de estos ficheros puede ser considerada una base de datos en un sentido amplio. Es decir, se almacena y se consulta la información cuando es necesario. Sin embargo, en el sentido informático, la palabra base de datos se refiere a una colección, conjunto o depósito de datos, almacenados en un soporte magnético o de otro tipo, accesibles por múltiples usuarios de forma rápida y eficaz mediante el ordenador a través de una o de varias aplicaciones informáticas independientes de los datos. Éstos se relacionan entre sí y están organizados de tal manera que es fácil introducirlos, actualizarlos, recuperarlos o llevar a cabo con ellos otras operaciones de gestión. En el caso de Android, las bases de datos son privadas y únicamente una aplicación puede acceder a ellas para leer y escribir datos. Cuando una aplicación desea consultar o modificar la información de la base de datos de otra aplicación, Android dispone de los Content Providers que permiten a otras aplicaciones hacer las peticiones necesarias a la aplicación que alberga la base de datos. Ésta devuelve a la aplicación la información solicitada con los resultados de esas operaciones. Android usa SQLite como motor de base de datos relacional. En el siguiente apartado veremos sus características. La información que mostramos a continuación está basada en la versión 3 de SQLite. 275
Generalmente, en las bases de datos relacionales, de las que hablaremos después, la información está almacenada y organizada en ficheros formados por filas y columnas, como puede verse en el ejemplo siguiente, en el que se presentan algunos datos de cinco libros de una biblioteca: Columnas Título
Autor
Editorial
Antonio El invierno en Lisboa Muñoz Molina
Seix Barral
¿Tener o ser?
Fondo de Económica
Erich Fromm
Cultura
Filas Crónica de una Gabriel García Bruguera muerte anunciada Márquez
El lobo estepario
Hermann Hesse
Anaya Editores
La vida está en otra Milan Kundera Seix Barral parte
Cada fila contiene el título, el autor y la editorial de un libro y se relaciona con las demás filas gracias a que incluye el mismo tipo de información (datos de los libros) y en todas ellas la información está organizada de la misma forma: la primera columna contiene el título del libro, la segunda, el autor y la tercera, la editorial. Así pues, una base de datos contiene un conjunto de ficheros cuya información está organizada de tal forma que puede ser tratada informáticamente con rapidez y eficacia. La información de una base de datos puede almacenarse en un solo fichero o en varios. Los ficheros de una base de datos están grabados en el servidor. Tienen un nombre (por ejemplo, flores, ríos, libros, coches, amigos, artículos, clientes, ventas, facturas, etcétera). Su denominación debe seguir las normas establecidas para que el nombre de un fichero sea correcto. Como puede verse, hemos prescindido de las tildes en los identificadores que las llevan ortográficamente. El tipo o extensión de estos ficheros de base de datos puede ser muy variado, según el tipo de base de datos utilizado: en dBase es dbf (Data Base File, Fichero de Base de Datos), en Access mdb, en Interbase db o dbf, en MySQL myd, etcétera. En el caso de SQLite en Android, el archivo de la base de datos suele tener la extensión .db, si bien, puede almacenarse en el directorio específico de la aplicación con
276
Introducción al entorno Android
cualquier extensión e, incluso, sin ella. En otros sistemas operativos, los archivos de una base de datos de tipo SQLite suelen tener la extensión .sqlite. Las filas de un archivo de base de datos se denominan registros y las columnas, campos (fields, en inglés). Así pues, un fichero de base de datos está integrado por registros, que son cada uno de sus elementos o componentes (flor, río, libro, coche, amigo, artículo, cliente, venta o factura). Todos los registros contienen un conjunto de campos en los que se almacena su información; este conjunto define la estructura del fichero que integra una base de datos. En la representación gráfica siguiente puede observarse, en forma de tabla, la estructura de un fichero que contiene una base de datos con información sobre personas: Campos
Nombre Sueldo
Fecha_nac
Observacion Foto
1 2 3 4 Registros 5 6 7 8
En las filas aparecen hasta once registros, cada uno de los cuales, en este caso, contiene los cinco campos siguientes: Nombre, Sueldo, Fecha_nac, Observacion y Foto. En el ejemplo anterior sólo se han incluido once registros y cinco campos, pero de hecho en las bases de datos que vamos a usar el número de registros es ilimitado (depende de la capacidad del soporte) y el de campos es muy amplio, según el tipo de base de datos usada. Todos los registros tienen los mismos campos. Si comparamos un fichero de base de datos con los archivadores de una biblioteca, podemos decir que éstos integran la base de datos. Cada archivador es como un fichero de la base de datos, las fichas que hay en su interior son los registros y los apartados de cada ficha (título, autor, editorial, etcétera) son los campos.
277
Cada campo de un registro tiene un nombre, un tipo, una longitud o ancho, un número de decimales si es de tipo numérico o de coma flotante y un índice opcional. Según el tipo de base de datos que se esté utilizando, el identificador del campo, la clase de tipos y la longitud de los mismos pueden ser diferentes. Vamos a centrarnos en los tipos de campos que define SQLite. Este tipo de base de datos no define todos los tipos de campos típicos en bases de datos relacionales. Únicamente define unos tipos de campos básicos y luego los reutiliza para especificar otros tipos de campos. El nombre de cada campo puede ser muy largo, si bien recomendamos que en el orden práctico sea lo más breve posible y tenga algún significado. Debe atenerse a las reglas de todos los identificadores ya comentadas anteriormente. Hay estos tipos de campos básicos en SQLite: 1. Campo de tipo Carácter. Es el más común (letras, dígitos, signos, etcétera), y contiene información que es tratada como una cadena de caracteres. Se asigna este tipo a un campo cuando no se realizan operaciones aritméticas con sus datos, ni contiene una fecha, ni es un texto mayor de 255 caracteres. Por ejemplo, se asigna este tipo al campo cuyo contenido va a ser el nombre de una persona, sus apellidos, domicilio, localidad, provincia, etcétera. Admite índice. En SQLite hay un tipo único de campo para almacenar datos de esta clase: TEXT que tiene una longitud máxima de 255 caracteres. También podemos usar los siguientes tipos de campos de texto en SQLite: CHARACTER(20): campo de texto con 20 caracteres de longitud. VARCHAR(255): campo de texto de longitud variable hasta 255 caracteres. VARYING CHARACTER(255): similar al anterior. NCHAR(x): campo de texto con x caracteres de longitud. NATIVE CHARACTER(70): campo de texto con 70 caracteres de longitud. NVARCHAR(100): campo de texto de longitud variable hasta 100 caracteres. CLOB: campo similar a TEXT. Todos estos tipos de campo de texto se pueden definir al crear una tabla, si bien, internamente, SQLite los traduce por afinidad al tipo TEXT inicial. 2. Campo de tipo Numérico. Se utiliza para escribir números, incluidos los signos positivo y negativo. Se asigna este tipo a un campo cuando se realizan operaciones aritméticas con números enteros o reales, como sumar, restar, multiplicar, dividir, etcétera. Admite índice. SQLite admite estos valores para determinar los campos de este tipo: INTEGER y REAL. Como puede verse, en realidad los valores posibles se refieren a si es un campo de número entero o decimal. Podemos usar los siguientes tipos de campos de tipo entero en SQLite:
278
Introducción al entorno Android
INT TINYINT SMALLINT MEDIUMINT BIGINT UNSIGNED BIG INT INT2 INT8 Todos estos tipos de campo de número entero se pueden definir al crear una tabla, si bien, internamente, SQLite los traduce por afinidad al tipo INTEGER anterior. En el caso del tipo de campo numérico con decimales, podemos usar los siguientes tipos de campos: DOUBLE DOUBLE PRECISION FLOAT Todos estos tipos de campo de número con decimales se pueden definir al crear una tabla, si bien, internamente, SQLite los traduce por afinidad al tipo REAL anterior. 3. Campo de tipo Fecha y Lógico. Puede contener fechas y tiempos (horas, minutos, segundos) o almacenar valores lógicos (true / false). Admite índice. SQLite define el tipo de campo interno NUMERIC para almacenar otros tipos de campos necesarios en las aplicaciones en una tabla, tales como los campos lógicos o de fecha, así como los que establecen los decimales exactos en un campo numérico. Podemos usar los siguientes tipos de campos en SQLite: DECIMAL(10,5) BOOLEAN DATE DATETIME Todos estos tipos de campo se pueden definir al crear una tabla, si bien, internamente, SQLite los traduce por afinidad al tipo NUMERIC anterior. 4. Campo de tipo Memo. Es un campo de longitud variable que admite gran cantidad de texto o datos binarios según nuestras necesidades. Para cada registro tendrá una longitud distinta, según la cantidad de datos que se introduzcan en este campo. No admite índice. SQLite admite únicamente BLOB.
6.1.3
Ventajas de las bases de datos Hemos dicho que los archivadores de una biblioteca o de una agenda pueden
considerarse, en cierta forma, bases de datos, pues en ellos se almacena información en un 279
determinado orden y es posible buscar esta información, consultarla, modificarla o eliminarla con facilidad. Sin embargo, todas estas operaciones suelen llevar mucho tiempo y, en ocasiones, no se efectúan tan fácilmente como desearíamos. Además, ocupan bastante espacio si la información es abundante. Incluso, en ocasiones, algunas operaciones fundamentales son imposibles de realizar manualmente. Por ejemplo, si tenemos 1.000 fichas bibliográficas ordenadas por autor y necesitamos ordenarlas por título, la operación ha de realizarse manualmente, mirando una a una cada ficha, lo cual puede hacerse muy largo y pesado. Podíamos haber escrito dos ejemplares de cada ficha, uno para el archivo por autores y otro para el de títulos, pero esto hubiera llevado el doble de tiempo, de trabajo y éstas ocuparían el doble de espacio. Supongamos ahora que necesitamos seleccionar todas las fichas en las que aparece la misma editorial. De nuevo la tarea puede parecernos pesada y larga, y lo es. No digamos si se cambia la situación de los libros en los armarios de la biblioteca. También será necesario modificar la signatura en las fichas. Hemos puesto este ejemplo para explicar los graves problemas que se derivan de la gestión manual de la información. Las dificultades aumentan a medida que crece el volumen de información que debe manejarse y según sean los criterios de ordenación y selección. En una base de datos informática, en cambio, al gestionarse la información automáticamente, muchos de los problemas anteriormente mencionados desaparecen. En primer lugar, la rapidez de las operaciones fundamentales (introducción de datos, ordenación por diferentes campos, consultas, búsquedas, elaboración de informes, actualización y modificación de los datos, etcétera) aumenta de una forma muy destacada. En segundo lugar, el espacio que ocupa una base de datos es mucho menor que el de cualquier otra forma de archivo manual. En un disco flexible de 3,5 pulgadas puede almacenarse casi un millón y medio de caracteres. En los discos duros de los actuales servidores el volumen de información puede ser prácticamente ilimitado. En tercer lugar, las operaciones fundamentales de gestión de la información son automáticas, lo cual hace que sean menos pesadas y tediosas si son llevadas a cabo por el ordenador. Así pues, el trabajo se humaniza y el tiempo libre de las personas que manejan la información es mayor. Finalmente, la seguridad de los datos informatizados también es mayor que la contenida en archivos de tipo manual, pues el ordenador nos permite hacer rápidamente cuantas copias queramos de esa información en diferentes soportes. Desde la aparición de los ordenadores, éstos se han dedicado al almacenamiento y organización de grandes volúmenes de datos. Igualmente, se han aplicado a la evaluación de 280
Introducción al entorno Android
las diversas soluciones propuestas para resolver los problemas de estructuración y acceso a dicha información.
6.1.4
Bases de datos relacionales Se ha descubierto que la mejor forma de resolver estos problemas es organizar la
información de forma relacional. De aquí ha surgido el concepto de bases de datos relacionales (RDBMS, Relation DataBase Management System). El fundamento teórico de las bases de datos relacionales es complejo, ya que se basa en el concepto matemático de relación entre los elementos de un conjunto. Sus características y propiedades formales requieren ciertos conocimientos de la teoría de conjuntos. Sin embargo, en la práctica, el concepto de relación es muy sencillo de utilizar porque en ésta la organización de los datos es muy clara e intuitiva. En otros tipos de organización de la información, como las bases de datos jerárquicas o las bases de datos en red, anteriores a las relacionales, aparecían distintas categorías de datos y estructuras muy complejas y poco flexibles que dificultaban la posibilidad de relacionar éstos con eficacia y rapidez. En cambio, en las bases de datos relacionales la información se organiza en ficheros que tienen estructura tabular o en forma de tabla, en la que todos los datos tienen la misma categoría. Cada tabla también recibe el nombre de relación. Por ejemplo, en el gráfico siguiente puede observarse una tabla que contiene diversos datos de personas: Cabecera
Nombre
Dirección
Edad
Sexo
Profesión
1
León García
C/ Zurita, 25
25
V
Admtvo.
2
María Pérez
C/ Flores, 15
30
M
Abogada
C/ Río Sil, 11
50
V
Dependiente
Filas (Registros) 3
José Rodríguez
4
Juana de Dios
Avda. Canarias, 50
70
M
Jubilada
5
Begoña López
Pza. Segovia, s/n
15
M
Estudiante
Columnas (Campos)
Como se ve, una tabla consta de filas y de columnas; en cada columna, denominada campo en la base de datos, hay un dato: Nombre, Dirección, Edad, etcétera; cada fila es un registro que contiene todos los datos de los elementos de la base. Cada tabla tiene un 281
registro especial, denominado cabecera, que contiene los nombres de los campos y sus atributos (tipo y longitud). Generalmente, una base de datos no consta de una sola tabla, sino de varias. Gráficamente puede representarse así:
Estas tablas no son independientes unas de otras, sino que tienen al menos un campo común con las otras a través del cual se puede acceder a la información que contienen todas en conjunto. Por ejemplo, la base de datos de una biblioteca puede estar integrada por una tabla de libros, otra de lectores, otra de préstamos y otra de editoriales. El fichero de libros puede contener la información completa de cada volumen: título, autor, editorial, año de edición, precio, número de páginas, código de materia, número de registro, etcétera. El fichero de editoriales contendrá los datos de cada entidad editora: nombre, dirección, teléfono, plazo de entrega, descuentos, etcétera. El fichero de lectores estará integrado por los datos personales y profesionales de éstos: nombre, DNI, dirección, teléfono, profesión, centro de trabajo, número de carné, etcétera. El fichero de préstamos contendrá datos de este tipo: número de registro del libro prestado, número de carné del lector, fecha del préstamo, plazo, etcétera. Como puede verse, la información no debe repetirse en todos los ficheros, pero sí debe poder relacionarse. Por ejemplo, los ficheros de libros y editoriales, tienen en común el campo EDITORIAL. Los ficheros de libros y préstamos tienen en común, al menos, el NÚMERO DE REGISTRO del libro prestado, gracias a lo cual desde uno se puede acceder a los datos del otro. Los ficheros de lectores y préstamos tienen en común el campo CARNÉ, etcétera. 282
Introducción al entorno Android
Son bases de datos relacionales Microsoft Access, Oracle, SQL Server, MySQL, SQLite y otras.
6.1.5
Diseño de bases de datos El diseño de bases de datos puede presentar distinto tipo de dificultad dependiendo
de la complejidad e interrelación de los datos que se quiera gestionar. Imaginemos que una compañía aérea quiere gestionar toda la información contenida en una base de datos relativa a los aviones y su mantenimiento, a los vuelos, viajes, destinos, clientes, personal de la empresa, agencias de viajes, billetes, asistencia, etcétera. Es evidente que, en este caso, la complejidad es enorme y que para realizar el diseño de esta base se requiere la colaboración de técnicos especialistas que faciliten la tarea. Sin embargo, en la mayoría de las ocasiones el diseño de una base de datos se resuelve con uno, dos o tres ficheros como máximo. En este caso no es necesario profundizar en aspectos complejos de técnicas de diseño, sino que basta aplicar el sentido común para organizar los ficheros de la base de datos de forma coherente. Deben crearse tantos ficheros como categorías o grupos de elementos distintos haya que organizar. Por ejemplo, en una tienda que vende al por menor bastaría con crear un fichero de artículos y otro de proveedores, y a lo sumo otros tres: de pedidos, de ventas y de clientes. Antes de ponerse a crear una base de datos con el ordenador, es preciso diseñarla previamente sobre el papel. La planificación es fundamental en este caso para evitar errores graves: falta de datos necesarios, repetición innecesaria de algunos, equivocación del tipo de campo o falta de precisión en su longitud. Aunque es posible modificar la estructura de una base de datos, una vez creada, se puede perder mucho tiempo e incluso datos en esta operación. Diseñar una base de datos consiste en determinar los datos que van a introducirse en ella, la forma como se van a organizar y el tipo de esos datos. Además, se debe precisar la forma como se van a solicitar y las clases de operaciones que hay que realizar con los mismos: aritméticas, lógicas, de fechas, de carácter, etcétera. También conviene conocer los resultados concretos
que
se
espera
obtener: consultas, informes, actualizaciones,
documentos, etcétera. A continuación, se resumen las operaciones que deben llevarse a cabo al diseñar una base de datos: 1. Atendiendo a la información que contiene es preciso: • Identificar los diferentes elementos informativos (artículos, clientes, ventas, facturas, etcétera) que forman parte de la base de datos. 283
• Determinar los datos que debe contener cada uno de esos elementos. • Precisar el grado de necesidad y de utilización de cada dato. • Concretar las operaciones que se van a realizar con los datos: aritméticas, lógicas, de salida sólo por la pantalla, de salida también por la impresora, etcétera. • Seleccionar el dato o datos esenciales que deben ser el campo clave por el que se ordenarán las unidades o elementos mencionados. • Fijar los datos comunes a los diferentes ficheros de la base de datos que van a permitir relacionar la información distribuida entre ellos. 2. Atendiendo a la estructura de la base de datos • Distribuir la información en ficheros según los diferentes grupos que se hayan hecho (artículos, clientes, etcétera) y dar un nombre a cada fichero. • Determinar el nombre de cada campo de los registros de cada fichero. Este nombre ha de ser claro y debe significar algo para que pueda recordarse fácilmente. • Decidir qué tipo conviene asignar a cada campo según la clase de operaciones que vayamos a realizar con sus datos. • Asignar a cada campo una longitud apropiada para tener los datos fundamentales sin despilfarro de memoria interna ni de espacio en el disco duro o soporte empleado. • Establecer un orden lógico y práctico agrupando los campos según un criterio concreto: clase e importancia de los datos, frecuencia de utilización, proximidad, parecido, etcétera. • Decidir cuál o cuáles van a ser los campos clave permanentes y situarlos al principio de la estructura. • No incluir campos que puedan ser el resultado de diversas operaciones de tratamiento posterior. • Fijar los campos comunes a todos los ficheros para poder relacionarlos con otros de la misma aplicación.
6.2
6.2.1
SQLite en Android
Gestión de la información en Android Como ya hemos estudiado, en Android existen tres formas de almacenar información
para usarla en las aplicaciones: 284
Preferencias de la aplicación
Introducción al entorno Android
Ficheros locales en el sistema de archivos del sistema operativo
Base de datos SQLite
En la Unidad 4 hemos tratado las dos primeras formas y en esta Unidad 6 veremos las bases de datos.
6.2.2
Gestión de la Base de Datos SQLite en Android SQLite es un motor de bases de datos relacional muy popular por sus características,
que son muy especiales, como las siguientes:
No necesita un servidor, ya que la librería se enlaza directamente en la aplicación al compilarla.
Ocupa muy poco tamaño: sólo unos 275 KB.
Precisa de poca o nula configuración.
Es posible hacer transacciones.
Es de código libre.
Android incorpora todas las herramientas necesarias para la creación y gestión de bases de datos SQLite mediante una API completa. Después iremos viendo otros comandos para realizar consultas más complejas. Usar bases de datos Android puede hacer más lentas las aplicaciones debido a que es necesario escribir y leer información de la memoria física del dispositivo. Por lo tanto, es recomendable realizar esta operaciones de forma Asíncrona, tal como hemos estudiado en la Unidad 3 (Hilos). En los ejemplos de esta Unidad no vamos a incluir hilos, para mostrar únicamente las sentencias de SQLite.
285
6.2.3
Creación de Bases de datos SQLite La forma usual en Android de crear, modificar y conectar con una base de datos
SQLite consiste en usar la clase Java SQLiteOpenHelper. En realidad, debemos definir una clase propia que derive de ella y personalizarla para adaptarnos a las necesidades concretas de la aplicación. La clase SQLiteOpenHelper define un único constructor que, normalmente, no es necesario reescribir y los dos métodos abstractos onCreate() y onUpgrade() que tendremos que implementar con el código Java necesario para crear la base de datos acorde con las necesidades de la aplicación. También debemos hacerlo para modificar su estructura si hace falta cambiar los campos que definen alguna tabla al actualizar la versión de la aplicación. En el Ejemplo 1 de esta Unidad, vamos a crear una base de datos muy sencilla llamada BDBiblioteca.db, con una única tabla interna llamada Ejemplares que albergará únicamente cinco campos:
_id: id registro de tipo INTEGER, índice PRIMARIO y autoincremental
título: de tipo TEXT
autor: de tipo TEXT
año: de tipo INTEGER
prestado: BOOLEAN
En el caso de Android es obligatorio definir un índice primario en la tabla con el identificador “_id” para poder extraer, de manera sencilla, la información de los registros de una consulta SQL mediante cursores usando la clase Cursor de Android.
Para
esto,
vamos
a
crear
la
clase
BibliotecaSQLiteHelper
derivada
de
SQLiteOpenHelper, donde reescribimos los métodos onCreate() y onUpgrade() para adaptarlos a la estructura de campos anterior:
// La clase se debe heredar de SQLiteOpenHelper public class BibliotecaSQLiteHelper extends SQLiteOpenHelper { //Sentencia SQL para crear la tabla Ejemplares static String createBDSQL = "CREATE TABLE Ejemplares (_id integer primary key autoincrement, titulo TEXT, autor TEXT, anio TEXT, prestado BOOLEAN)";
// Definimos el constructor indicando el contexto de la aplicación, // el nombre de la base de datos y la versión de la BD
286
Introducción al entorno Android
public BibliotecaSQLiteHelper(Context contexto, String nombre, CursorFactory factory, int version) { super(contexto, nombre, factory, version); }
@Override // Si la BD no existe, Android llama a este método public void onCreate(SQLiteDatabase db) { //Se ejecuta la sentencia SQL de creación de la tabla db.execSQL(createBDSQL); }
@Override public void onUpgrade(SQLiteDatabase db, int versionAnterior, int versionNueva) { /* NOTA: para simplificar este ejemplo eliminamos directamente la tabla * anterior y la creamos de nuevo. * Sin embargo, lo normal sería migrar los datos de la tabla antigua * a la nueva estructura de campos, por lo que las sentencias podrían * ser del estilo ALTER TABLE. */ //Se elimina la versión anterior de la tabla db.execSQL("DROP TABLE IF EXISTS Ejemplares");
//Se crea la nueva versión de la tabla db.execSQL(createBDSQL); } }
En el código fuente anterior se define la variable estática (en Java se definen así las constantes) createBDSQL, donde se establece la orden SQL para crear la tabla llamada Ejemplares con los campos alfanuméricos descritos anteriormente. ATENCIÓN: en este curso no se describe la sintaxis del lenguaje SQL, pues se considera que el alumno o alumna conoce cómo usar una base de datos relacional.
287
El método onCreate() se ejecuta automáticamente cuando es necesario crear la base de datos, es decir, cuando aún no existe y se instala la aplicación por primera vez. Por lo tanto, en este método debemos crear todas las tablas necesarias y añadir, si fuera necesario, los registros iniciales. Para la creación de la tabla hemos aplicado la sentencia SQL ya definida en la constante y la ejecutamos en la base de datos utilizando el método más sencillo disponible en la API SQLite de Android execSQL(SQL). Este método ejecuta directamente la orden SQL incluida como parámetro. Por otra parte, el método onUpgrade() se ejecuta automáticamente cuando sea necesario actualizar la estructura de la base de datos al cambiar la versión de la aplicación que la alberga. Por ejemplo, desarrollamos la versión 1 de la aplicación que utiliza una tabla con los campos descritos en el ejemplo anterior. Más adelante, ampliamos la funcionalidad de la aplicación desarrollando la versión 2, que incluye en la tabla el campo "Editorial". Si un usuario tiene instalada la versión 1 de la aplicación en su dispositivo Android, la primera vez que ejecute la versión 2 de la aplicación hay que modificar la estructura de la tabla, para añadir el nuevo campo; en este caso, Android ejecutará automáticamente el método onUpgrade(). Este método recibe como parámetros la versión actual de la base de datos en el sistema y la nueva versión a la que se quiere convertir. En función de esta información debemos realizar unas acciones u otras. Por ejemplo, modificar la tabla con la orden "ALTER TABLE" para añadir el nuevo campo. Una vez que hemos implementado la clase SQLiteOpenHelper, podemos abrir fácilmente la base de datos desde la aplicación Android. Lo primero que hacemos es crear un objeto de la clase BibliotecaSQLiteHelper al que pasamos el contexto de la aplicación (en el ejemplo pasamos la referencia a la actividad principal), el nombre de la base de datos, un objeto CursorFactory (más adelante veremos cómo funcionan los cursores, en ese caso pasamos el valor null) y, por último, la versión de la base de datos de la aplicación. Al crear este objeto pueden ocurrir varias cosas:
Si ya existe la base de datos y su versión actual coincide con la solicitada, se conecta con ella.
Si la base de datos existe, pero su versión actual es anterior a la solicitada, se invocará automáticamente al método onUpgrade(), para convertir la base de datos a la nueva versión y conectarla con la base de datos convertida.
Si la base de datos no existe, se llamará automáticamente al método onCreate() para crearla y se conectará con la base de datos creada.
288
Introducción al entorno Android
Una vez obtenida una referencia al objeto BibliotecaSQLiteHelper, podemos invocar el método getReadableDatabase() para realizar consultas a la base de datos o el método getWritableDatabase() para llevar a cabo modificaciones, . Después, utilizando el método execSQL(), podemos ejecutar órdenes SQL para consultar o modificar registros. En el Ejemplo 1 se insertan cinco registros de prueba. Por último, cerramos la conexión con la base de datos llamando al método close(). A continuación, vemos el aspecto que tiene el código fuente de la actividad principal:
//Abrimos la base de datos 'DBBiblioteca.db' BibliotecaSQLiteHelper bibliodbh = new BibliotecaSQLiteHelper(this, "DBBiblioteca.db", null, 1); // Modo escritura SQLiteDatabase db = bibliodbh.getWritableDatabase(); resultado.append("- La base de datos DBBiblioteca se ha abierto correctamente."); //Si se ha abierto correctamente la base de datos, entonces cargamos algunos registros... if(db != null) { // Hemos definido los datos en un fichero de recursos de la aplicación Resources res = getResources(); String titulos[] = res.getStringArray(R.array.titulos); String autores[] = res.getStringArray(R.array.autores); String anios[] = res.getStringArray(R.array.anios); String prestados[] = res.getStringArray(R.array.prestados); //Insertamos 5 libros de ejemplo for(int i=0; i<5; i++) { String SQLStr="INSERT INTO Ejemplares (titulo, autor, anio, prestado) " + "VALUES ('" + titulos[i] +"', '" + autores[i] +"', " + anios[i] +", '" + prestados[i] +"')"; resultado.append("\n- SQL ejecutada: "+SQLStr); //Insertamos los datos en la tabla Ejemplares db.execSQL(SQLStr); }
289
//Cerramos la base de datos db.close(); resultado.append("\n- La base de datos DBBiblioteca se ha cerrado."); }
Android almacena los archivos de la base de datos en la memoria interna del dispositivo, en un directorio determinado, siempre el mismo, sin que el programador pueda cambiarlo. Es el siguiente: /data/data/paquete_java/databases/nombre_del_fichero En el ejemplo anterior se almacena en: /data/data/es.mentor.unidad6.eje1.crearbd/databases/DBBiblioteca.db Si ejecutas el Ejemplo 1 de esta Unidad, puedes comprobar en el DDMS cómo se crea el fichero correctamente en el directorio indicado. Para acceder a esta herramienta en Eclipse hay que hacer Clic en la opción del menú principal: Window -> Open Perspective -> DDMS:
Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Crear base de datos SQLite) de la Unidad 6. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado los métodos de la base de datos SQLite.
Hemos visto que el fichero de la base de datos del Ejemplo 1 se ha creado en la ruta correcta. Para comprobar que la tabla se ha creada correctamente y hemos insertado los registros en la misma, podemos usar dos métodos: 290
Introducción al entorno Android
1. Transferir la base de datos a nuestro PC y consultarla con cualquier administrador de bases de datos SQLite. Esto resulta un poco incómodo si sólo necesitamos hacer una pequeña consulta, pero, a veces, es imprescindible hacerlo para depurar errores en consultas SQL complejas. 2. Usar la consola de comandos del emulador de Android y recurrir a comandos SQL para acceder y consultar la base de datos SQLite. El primer método es simple. Podemos transferir el fichero de la base de datos a nuestro PC utilizando el botón de descarga situado en la esquina superior derecha del explorador de archivos (enmarcado en rojo en la imagen anterior). Al lado de este botón hay otro botón para hacer la operación contraria, es decir, copiar un fichero local al sistema de archivos del emulador. Además, hay otro botón para eliminar ficheros del emulador. NOTA: a veces, al desarrollar una aplicación Android con bases de datos, el programador debe eliminar a mano un fichero porque la estructura creada no es correcta y Android no elimina el fichero automáticamente cada vez que cargamos la aplicación en el emulador de Eclipse. Una vez hemos descargado el fichero a nuestro PC, podemos utilizar cualquier administrador de SQLite para abrir y consultar la base de datos. En el segundo método accedemos de forma remota al emulador a través de su consola de comandos (shell). Vamos a ver cómo hacerlo. Ten en cuenta que para que este método funcione debemos haber incluido bien el PATH del SDK de Android en el sistema operativo del PC donde trabajemos. En caso de duda, conviene repasar el documento de Instalación de Android de este curso. Con el emulador de Android ejecutándose, abrimos una consola de Windows (o del sistema operativo correspondiente) y utilizamos la utilidad adb.exe (Android Debug Bridge) situada en la carpeta platform-tools del SDK de Android. En primer lugar, consultamos todos los identificadores de los emuladores en ejecución mediante el comando "adb devices". Este comando debe devolver una única instancia con el emulador abierto, que en el ejemplo se denomina “emulator-5554“. Tras obtener este identificador del emulador activo vamos a acceder a su shell mediante el comando “adb -s shell“. Una vez conectados a la consola del emulador, podemos acceder a la base de datos utilizando el comando sqlite3 e indicando la ruta del fichero de la base de datos; en el caso del ejemplo debemos escribir “sqlite3 /data/data/es.mentor.unidad6.eje1.crearbd/databases/DBBiblioteca.db“. A continuación, debe aparecer el prompt de SQLite “sqlite>“, que nos indica que ya podemos escribir consultas SQL sobre la base de datos.
291
Vamos a comprobar que se han insertado bien los cinco registros del ejemplo en la tabla Ejemplares. Para ello, escribimos la siguiente orden: “SELECT * FROM Ejemplares;“. Si la orden está escrita correctamente, veremos el resultado en la pantalla; si no, se mostrará el error correspondiente. En la imagen siguiente se muestra el resultado de todos los comandos:
Para salir del cliente SQLite debemos escribir el comando ".exit" (fíjate que lleva un punto delante) y para abandonar la shell del emulador debemos escribir el comando "exit".
6.2.4
Modificación de la información de BD SQLite La librería de SQLite incluida en Android proporciona dos formas para llevar a cabo
operaciones sobre una base de datos que no devuelven resultados. Por ejemplo, añadir, actualizar y eliminar registros de una tabla; también se puede crear tablas, índices de búsqueda, etcétera. La primera forma. que ya la hemos visto anteriormente, consiste en usar el método execSQL() de la clase SQLiteDatabase. Este método permite ejecutar cualquier orden SQL sobre la base de datos siempre que no sea necesario obtener los resultados de la orden. Ya hemos utilizado este método indicando como parámetro la cadena de texto de la orden SQL. Aunque ya hemos visto en el Ejemplo 1 cómo se usa este método, a continuación, mostramos algunos ejemplos más:
292
Introducción al entorno Android
//Insertar un registro db.execSQL("INSERT INTO Ejemplares (titulo, autor, anio, prestado) VALUES ('Título', 'Autor', 2001, 'false')”); //Eliminar un registro db.execSQL("DELETE FROM Ejemplares WHERE _id=1"); //Actualizar un registro db.execSQL("UPDATE Ejemplares SET autor='Nombre' WHERE _id=1");
La segunda forma disponible en Android consiste en utilizar los métodos insert(), update() y delete() proporcionados también por la clase SQLiteDatabase. Estos métodos permiten añadir, actualizar y eliminar registros de la tabla mediante parámetros, el valor del campo y las condiciones en que debe aplicarse la operación. Veamos un ejemplo de cada uno de ellos: 6.2.4.1
Método insert()
Este método se usa para añadir nuevos registros en una tabla de la base de datos. Al invocar insert (String table, String nullColumnHack, ContentValues values), es necesario definir tres parámetros:
table: nombre de la tabla en la que insertamos un registro.
nullColumnHack: sólo es necesario en casos muy puntuales, por ejemplo, al insertar registros completamente vacíos. Normalmente debemos indicar el valor null en este segundo parámetro.
values: valores del registro que se inserta.
Los valores que queremos insertar los pasamos como elementos de una colección de tipo ContentValues. Esta colección es del tipo duplos de clave-valor, donde la clave es el nombre del campo de la tabla y el valor es el dato que debemos insertar en dicho campo. Veamos un ejemplo sencillo: //Creamos el registro a partir del objeto ContentValues ContentValues nuevoRegistro = new ContentValues(); nuevoRegistro.put("titulo", "Título de la obra"); nuevoRegistro.put("autor","Nombre del autor"); ... //Insertamos el registro en la tabla de la base de datos db.insert("Ejemplares", null, nuevoRegistro);
293
Este método devuelve el campo ID del nuevo registro insertado o el valor -1 si ocurre algún error durante la operación.
6.2.4.2
Método update() y método delete()
Estos métodos se usan para actualizar o borrar registros de una tabla. Los métodos update (String table, ContentValues values, String whereClause, String[] whereArgs) y delete(String table, String whereClause, String[] whereArgs) se invocan de manera parecida a insert(). En estos métodos hay que usar el parámetro adicional whereArgs para indicar la condición WHERE de la orden SQL. Por ejemplo, para actualizar el autor del usuario de id 1 escribimos lo siguiente: //Establecemos los campos-valores que actualizamos ContentValues valores = new ContentValues(); valores.put("autor","Otro autor"); //Actualizamos el registro de la tabla db.update("Ejemplares", valores, "_id=1");
En el tercer parámetro del método update() indicamos la condición tal como haríamos en la cláusula WHERE en una orden UPDATE de SQL. El método delete() se aplica de igual forma. Por ejemplo, para eliminar el registro 2 escribimos lo siguiente:
//Eliminamos el registro del _id 2 db.delete("Ejemplares", "_id=2");
De nuevo, indicamos como primer parámetro el nombre de la tabla y como segundo la condición WHERE. Si queremos vaciar toda la tabla, podemos indicar null en este segundo parámetro.
6.2.5
Uso de parámetros en los métodos SQLite En el caso de los métodos execSQL(), update() y delete() de SQLiteDatabase
podemos utilizar argumentos como condiciones de la sentencia SQL. De esta manera, podemos prescindir de SQL formadas con cadenas de texto muy largas y así evitamos errores de codificación. 294
Introducción al entorno Android
Estos argumentos son piezas variables de la sentencia SQL, en forma de matriz, que evitan tener que construir una sentencia SQL concatenando cadenas de texto y variables para formar la orden final SQL. Estos argumentos SQL se indican con el símbolo ‘?’ y los valores de dichos argumentos deben pasarse en la matriz en el mismo orden que aparecen en la sentencia SQL. Fíjate en el siguiente ejemplo: //Elimina un registro con execSQL() utilizando argumentos String[] args = new String[]{"Nombre de autor"}; db.execSQL("DELETE FROM Ejemplares WHERE autor=?", args); //Actualiza dos registros con update() utilizando argumentos ContentValues valores = new ContentValues(); valores.put("Título 1","Título 2"); String[] args = new String[]{“1”, "2"}; db.update("Ejemplares", valores, "_id=? OR _id=?", args);
6.3
6.3.1
Consultas SQLite en Android
Selección y recuperación de Consultas de BD SQLite A continuación, vamos a describir la manera de hacer consultas a una base de datos
SQLite desde Android y de extraer la información de datos del resultado. Existen dos formas de buscar y recuperar registros de una base de datos SQLite. La primera de ellas consiste en utilizar directamente un comando de consulta SQL; la segunda forma consiste en utilizar un método específico con parámetros de consulta a la base de datos. La primera forma se basa en la utilización del método rawQuery() de la clase SQLiteDatabase, que ya hemos estudiado en el apartado anterior. En este indicamos directamente como parámetro el comando SQL que queremos usar en la consulta señalando los campos seleccionados y los criterios de selección. El resultado de la consulta lo obtenemos en forma de Cursor. La clase Cursor permite acceder en modo lectura/escritura a los resultados devueltos por una consulta a la base de datos. Esta clase Cursor se puede usar con varios hilos (subprocesos) para obtener asíncronamente información de una BD. Fíjate en el siguiente ejemplo: Cursor c = db.rawQuery(" SELECT autor,titulo FROM Ejemplares WHERE _id=1");
295
Tal y como hemos visto anteriormente en algunos métodos de modificación de datos, también es posible incluir en este método una lista de argumentos variables que indicamos en la orden SQL con el símbolo ‘?‘; por ejemplo, así: String[] args = new String[] {"1"}; Cursor c = db.rawQuery(" SELECT autor,titulo FROM Ejemplares WHERE _id=? ", args);
La segunda forma de obtención de datos se basa en utilizar el método query() de la clase SQLiteDatabase. Este método query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit) se invoca con varios parámetros:
table: nombre de la tabla consultada
columns: matriz con los nombres de los campos seleccionados
selection: cláusula WHERE del lenguaje SQL
selectionArgs: matriz con los argumentos variables incluidos en el WHERE o null si no se indican.
groupBy: cláusula GROUP BY si existe; si no, escribimos null.
having: cláusula HAVING si existe; si no, escribimos null.
orderBy: cláusula ORDER BY si existe; si no, escribimos null.
limit: número máximo de registros devueltos por la consulta.
Veamos el mismo ejemplo anterior utilizando el método query():
String[] campos = new String[] {"autor", "titulo"}; String[] args = new String[] {"1"};
Cursor c = db.query("Ejemplares", campos, "_id=?", args, null, null, null);
Tanto en la primera forma como en la segundo, ambos métodos devuelven como resultado de su ejecución un objeto de tipo Cursor, que debemos recorrer para procesar los registros obtenidos. La clase Cursor dispone de varios métodos para recorrer y manipular los registros devueltos por la consulta. Entre ellos podemos destacar dos de los dedicados a recorrer el cursor de forma secuencial y en orden natural:
moveToFirst(): mueve el puntero del cursor al primer registro devuelto. Si no hay ningún primer registro este método devuelve false.
296
Introducción al entorno Android
moveToNext(): mueve el puntero del cursor al siguiente registro devuelto. Si no hay ningún registro después, este método devuelve false.
moveToPrevious(): mueve el cursor al registro anterior. Si no hay ningún registro anterior, este método devuelve false.
getCount(): devuelve el número de registros devueltos por la consulta.
getColumnIndexOrThrow(String columna): devuelve el índice de la columna dada o lanza la excepión IllegalArgumentException si no existe la columna.
getColumnName(int indice): devuelve el nombre de la columna indicada en el índice dado.
getColumnNames(): devuelve una matriz con los nombres de las columnas seleccionadas.
moveToPosition(int posicion): mueve el cursor al registro que hay en esa posición. Si no hay ningún registro en esa posición, este método devuelve false.
getPosition(): devuelve la posición actual del cursor.
Una vez colocado el cursor en el registro que queremos leer, podemos utilizar cualquiera de los métodos getXXX(índice_columna) existentes para cada tipo de dato y así recuperar el dato de cada campo de ese registro. Por ejemplo, si queremos recuperar la segunda columna del registro actual y ésta contiene un campo alfanumérico, usamos la sentencia getString(1). La primera columna de la consulta tiene el índice 0, la segunda columna tiene índice 1 y así sucesivamente. En el caso de que la columna contenga un dato de tipo real, ejecutaríamos la sentencia getDouble(1). Teniendo todo esto en cuenta, veamos, a continuación, cómo recorrer todos los registros devueltos por la consulta del ejemplo anterior usando un cursor: String[] campos = new String[] {"autor", "titulo"}; String[] args = new String[] {"1"};
Cursor c = db.query("Ejemplares", campos, "_id=?", args, null, null, null);
//Comprobamos que existe, al menos, un registro if (c.moveToFirst()) {
297
//Recorremos el cursor mientras haya registros sin leer do { String autor = c.getString(0); String titulo = c.getString(1); } while(c.moveToNext()); }
Además de los métodos comentados de la clase Cursor, existen muchos más métodos que pueden ser muy útiles. El alumno o la alumna puede consultar la lista completa en la documentación oficial de la clase Cursor. Como hemos comentado ya en esta Unidad, las bases de datos SQLite de una aplicación son siempre privadas e internas a esta aplicación. Para que el resto de aplicaciones pueda acceder a la información de la BD, Android define los Content Provider. En la Unidad 7 tratamos este tema en profundidad. Además, se pueden tratar datos dinámicos de una base de datos usando la clase de Android SQLiteQueryBuilder. Esta clase es similar a la interfaz de un proveedor de contenidos, por lo que suele utilizarse conjuntamente con los Content Providers.
NOTA: Existen funcionalidades más avanzadas de gestión de BD con Android, como la utilización de transacciones, pero no vamos a tratarlas en este curso por considerarse programación avanzada.
6.3.2
Ejemplo práctico de BD SQLite con Android A continuación, vamos a mostrar mediante un ejemplo completo cómo se usan todos
los métodos de acceso a base de datos que hemos estudiado hasta ahora.
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Notas) de la Unidad 6. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado métodos de la base de datos SQLite.
Si ejecutas la aplicación, verás que tiene el siguiente aspecto:
298
Introducción al entorno Android
Se trata de una aplicación donde un usuario puede gestiona notas sencillas por categorías. Estas notas se almacenan en la base de datos "bdnotas.db" en la tabla "notas" que tiene la siguiente estructura:
_id: índice de registro de tipo entero con índice primario autoincremental
categoria: texto no nulo
titulo: texto no nulo
descripcion: texto no nulo
La aplicación está formada por dos actividades: la primera muestra todas las notas en un listado y la segunda permite editarlas o dar de alta una nueva. Ambas actividades se interconectan con Intents invocados de manera explícita. Para mostrar el listado con las notas en la actividad principal hemos heredado la clase ListActivity. Como ya hemos visto anteriormente en el curso, esta clase define un ListView interno. Podemos conectarlo con la clase Cursor, que devuelve los resultados de las consultas a la BD, usando la clase SimpleCursorAdapter de Android. Veamos cómo hacerlo en la práctica:
6.3.3
Acceso y creación de la Base de datos Como ya hemos visto anteriormente, para acceder y crear la base de datos de la
aplicación es necesario crear una clase heredada de SQLiteOpenHelper. En este ejemplo hemos definido la clase NotasBDHelper: 299
public class NotasBDHelper extends SQLiteOpenHelper { // Definimos el nombre y la versión de la BD private static final String BD_NOMBRE = "bdnotas.db"; private static final int BD_VERSION = 1;
// SQL que crea la base de datos // Es muy importante usar el campo _id private static final String BD_CREAR = "create table notas (_id integer primary key autoincrement, " + "categoria text not null, titulo text not null, descripcion text not null);";
// Contructor de la clase public NotasBDHelper(Context context)
{
super(context, BD_NOMBRE, null, BD_VERSION); } // Método invocado por Android si no existe la BD @Override public void onCreate(SQLiteDatabase database) { // Creamos la estructura de la BD database.execSQL(BD_CREAR); } // Método invocado por Android si hay un cambio de versión de la BD @Override public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) { // Eliminamos la BD y la volvemos a crear otra vez database.execSQL("DROP TABLE IF EXISTS notas"); onCreate(database); } }
Basada en esta clase anterior vamos a definir la nueva clase NotasBDAdapter, que es la encargada de hacer las consultas a la base de datos, borrar y actualizar registros de ésta. 300
Introducción al entorno Android
Dentro de esta clase hemos definido el método abrir(), que se conecta a la base de datos utilizando la clase NotasBDHelper. Para actualizar y dar de alta registros hemos usado un argumento del tipo ContentValues, que hemos estudiado en el apartado anterior.
class NotasBDAdapter { // Campos de la BD public static final String CAMPO_ID = "_id"; public static final String CAMPO_CATEGORIA = "categoria"; public static final String CAMPO_TITULO = "titulo"; public static final String CAMPO_DESCRIPCION = "descripcion"; private static final String TABLA_BD = "notas"; private Context contexto; private SQLiteDatabase basedatos; private NotasBDHelper bdHelper;
public NotasBDAdapter(Context context) { this.contexto = context; } // Método que abre la BD public NotasBDAdapter abrir() throws SQLException { // Abrimos la base de datos en modo escritura bdHelper = new NotasBDHelper(contexto); basedatos = bdHelper.getWritableDatabase(); return this; } // Método que cierra la BD public void cerrar() { bdHelper.close(); } // Método que crear una nota. Devuelve el id del registro nuevo si se ha // dado de alta correctamente o -1 si no. public long crearNota(String categoria, String titulo, String descripcion) { // Usamos un argumento variable para añadir el registro
301
ContentValues initialValues = crearContentValues(categoria, titulo, descripcion); // Usamos la función insert del SQLiteDatabase return basedatos.insert(TABLA_BD, null, initialValues); } // Método que actualiza una nota public boolean actualizarNota(long id, String categoria, String titulo, String descripcion) { // Usamos un argumento variable para modificar el registro ContentValues updateValues = crearContentValues(categoria, titulo, descripcion); // Usamos la función update del SQLiteDatabase return basedatos.update(TABLA_BD, updateValues, CAMPO_ID + "=" + id, null) > 0; } // Método que borra una nota public boolean borraNota(long id) { // Usamos la función delete del SQLiteDatabase return basedatos.delete(TABLA_BD, CAMPO_ID + "=" + id, null) > 0; } // Devuelve un Cursor con la consulta a todos los registros de la BD public Cursor obtenerNotas() { return basedatos.query(TABLA_BD, new String[] { CAMPO_ID, CAMPO_CATEGORIA, CAMPO_TITULO, CAMPO_DESCRIPCION }, null, null, null, null, null); } // Devuelve la Nota del id public Cursor getNota(long id) throws SQLException { Cursor mCursor = basedatos.query(true, TABLA_BD, new String[] { CAMPO_ID, CAMPO_CATEGORIA, CAMPO_TITULO, CAMPO_DESCRIPCION }, CAMPO_ID + "=" + id, null, null, null, null, null); // Nos movemos al primer registro de la consulta if (mCursor != null) { mCursor.moveToFirst();
302
Introducción al entorno Android
} return mCursor; } // Método que crea un objeto ContentValues con los parámetros indicados private ContentValues crearContentValues(String categoria, String titulo, String descripcion) { ContentValues values = new ContentValues(); values.put(CAMPO_CATEGORIA, categoria); values.put(CAMPO_TITULO, titulo); values.put(CAMPO_DESCRIPCION, descripcion);
return values; } }
6.3.4
Recursos de diseño XML A continuación, indicamos los ficheros XML de Layout que componen el diseño de la
interfaz del usuario:
res/menu/menu_listado.xml: define el diseño del menú principal de la aplicación.
res/layout/main.xml: define el diseño de la pantalla de la actividad principal NotasActivity.
res/layout/editar_nota.xml: define el diseño de la actividad secundaria GestionarNota, que sirve para editar y dar de alta notas.
res/layout/fila_notas.xml: define el diseño de los elementos del ListView de la actividad principal, es decir, el estilo de cada nota en el listado.
El alumno o alumna puede abrir estos ficheros en su ordenador y ver cómo están implementados los distintos diseños. Además, se definen los dos ficheros strings.xml y categorias.xml en la carpeta res/values con los literales que usa la aplicación.
303
6.3.5
Actividades Como hemos comentado, la aplicación está formada por dos actividades: la actividad
principal (NotasActivity) muestra un listado con todas las notas y la segunda (GestionarNota) sirve para editarlas o dar de alta una nueva. Veamos el contenido de la actividad principal:
public class NotasActivity extends ListActivity { private NotasBDAdapter bdHelper; private static final int ACTIVIDAD_NUEVA = 0; private static final int ACTIVIDAD_EDITAR = 1; private static final int MENU_ID = Menu.FIRST + 1; private Cursor cursor;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Hacemos más ancha la línea de división entre elementos en el listado this.getListView().setDividerHeight(3);
// Creamos el adaptador que conecta con la BD bdHelper = new NotasBDAdapter(this); // Cargamos todos los datos bdHelper.abrir(); cargaDatos(); // Indicamos el menú contextual asociado al listado registerForContextMenu(getListView()); } // Creamos el menú principal @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menulistado, menu);
304
Introducción al entorno Android
return true; } // El usuario hace clic en una opción del menú principal @Override public boolean onMenuItemSelected(int id, MenuItem item) { // Buscamos la opción del menú principal seleccionada switch (item.getItemId()) { case R.id.insertar: // Creamos una actividad indicando el tipo de petición // "ACTIVIDAD_NUEVA" y esperamos el resultado de la misma Intent i = new Intent(this, DetallesNota.class); startActivityForResult(i, ACTIVIDAD_NUEVA); // Indicamos que hemos manejado la opción del menú return true; } return super.onMenuItemSelected(id, item); } // El usuario hace clic en una opción del menú contextual del listado @Override public boolean onContextItemSelected(MenuItem item) { // Buscamos la opción del menú contextual seleccionada switch (item.getItemId()) { case MENU_ID: // Obtenemos el id del elemento seleccionado AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); // Borramos ese registro bdHelper.borraNota(info.id); // Recargamos los datos cargaDatos(); // Indicamos que hemos manejado la opción del menú return true; } return super.onContextItemSelected(item);
305
} // Cuando hacemos clic en un elemento del listado, se edita la Nota @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); // Creamos una actividad indicando el tipo de petición // "ACTIVIDAD_EDITAR" y esperamos el resultado de la misma Intent i = new Intent(this, DetallesNota.class); // Pasamos el campo _id como un dato extra i.putExtra(NotasBDAdapter.CAMPO_ID, id); startActivityForResult(i, ACTIVIDAD_EDITAR); } // Método que se llama cuando una subactividad devuelve el resultado @Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); // Recargamos los datos si se ha modificado algo. // Es decir, el usuario ha hecho clic en OK if (resultCode == Activity.RESULT_OK) cargaDatos(); } private void cargaDatos() { cursor = bdHelper.obtenerNotas(); // Se indica que a la Actividad principal que controle los recursos // cursor. Es decir, si se termina la Actividad, se elimina esta // Cursor de la memoria startManagingCursor(cursor);
// Indicamos cómo debe pasarse el campo título de (from) a (to) // la Vista de la opción (fila_notas.xml) String[] from = new String[] { NotasBDAdapter.CAMPO_CATEGORIA, NotasBDAdapter.CAMPO_TITULO }; int[] to = new int[] { R.id.fila_categoria, R.id.fila_titulo };
306
Introducción al entorno Android
// Creamos un sencillo adaptador de tipo Matriz // asociado al cursor SimpleCursorAdapter notas = new SimpleCursorAdapter(this, R.layout.fila_notas, cursor, from, to); // Indicamos al listado el adaptador que le corresponde setListAdapter(notas); }
// Creamos el menú contextual @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); menu.add(0, MENU_ID, 0, R.string.menu_borrar); }
// Cuando se acaba la Actividad cerramos la BD // Es muy importante hacer esto para que se escriba toda la información @Override protected void onDestroy() { super.onDestroy(); if (bdHelper != null) { bdHelper.cerrar(); } } }
A continuación, vamos a ver el código de la Actividad secundaria o subactividad:
public class GestionarNota extends Activity { private EditText tituloText; private EditText descripcionText;
307
private Spinner categoriaSpinner; // Usamos esta variable para saber si estamos editando (filaId=id) o // se trata de un registro nuevo (filaId=null) private Long filaId; private NotasBDAdapter bdHelper;
@Override protected void onCreate(Bundle bundle) { super.onCreate(bundle); // Creamos un adaptador u abrimos la BD bdHelper = new NotasBDAdapter(this); bdHelper.abrir(); // Dibujamos el UI y buscamos sus Vistas setContentView(R.layout.editar_nota); categoriaSpinner = (Spinner) findViewById(R.id.category); tituloText = (EditText) findViewById(R.id.nota_editar_titulo); descripcionText = (EditText) findViewById(R.id.nota_editar_descripcion); Button aceptaBoton = (Button) findViewById(R.id.nota_editar_boton); // Variable con el ID del registro actual filaId = null; // Obtenemos el campo ID que se debe haber pasado en la invocación // de la actividad si estamos editando el registro Bundle extras = getIntent().getExtras(); // Si extras contiene algo cargamos ese ID if (extras != null) { filaId = extras.getLong(NotasBDAdapter.CAMPO_ID); }
// Cargamos el registro en los componentes de la pantalla cargarRegistro(); // Método del botón OK aceptaBoton.setOnClickListener(new View.OnClickListener() {
308
Introducción al entorno Android
public void onClick(View view) { // Si pulsa este botón guardamos los datos y devolvemos OK a la Actividad String categoria = (String) categoriaSpinner.getSelectedItem(); String titulo = tituloText.getText().toString(); String descripcion = descripcionText.getText().toString();
// Alta de registro if (filaId == null) { bdHelper.crearNota(categoria, titulo, descripcion); } else { // Modificación de registro bdHelper.actualizarNota(filaId, categoria, titulo, descripcion); }
setResult(RESULT_OK); // Acabamos la actividad finish(); }
}); } // end onCreate
private void cargarRegistro() { if (filaId != null) { Cursor nota = bdHelper.getNota(filaId); // Volvemos a dejar que la actividad actual controle el Cursos startManagingCursor(nota); // Obtenemos el campo categoria String categoria = nota.getString( nota.getColumnIndexOrThrow(NotasBDAdapter.CAMPO_CATEGORIA));
for (int i=0; i
309
String s = (String) categoriaSpinner.getItemAtPosition(i); // Si coindice con la que está en la BD la seleccionamos en el listado desplegable if (s.equalsIgnoreCase(categoria)){ categoriaSpinner.setSelection(i); break; } }
// Rellenamos las Vistas de Título y Descripción tituloText.setText(nota.getString( nota.getColumnIndexOrThrow(NotasBDAdapter.CAMPO_TITULO))); descripcionText.setText(nota.getString( nota.getColumnIndexOrThrow(NotasBDAdapter.CAMPO_DESCRIPCION))); } } // end cargarRegistro }
6.3.6
Fichero Androidmanifest.xml Para que la subactividad GestionarNota esté disponible en el sistema operativo,
debemos declararla en el archivo "AndroidManifest.xml" del proyecto, incluso si la vamos a invocar de manera explícita. Para esto, escribimos en este fichero las siguientes líneas:
El
atributo
android:windowSoftInputMode
subactividad con el teclado flotante de Android:
310
indica
cómo
interacciona
esta
Introducción al entorno Android
El establecimiento de este atributo afecta a dos aspectos del teclado:
Al estado del teclado de la pantalla, es decir, si está oculto o visible cuando la actividad está en primer plano y el usuario interacciona con ella.
Al ajuste que sufren los componentes de la ventana principal de la actividad para que el teclado quepa en la pantalla, es decir, si se ajusta el contenido para dejar espacio al teclado o el contenido se mantiene intacto y el tecla "flota" sobre éste.
En este ejemplo hemos usado las opciones stateVisible y adjustResize para que el teclado se muestre cuando el usuario acceda a un componente de introducción de texto y cambie las proporciones de la pantalla para hacer un "hueco" al teclado. En la ayuda oficial de Android puedes encontrar todos los posibles valores con su descripción.
6.4
GESTIÓN DE FICHEROS XML
EXtensible Markup Language (XML) es un formato de datos que se usa comúnmente en las aplicaciones web modernas. XML utiliza etiquetas personalizadas para describir los tipos de datos y se codifica como texto sin formato, por lo que es muy flexible y sencillo de utilizar. Android incluye bibliotecas de clases diseñadas para el procesamiento de datos en formato XML. Los tres modelos más extendidos para leer y escribir ficheros de tipo XML son DOM (Document Object Model), SAX (Simple API for XML) y StAX (Streaming API for XML):
311
DOM: vuelca el documento XML en la memoria del dispositivo en forma de estructura de árbol, de manera que se puede acceder aleatoriamente a los elementos de las ramas.
SAX: en este modelo, basado en eventos, la aplicación recorre todos los elementos del archivo XML de una sola vez. La ventaja respecto al modelo anterior consiste en que es más rápido y requiere menos memoria, si bien no permite el acceso aleatorio a una de sus ramas.
StAX: es una mezcla de las dos modelos anteriores. En este caso, también se lee el fichero XML de forma secuencial, pero podemos controlar la forma en que se leen sus elementos. En el caso de SAX es obligatorio leer todos los elementos a la vez. Este modelo es también mucho más rápido que DOM, pero algo más lento de SAX.
Un analizador sintáctico (en inglés parser) convierte el texto de entrada en otras estructuras (comúnmente árboles), que son más útiles para el posterior análisis y capturan la jerarquía implícita de la entrada. Android dispone de analizadores XML para estos tres modelos. Con cualquiera de ellos podemos hacer las mismas tareas. Ya veremos más adelante que, dependiendo de la naturaleza de la aplicación, es más eficiente utilizar un modelo u otro. Estas técnicas se pueden utilizar para leer cualquier documento XML, tanto de Internet como del sistema de archivos. En el Ejemplo 3 de esta Unidad vamos a leer datos XML de un documento RSS de un periódico; concretamente, del canal RSS de noticias de 20minutos.es. Puedes modificar esta dirección cambiado la variable de la Actividad principal XMLActivity: static String feedUrl = "http://20minutos.feedsportal.com/c/32489/f/478284/index.rss";
Si abrimos el documento RSS de esta fuente de noticias (en inglés feed), vemos la estructura siguiente: 20minutos.es http://www.20minutos.es/ Diario de información general y ... es-ES Fri, 28 Oct 2011 18:54:41 GMT
312
Introducción al entorno Android
Fri, 28 Oct 2011 18:54:41 GMT -
Título de la noticia 1 http://link_de_la_noticia_2.es Descripción de la noticia 2 Fecha de publicación 2 -
Título de la noticia 2 http://link_de_la_noticia_2.es Descripción de la noticia 2 Fecha de publicación 2 ...
Como puedes observar, se compone de un elemento principal , seguido de varios datos relativos al canal y, posteriormente, de una lista de elementos - para cada noticia. En este apartado vamos a describir cómo leer este archivo XML sirviéndonos de cada una de las tres alternativas citadas anteriormente. Desde Eclipse puedes abrir el proyecto Ejemplo 3 (XML) de la Unidad 6. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado métodos de lectura del formato XML.
Si ejecutas la aplicación en el emulador de Android, verás que tiene el siguiente aspecto:
313
Para implementar la Actividad principal, hemos usado la clase ListActivity, donde mostraremos un listado con las noticias. Para empezar, en primer lugar debemos definir una clase Java para almacenar los datos leídos de una noticia. Para cargar el listado de la clase ListActivity con los titulares de las noticias usamos una lista de objetos de este tipo. Veamos el código fuente de esta clase que hemos denominado Noticia:
// Clase que sirve para cargar en un objeto cada noticia que leamos del fichero XML public class Noticia { // Dispone de las variables y métodos típicos de una clase sencilla private String titulo; private URL enlace; private String descripcion; private String fecha;
public URL getEnlace() { return enlace; } public void setEnlace(String enlace) { // Intentamos cargar el enlace en forma de URL. // Si tenemos un error lanzamos una excepción
314
Introducción al entorno Android
try { this.enlace = new URL(enlace); } catch (MalformedURLException e) { throw new RuntimeException(e); } }
public void setFecha(String fecha) { while (!fecha.endsWith("00")){ fecha += "0"; } this.fecha = fecha.trim(); } public String getFecha() { return this.fecha; } public String getTitulo() { return titulo; } public void setTitulo(String titulo) { this.titulo = titulo; } public String getDescripcion() { return descripcion; } public void setDescripcion(String descripcion) { this.descripcion = descripcion; } }
Por simplificación, hemos tratado todos los datos como cadenas de texto.
315
6.4.1
SAX es el modelo clásico en Android
En el modelo clásico de SAX el tratamiento de un archivo XML se basa en un analizador (parser) que lee secuencialmente el documento XML y va generando diferentes eventos con la información de cada elemento leído. Por ejemplo, a medida que lee el documentos XML, si el analizador encuentra una etiqueta lanzará el método startElement() del parser de inicio de etiqueta con la información asociada. Si después de esa etiqueta encuentra una cadena de texto, invocará el método characters() del parser con toda la información necesaria. Por lo tanto, debemos implementar las sentencias necesarias para tratar cada uno de los métodos posibles que el analizador puede lanzar durante la lectura del documento XML. Los principales métodos que se pueden producir son los siguientes:
startDocument(): comienza el documento XML.
endDocument(): termina el documento XML.
startElement(): comienza una etiqueta XML.
endElement(): termina una etiqueta XML.
characters(): se ha encontrado una cadena de texto.
Puedes encontrar la lista completa de los métodos en este enlace. Estos métodos se definen en la clase org.xml.sax.helpers.DefaultHandler. Por esto hay que heredar esta clase y sobrescribir los métodos necesarios. En este ejemplo la clase se llama ParserSaxClasicoHandler:
public class ParserSaxClasicoHandler extends DefaultHandler{ // Variables temporales que usamos a lo largo del Handler // Listado completo de las noticias private List noticias; // Noticia que estamos leyendo en ese momento private Noticia noticiaActual; // Variable temporal para almacenar el texto contenido en una etiqueta private StringBuilder sbTexto;
// Método que devuelve todo el listado de noticias public List getNoticias(){
316
Introducción al entorno Android
return noticias; }
// Método que se lanza al iniciar la lectura de un XML @Override public void startDocument() throws SAXException { // Lanzamos el método de la clase madre super.startDocument(); // Iniciamos los variables temporales noticias = new ArrayList(); sbTexto = new StringBuilder(); }
// Comienza una etiqueta XML @Override public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { // Lanzamos el método de la clase madre super.startElement(uri, localName, name, attributes); // Si leemos una nueva etiqueta item es que empieza una noticias if (localName.equals(EtiquetasRSS.ITEM)) { noticiaActual = new Noticia(); } }
// Finaliza una etiqueta XML @Override public void endElement(String uri, String localName, String name) throws SAXException { // Lanzamos el método de la clase madre super.endElement(uri, localName, name); // Si estamos leyendo una noticia if (this.noticiaActual != null) {
317
// Cargamos el campo correspondiente de la etiqueta que acabamos de leer if (localName.equals(EtiquetasRSS.TITLE)) { noticiaActual.setTitulo(sbTexto.toString()); } else if (localName.equals(EtiquetasRSS.LINK)) { noticiaActual.setEnlace(sbTexto.toString()); } else if (localName.equals(EtiquetasRSS.DESCRIPTION)) { noticiaActual.setDescripcion(sbTexto.toString()); } else if (localName.equals(EtiquetasRSS.PUB_DATE)) { noticiaActual.setFecha(sbTexto.toString()); } else if (localName.equals(EtiquetasRSS.ITEM)) { // Si leemos el final de la etiqueta "item" añadimos la noticia al listado noticias.add(noticiaActual); } // Reiniciamos la variable temporal de texto sbTexto.setLength(0); } }
// Se ha encontrado una cadena de texto @Override public void characters(char[] ch, int start, int length) throws SAXException { // Lanzamos el método de la clase madre super.characters(ch, start, length); // Si estamos leyendo una noticia if (this.noticiaActual != null) // Asignamos el texto a la variable temporal sbTexto.append(ch, start, length); } }
318
Introducción al entorno Android
En el código fuente anterior usamos la lista de noticias List para almacenar todas la noticias leídas y el método getNoticias() las devuelve al finalizar la lectura del documento. Después, hay que implementar los métodos SAX necesarios. Una vez hemos implementado nuestro handler, vamos a crear la nueva clase ParserSaxClasico que hace uso de este handler para analizar un documento XML en concreto usando el modelo SAX. Esta clase únicamente define un constructor que recibe como parámetro la dirección de Internet del documento XML que hay que analizar. El método público analizar() analiza el documento XML y devuelve como resultado una lista de noticias. Veamos cómo queda esta clase:
public class ParserSaxClasico
implements RSSParser {
// URL del archivo XML private URL feedUrl;
// Constructor de la clase, se asigna la URL a la variable local protected ParserSaxClasico(String feedUrl){ try { this.feedUrl= new URL(feedUrl); } catch (MalformedURLException e) { throw new RuntimeException(e); } }
// Método que lee el documento XML public List analizar() { // Creamos acceso a la API Sax de Android SAXParserFactory factory = SAXParserFactory.newInstance(); try { // Creamos un analizador (parser) de SAX
319
SAXParser parser = factory.newSAXParser(); // Creamos el handle de SAX que implementamos en otra clase ParserSaxClasicoHandler handler = new ParserSaxClasicoHandler(); // Analizamos el archivo con el handler anterior parser.parse(getInputStream(), handler); // Devolvemos las noticias encontradas return handler.getNoticias(); } catch (Exception e) { throw new RuntimeException(e); } }
// Método que abre una conexión a la URL y devuelve el // puntero de tipo fichero al analizador correspondiente private InputStream getInputStream() { try { return feedUrl.openConnection().getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } } }
El constructor de la clase anterior acepta como parámetro una dirección URL del documento XML que analiza y controla la validez de dicha dirección
generando una
excepción si no puede crear la clase URL correspondiente. Por otra parte, el método analizar() es el encargado de crear un nuevo parser SAX y de iniciar el proceso de análisis pasando al parser una instancia del handler que hemos creado anteriormente con una referencia al documento XML en forma de stream. 320
Introducción al entorno Android
Para pasar una referencia en forma de stream, implementamos el método auxiliar privado getInputStream(), que abre la conexión con la dirección URL especificada mediante el método openConnection() y obtiene el stream de entrada mediante el método getInputStream(). En el apartado de Tratamiento de ficheros de la Unidad 4 hemos visto cómo usar la clase stream en Android. Finalmente, sólo queda aplicar la clase ParserSaxClasico para cargar un documento XML con el modelo SAX. Para ello, en la Actividad principal XMLActivity de la aplicación escribimos las siguientes sentencias:
// Creamos un objeto del parser (analizador XML) en función del tipo (opción // menú principal). La dirección (URL) de la fuente de noticias es una //constante en este ejemplo RSSParser analizador = XMLParser.getParser(tipo, feedUrl); // Guardamos el momento actual de inicio de long inicio = System.currentTimeMillis(); // Descargamos y analizamos el fichero XML noticias = analizador.analizar(); // Calculamos el tiempo que ha tardado en leer el XML long duracion = System.currentTimeMillis() - inicio; // Mostramos el tiempo de lectura del XML Toast.makeText(this, "Se han cargado los datos en "+duracion+" milisegundos", 1).show(); // Creamos un listado con todos los títulos de las noticias List titulos = new ArrayList(noticias.size()); for (Noticia msg : noticias){ titulos.add(msg.getTitulo()); }
// Definimos Adaptador sencillo con un diseño sencillo y el listado títulos ArrayAdapter adaptador = new ArrayAdapter(this, R.layout.fila, titulos); this.setListAdapter(adaptador);
321
Primero creamos el parser correspondiente usando la dirección URL del documento XML y, después, ejecutamos el método analizar() para obtener una lista de objetos de tipo Noticia que, posteriormente, asignamos al adaptador del listado de la Actividad principal. Si te fijas en el código anterior estamos creando el objeto analizador a partir de la clase XMLParser en lugar de ParserSaxClasico. Si abrimos el fichero que define la clase XMLParser veremos el código siguiente:
// Clase que crea un analizador XML del tipo necesario // Se crea una interface RSSParser para poder renombrar el método analizar() public abstract class XMLParser {
public static RSSParser getParser(TiposParser tipo, String feedURL){ switch (tipo){ case SAX_CLASICO: return new ParserSaxClasico(feedURL); case DOM: return new ParserDom(feedURL); case SAX_SIMPLIFICADO: return new ParserSaxSimplificado(feedURL); case XML_PULL: return new ParserXmlPull(feedURL); default: return null; } } }
Observa que, como estamos usando la misma aplicación para mostrar cómo funcionan todos los modelos de carga de archivos XML en Android, hemos creado una clase abstracta que devuelve un objeto en función del tipo de analizador que el usuario ha decido usar en ese momento.
NOTA: Para que esta aplicación Android acceda a Internet, es necesario declararlo en el fichero AndroidManifest.xml, que requiere el permiso "android.permission.INTERNET".
322
Introducción al entorno Android
6.4.2
SAX simplificado en Android El modelo SAX anterior de tratamiento de archivos XML, a pesar de funcionar perfecta
y eficientemente, tiene claras desventajas, ya que es obligatorio definir una clase independiente para el handler. Adicionalmente, el modelo SAX implica poner bastante atención al definir dicho handler, ya que los métodos SAX definidos no están asignados a etiquetas concretas del documento XML, sino que se lanzan para todas ellas. Esto obliga a realizar distinciones entre etiquetas dentro de cada método. Esto se observa perfectamente en el método endElement() que definimos en el ejemplo anterior. En primer lugar, hay que comprobar con la sentencia condicional si el atributo noticiaActual no está vacío (null), para no confundir el elemento descendiente de con el elemento descendiente de - , que es el que queremos leer. Posteriormente, hay que distinguir con unas sentencias IF entre todas las etiquetas posibles la acción que debemos realizar. Tengamos en cuenta que hemos usado un documento XML muy sencillo, pero si tratamos un documento XML más enrevesado, la complejidad de este handler aumenta mucho y pueda dar lugar a errores de programación. Para evitar estos problemas, Android propone una variante del modelo SAX que evita definir una clase separada para el handler y que permite asociar directamente las acciones a etiquetas concretas dentro de la estructura del documento XML. Veamos cómo queda el analizador XML utilizando SAX simplificado para Android:
public class ParserSaxSimplificado
implements RSSParser {
// Variables temporales que usamos a los largo del Handler // Noticia que estamos leyendo en ese momento private Noticia noticiaActual; // Variable que define la etiqueta raíz del XML que es static final String RSS = "rss"; // URL del archivo XML private URL feedUrl;
// Constructor de la clase, se asigna la URL a la variable local protected ParserSaxSimplificado(String feedUrl){ try { this.feedUrl= new URL(feedUrl);
323
} catch (MalformedURLException e) { throw new RuntimeException(e); } }
// Método que lee el documento XML public List analizar() { // Variable que almacena las noticias encontradas final List noticias = new ArrayList(); // Buscamos el elemento raíz RootElement root = new RootElement(RSS); // Buscamos el elemento channel dentro de la etiqueta raíz (root) Element channel = root.getChild(EtiquetasRSS.CHANNEL); // Buscamos el elemento item dentro de la etiqueta channel Element item = channel.getChild(EtiquetasRSS.ITEM);
/* * Definimos los listerners de estos elementos anteriores */ // Método de inicio de una nueva etiqueta item item.setStartElementListener(new StartElementListener(){ public void start(Attributes attrs) { noticiaActual = new Noticia(); } }); // Método de finalización de una nueva etiqueta item item.setEndElementListener(new EndElementListener(){ public void end() { noticias.add(noticiaActual); } });
324
Introducción al entorno Android
// Método de obtención etiqueta title dentro de la etiqueta item item.getChild(EtiquetasRSS.TITLE).setEndTextElementListener(new EndTextElementListener(){ public void end(String body) { noticiaActual.setTitulo(body); } }); // Método de obtención etiqueta link dentro de la etiqueta item item.getChild(EtiquetasRSS.LINK).setEndTextElementListener(new EndTextElementListener(){ public void end(String body) { noticiaActual.setEnlace(body); } }); // Método de obtención etiqueta description dentro de la etiqueta item item.getChild(EtiquetasRSS.DESCRIPTION).setEndTextElementListener(new EndTextElementListener(){ public void end(String body) { noticiaActual.setDescripcion(body); } }); // Método de obtención etiqueta pub_date dentro de la etiqueta item item.getChild(EtiquetasRSS.PUB_DATE).setEndTextElementListener(new EndTextElementListener(){ public void end(String body) { noticiaActual.setFecha(body); } }); //Usamos el objeto Xml de Android para leer el archivo XML try { Xml.parse(this.getInputStream(), Xml.Encoding.UTF_8, root.getContentHandler()); } catch (Exception e) { throw new RuntimeException(e); }
325
// Devolvemos las noticias leídas return noticias; }
// Método que abre una conexión a la URL y devuelve el // puntero de tipo fichero al analizador correspondiente private InputStream getInputStream() { try { return feedUrl.openConnection().getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } } }
En este nuevo modelo SAX simplificado de Android las acciones que debemos realizar dentro de cada método se definen dentro de la misma clase asociadas a etiquetas concretas del XML. Para esto, lo primero que hacemos es navegar por la estructura del archivo XML hasta encontrar las etiquetas que tenemos que tratar y asignarlaa a algunos métodos de tipo listeners ("escuchadores") disponibles como StartElementListener() de inicio de etiqueta o EndElementListener() de finalización de etiqueta, incluyendo las sentencias oportunas dentro de estos métodos. Por ejemplo, para obtener el elemento - , en primer lugar buscamos el elemento raíz del XML (
) declarando un objeto RootElement. Después, accedemos a su elemento hijo y, finalmente, obtenemos de éste último el elemento hijo - . En cada "salto" hemos utilizado el método getChild(). Una vez hemos llegado a la etiqueta buscada, asignamos los listeners necesarios. En este caso, uno de apertura y otro de cierre de etiqueta item, donde inicializamos la noticia actual y la añadimos a la lista final, respectivamente, de forma similar a como lo hemos hecho para el modelo SAX clásico.
326
Introducción al entorno Android
Para el resto de etiquetas internas de item, procedemos de la misma manera, accediendo a ellas con getChild() y asignando los listeners necesarios. Para acabar, arrancamos todo el proceso de análisis del XML llamando al método parse(), definido en la clase android.Util.Xml, al que pasamos como parámetros el stream del archivo XML, la codificación del documento XML y un handler SAX obtenido directamente del objeto RootElement definido anteriormente. Este modelo SAX simplificado evita implementar el handler necesario en el modelo SAX clásico. Además, ayuda a evitar posibles errores de programación disminuyendo la complejidad del mismo Hay que tener en cuenta que el modelo clásico es tan válido y eficiente como éste, que únicamente simplifica el trabajo al programador.
6.4.3
DOM en Android Como hemos comentado, el modelo DOM debe leer el documento XML
completamente antes de poder realizar ninguna acción con su contenido. Es decir, cambia radicalmente la manera de leer los archivos XML. Al acabar la lectura del documento XML este modelo devuelve todo su contenido en una estructura de tipo árbol, donde los distintos elementos del fichero XML se representan en forma de nodos y su jerarquía padre-hijo se establece mediante relaciones entre dichos nodos. Por ejemplo, si tenemos el siguiente documento XML: Título 1 Enlace 1 Título 2 Enlace 2
El modelo DOM traduce este documento XML en el árbol siguiente:
327
Como vemos, este árbol conserva la misma información del fichero XML, pero en forma de nodos y relaciones entre nodos. Por esta razón es sencillo buscar fácilmente dentro de la estructura un elemento en concreto. Este árbol se conserva en memoria una vez leído el documento completo, lo que permite procesarlo en cualquier orden y tantas veces como sea necesario, a diferencia del modelo SAX, donde el tratamiento es secuencial y siempre desde el principio hasta el final del documento. Es decir, no se puede volver atrás una vez finalizada la lectura del documento XML. El modelo DOM de Android ofrece una serie de clases y métodos que permiten cargar la información de la forma descrita y facilitan la búsqueda de elementos dentro de la estructura creada. Veamos cómo queda el analizador XML usando el modelo DOM de Android:
public class ParserDom
implements RSSParser {
// URL del archivo XML private URL feedUrl;
// Constructor de la clase, se asigna la URL a la variable local protected ParserDom(String feedUrl){ try { this.feedUrl= new URL(feedUrl);
328
Introducción al entorno Android
} catch (MalformedURLException e) { throw new RuntimeException(e); } }
// Método que lee el documento XML public List analizar() { // Creamos acceso a la API DOM de Android DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); List noticias = new ArrayList(); try { // Creamos un analizador (parser) de DOM DocumentBuilder parser = factory.newDocumentBuilder(); // Analizamos el archivo XML Document dom = parser.parse(this.getInputStream()); // Obtenemos el elemento raíz del parser Element root = dom.getDocumentElement(); // Buscamos las etiquetas ITEM dentro del elemento raíz NodeList items = root.getElementsByTagName(EtiquetasRSS.ITEM); // Recorremos todos los items y vamos cargando la lista de noticias for (int i=0;i
329
if (nombre.equalsIgnoreCase(EtiquetasRSS.TITLE)){ noticia.setTitulo(contenido.getFirstChild().getNodeValue()); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.LINK)){ noticia.setEnlace(contenido.getFirstChild().getNodeValue()); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.DESCRIPTION)){ // Puede ocurrir que el texto esté distribuido en varias // líneas, por lo que hay que leer todos los nodos internos StringBuilder text = new StringBuilder(); NodeList chars = contenido.getChildNodes(); for (int k=0;k
// Método que abre una conexión a la URL y devuelve el // puntero de tipo fichero al analizador correspondiente private InputStream getInputStream() { try { return feedUrl.openConnection().getInputStream(); } catch (IOException e) {
330
Introducción al entorno Android
throw new RuntimeException(e); } } }
El método más importante es analizar(). De igual forma que se hace en el modelo SAX,
el
primer
paso
es
instanciar
la
API
de
DOM
a
partir
de
la
clase
DocumentBuilderFactory. Posteriormente, creamos un nuevo parser a partir del método newDocumentBuilder(). Después, únicamente hay que leer el documento XML invocando el método parse() del parser DOM, pasándole como parámetro el stream de entrada del fichero. Al hacer esto, el documento XML se lee completamente y se crea la estructura de árbol equivalente, que se devuelve como un objeto de tipo Document por el que podemos movernos para buscar los elementos - que necesita la aplicación. Para esto, lo primero que hacemos es acceder al nodo raíz (root) del árbol utilizando el método getDocumentElement(); en este ejemplo es la etiqueta
,. Una vez que sabemos dónde está el nodo raíz, vamos a buscar todos los nodos con la etiqueta - . Para esto, usamos el método de búsqueda por el nombre de etiqueta getElementsByTagName(“nombre_de_etiqueta“), que devuelve una lista de tipo NodeList con todos los nodos hijos del nodo actual cuya etiqueta coincida con el nombre indicado. Una vez hemos obtenido todos los elementos
- que contienen cada noticia, vamos a recorrerlos de uno en uno para crear todos los objetos de tipo Noticia necesarios. Para cada uno de estos elementos obtenemos sus nodos hijos mediante el método getChildNodes().
Después,
recorremos
estos
nodos
hijos
obteniendo
su
texto
y
almacenándolo en el campo correspondiente del objeto Noticia. Para saber qué etiqueta representa cada nodo hijo utilizamos el método getNodeName().
6.4.4
StAX en Android Este modelo StAX de lectura de documentos XML es muy parecido a SAX. La
diferencia principal está en que, mientras que en el modelo SAX no hay control de ejecución una vez iniciada la lectura del XML (el parser lee automáticamente todo el XML desde el inicio hasta el final invocando los métodos necesarios), en el modelo StAX podemos guiar la lectura del documento o intervenir en ella, solicitando de forma explícita la lectura del siguiente elemento del documento XML y respondiendo al resultado con las acciones oportunas. En este ejemplo hemos usado la implementación de StAX de Android que se lama XmlPull. Fíjate en el código fuente de este analizador:
331
public class ParserXmlPull
implements RSSParser {
// URL del archivo XML private URL feedUrl;
// Constructor de la clase, se asigna la URL a la variable local protected ParserXmlPull(String feedUrl){ try { this.feedUrl= new URL(feedUrl); } catch (MalformedURLException e) { throw new RuntimeException(e); } }
// Método que lee el documento XML public List analizar() { List noticias = null; // Creamos un analizador (parser) de StAX que en Android se llama XmlPullParser XmlPullParser parser = Xml.newPullParser();
try { // Asignamos el stream del XML al parsr parser.setInput(this.getInputStream(), null); // Guardamos el tipo de evento actual = START_DOCUMENT int eventType = parser.getEventType(); Noticia noticiaActual = null; // Variable que controla si se ha acabado el documento XML boolean docAcabado = false; // Mientras no acabe el documento
332
Introducción al entorno Android
while (eventType != XmlPullParser.END_DOCUMENT && !docAcabado){ // Variable temporal que guarda el nombre de la etiqueta String nombre = null; switch (eventType){ case XmlPullParser.START_DOCUMENT: // Creamos el listado con las noticias noticias = new ArrayList(); break; // Etiqueta de incicio case XmlPullParser.START_TAG: // Creamos el objeto noticia o guardamos el campo correspondiente nombre = parser.getName(); if (nombre.equalsIgnoreCase(EtiquetasRSS.ITEM)){ noticiaActual = new Noticia(); } else if (noticiaActual != null){ if (nombre.equalsIgnoreCase(EtiquetasRSS.LINK)){ noticiaActual.setEnlace(parser.nextText()); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.DESCRIPTION)){ noticiaActual.setDescripcion(parser.nextText()); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.PUB_DATE)){ noticiaActual.setFecha(parser.nextText()); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.TITLE)){ noticiaActual.setTitulo(parser.nextText()); } } break; // Etiqueta de cierre case XmlPullParser.END_TAG: nombre = parser.getName(); if (nombre.equalsIgnoreCase(EtiquetasRSS.ITEM) && noticiaActual != null){ noticias.add(noticiaActual); } else if (nombre.equalsIgnoreCase(EtiquetasRSS.CHANNEL)){
333
docAcabado = true; } break; } eventType = parser.next(); } // end while } catch (Exception e) { throw new RuntimeException(e); } // Devolvemos las noticias return noticias; }
// Método que abre una conexión a la URL y devuelve el // puntero de tipo fichero al analizador correspondiente private InputStream getInputStream() { try { return feedUrl.openConnection().getInputStream(); } catch (IOException e) { throw new RuntimeException(e); } } }
Una vez más nos centramos en el método analizar() de la clase. Primero, creamos el nuevo analizador XmlPull y asignamos el fichero de entrada en forma de stream. Después, definimos un bucle en el que solicitamos en cada iteración al parser el siguiente evento encontrado en la lectura del archivo XML mediante el método parser.next(). Para cada evento devuelto obtenemos su tipo mediante el método parser.getEventType() y ejecutamos las sentencias oportunas.
334
Introducción al entorno Android
Una vez identificado el tipo de evento, podemos consultar el nombre de la etiqueta del elemento XML mediante parser.getName() y el texto contenido mediante parser.nextText(). Si ejecutas la aplicación en el emulador de Android, verás que puedes acceder a los distintos modelos de tratamiento de ficheros XML pulsando la tecla menú
del emulador:
Si seleccionamos una de las opciones, podemos ver que se recarga el listado de noticias y el tiempo que tarda en cargar el documento XML:
Se muestra un mensaje con el tiempo que ha tardado en cargar el documento XML. Así podemos valorar la eficacia de cada modelo a la hora de leer un XML completo. Observarás que los modelos SAX son los más rápidos.
335
Si haces clic sobre una noticia verás que Android te permite seleccionar el navegador que quieres usar para iniciar la acción Intent.ACTION_VIEW que permite abrir una página Web:
Esto ocurre porque en el Ejemplo 2 de la Unidad 5 hemos definido un navegador sencillo que carga la página en formato HTML para invocar implícitamente una Intención propia.
336
Introducción al entorno Android
El término base de datos se refiere a una colección, conjunto o depósito de datos, almacenados en un soporte magnético o de otro tipo, accesibles por múltiples usuarios de forma rápida y eficaz mediante el ordenador a través de una o de varias aplicaciones informáticas independientes de los datos.Para crear una Intención hay que usar el objeto Intent de Android. En Android las bases de datos son privadas y únicamente una aplicación puede acceder a ellas para leer y escribir datos. Para compartir información de base de datos entre aplicaciones Android hay que usar los Content Providers.Explícita: invocando la clase Java del componente que queremos ejecutar. Normalmente, se hace para invocar componentes de una misma aplicación. Android usa SQLite como motor de base de datos relacional. Antes de crear una base de datos con el ordenador, es preciso diseñarla previamente sobre el papel. Usar bases de datos Android hace más lentas las aplicaciones debido a que es necesario escribir y leer información de la memoria física del dispositivo. Por esto, es recomendable usar hilos de ejecución. La forma usual en Android de crear, modificar y conectar con una base de datos SQLite consiste en usar la clase Java SQLiteOpenHelper. Existen dos formas de buscar y recuperar registros de una base de datos SQLite. La primera de ellas consiste en utilizar directamente el comando de consulta SQL rawQuery(). La segunda forma consiste en utilizar el método específico query() con parámetros de consulta a la base de datos. EXtensible Markup Language (XML) es un formato de datos que se usa comúnmente en las aplicaciones web modernas. Los tres modelos más extendidos para leer y escribir ficheros de tipo XML son DOM (Document Object Model), SAX (Simple API for XML) y StAX (Streaming API for XML).
337
El modelo DOM vuelca el documento XML en la memoria del dispositivo en forma de estructura de árbol, de manera que se puede acceder aleatoriamente a los elementos de las ramas. El modelo SAX se basa en eventos. La aplicación recorre todos los elementos del archivo XML de una sola vez. La ventaja respecto a la anterior es que es más rápido y requiere menos memoria, si bien no permite el acceso aleatorio a una de sus ramas. El modelo StAX es una mezcla de las dos modelos anteriores. En este caso, también se lee el fichero XML de forma secuencial, pero podemos controlar la forma en que se leen los elementos. Este modelo es también mucho más rápido que DOM, pero algo más lento que SAX. Un analizador sintáctico (en inglés parser) convierte el texto de entrada en otras estructuras (comúnmente árboles), que son más útiles para el posterior análisis; también captura la jerarquía implícita de la entrada.
338
CONTENT PROVIDERS, SERVICIOS Y NOTIFICACIONES
ÍNDICE 7.1
CONTENT PROVIDERS ............................................................. 341 7.1.1 Introducción ................................................................... 341 7.1.2 Proveedores de contenido (Content Providers) ....... 341 7.1.3 Construcción de un Content Provider ........................ 342
7.2
Uso de un Content Provider nuevo ........................................ 352
7.3
Uso de un Content Provider ya existente en Android ......... 355
7.4 SERVICIOS DE ANDROID Y RECEPTORES DE MENSAJES DE DIFUSIÓN .............................................................................................. 359 7.4.1 Servicios (Services) ...................................................... 359 7.4.2 Servicios propios ........................................................... 360 7.4.3 Receptor de mensajes de difución (Broadcast Receiver) 361 7.4.4 Intención pendiente (Pending Intent) ......................... 361 7.4.5 Ejemplo de Receptor de mensajes (Broadcast Receiver) ................................................................................ 362 7.4.6 Ejemplo de envío y recepción de mensajes internos en una aplicación y uso de servicios por defecto de Android364 7.4.7 Crear un servicio propio ............................................... 367 7.5
NOTIFICACIONES AL USUARIO EN ANDROID ..................... 373 7.5.1 Mensajes emergentes (Toast) .................................... 373 7.5.2 Notificaciones en la barra de estado.......................... 378
7.6
USO DE VIEWPAGER EN APLICACIONES ANDROID ......... 383 7.6.1 Cómo se usa el componente ViewPager .................. 385
2
Content Providers, servicios y notificaciones
7.1
CONTENT PROVIDERS
7.1.1 Introducción En esta Unidad vamos a estudiar los proveedores de contenidos (Content Providers) para compartir información entre aplicaciones y el Resolvedor de contenidos (Content Resolver) para consultar y actualizar la información de los Content Providers. Después, explicaremos cómo funcionan los servicios en Android. A continuación, detallaremos el uso de notificaciones en las aplicaciones Android. Finalmente, veremos cómo utilizar el componente ViewPager que permite cambiar de pantalla deslizando el dedo horizontalmente en el dispositivo.
7.1.2 Proveedores de contenido (Content Providers) Un Proveedor de contenido (en inglés Content Provider) es el mecanismo proporcionado por Android para compartir información entre aplicaciones. Una aplicación que desee compartir de manera controlada parte de la información que almacena con resto de aplicaciones debe declarar un Content Provider al sistema operativo a través del cuál se realiza el acceso a dicha información. Este mecanismo lo utilizan muchas aplicaciones estándar de un dispositivo Android, como la lista de contactos, la aplicación de SMS para mensajes cortos, el calendario, etcétera. Es decir, podemos acceder desde una aplicación cualquiera a los datos gestionados por otras aplicaciones Android haciendo uso de los Content Providers correspondientes. Para ello, es preciso que la aplicación tenga asignados los permisos adecuados para acceder a estos contenidos. Android, de serie, incluye varios proveedores de contenido para los tipos de datos más comunes, como audio, vídeo, imágenes, agenda de contactos personal, etcétera. Puedes ver el listado completo en el paquete android.provider. En este apartado vamos a tratar dos funcionalidades diferenciadas, que son las siguientes: •
Construcción de nuevos Content Providers personalizados, para que otras aplicaciones puedan acceder a la información contenida en la nuestra.
•
Utilización de un Content Provider ya existente, para que la nuestra pueda acceder a los datos publicados por otras aplicaciones.
En la Unidad 5 ya hemos visto un ejemplo muy sencillo sobre del acceso a un Content Provider ya existente, concretamente en la lista de contactos de Android.
341
Dado que es importante conocer el funcionamiento interno de un Content Provider, antes de pasar a utilizarlo en nuestras aplicaciones, vamos a estudiar cómo se construye.
7.1.3 Construcción de un Content Provider Hay dos formas de compartir información de una aplicación: implementando el proveedor de contenidos propio mediante la clase ContentProvider de Android o agregando datos a un proveedor ya existente en el dispositivo, siempre y cuando los tipos de datos sean parecidos y la aplicación tenga permiso para escribir en el Content Provider. En el Ejemplo 1 de esta Unidad vamos a mostrar cómo crear un Content Provider nuevo y cómo usarlo. Por simplificación, será la misma aplicación la que acceda al Content Provider interno, si bien el código necesario desde otra aplicación es exactamente el mismo. Desde Eclipse puedes abrir el proyecto Ejemplo 1 (Content Provider) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa, en el que hemos utilizado un Content Provider.
Si ejecutas la aplicación, verás que tiene el siguiente aspecto:
Fíjate que en este ejemplo los botones "Insertar" y "Eliminar" son excluyentes. Sólo se puede borrar un alumno si previamente ha sido dado de alta y viceversa. Se trata de un Content Provider que comparte información de los alumnos de un colegio. La aplicación del colegio almacena la información que queremos compartir en una base de datos SQLite. 342
Content Providers, servicios y notificaciones
Si bien internamente podemos tener la información almacenada de cualquier otra forma, por ejemplo, en un ficheros de tipo texto, en XML, etcétera, en este ejemplo vamos a usar una base de datos porque es más fácil gestionar información estructurada. El Content Provider es el mecanismo que permite compartir estos datos con otras aplicaciones de una forma homogénea usando una interfaz estandarizada. Las tablas de la base de datos SQLite usadas por un Content Provider deben incluir siempre el campo _ID que identifica sus registros de forma unívoca.
En este ejemplo, los registros devueltos por el Content Provider de alumnos tiene este aspecto:
Lo primero que hemos hecho en este Ejemplo es crear una aplicación muy simple que almacena y consulta los datos de los alumnos con la estructura similar a la tabla anterior. Para esto, aplicamos los mismos conceptos que ya hemos estudiado en la Unidad 6 para el tratamiento de bases de datos. Creamos una clase heredada de SQLiteOpenHelper donde definimos las sentencias SQL que crean la tabla de alumnos implementando los métodos onCreate() y onUpgrade(). El código de esta nueva clase tiene este aspecto:
public class ColegioSqliteHelper extends SQLiteOpenHelper {
//Sentencia SQL para crear la tabla de Alumnos en la BD BDColegio String sqlCreate = "CREATE TABLE Alumnos " + "(_id INTEGER PRIMARY KEY AUTOINCREMENT, " + " nombre TEXT, " + " apellidos TEXT, " + " curso TEXT )";
343
public ColegioSqliteHelper(Context contexto, String nombre, CursorFactory factory, int version) { super(contexto, nombre, factory, version); } @Override public void onCreate(SQLiteDatabase db) { //Se ejecuta la sentencia SQL de creación de la tabla db.execSQL(sqlCreate);
String[] nombres={"Juan", "José", "Miguel", "Antonio", "Alicia", "Luis", "Fernanda", "Lucía", "Mercedes", "Elisa"}; String[] apellidos={"Valle", "Fernández", "Martín", "Navas", "Conde", "Díaz", "Verdú", "Cuenca", "Pérez", "Sevilla"}; String[] cursos={"1º ESO", "1º ESO", "2º ESO", "3º ESO", "1º ESO", "4º ESO", "2º ESO", "2º ESO", "1º ESO", "4º ESO"};
//Insertamos 10 alumnos de ejemplo for(int i=0; i<10; i++) { //Insertamos los datos en la tabla Alumnos db.execSQL("INSERT INTO Alumnos (nombre, apellidos, curso) " + "VALUES ('" + nombres[i] + "', '" + apellidos[i] +"', '" + cursos[i] + "')"); } } @Override public void onUpgrade(SQLiteDatabase db, int versionAnterior, int versionNueva) { // NOTA: Por simplicidad, se elimina la tabla anterior y se crea de nuevo. //
Sin embargo, lo normal sería migrar datos de la tabla antigua
//
a la nueva, por lo que este método debería ser más complejo.
//Se elimina la versión anterior de la tabla db.execSQL("DROP TABLE IF EXISTS Alumnos");
344
Content Providers, servicios y notificaciones
//Se crea la nueva versión de la tabla db.execSQL(sqlCreate); } }
Fíjate que hemos incluido el campo _id en la tabla de la base de datos de alumnos. Este campo lo declaramos como INTEGER PRIMARY KEY AUTOINCREMENT para que se incremente automáticamente cada vez que insertamos un nuevo registro en la tabla. Además, esta clase añade algunos registros de ejemplo para poder hacer pruebas. Una vez que ya contamos con una aplicación que ha definido su base de datos, vamos a construir el nuevo Content Provider que permite compartir sus datos con otras aplicaciones. El acceso a un Content Provider se realiza siempre mediante un identificador URI. Un identificador URI es una cadena de texto parecida a una dirección Web de Internet. Es decir, si para acceder a Google con el navegador escribimos “http://www.google.es“, para acceder a un Content
Provider
utilizamos
una
dirección
similar
a
“content://es.mentor.unidad7.ejemplo/alumnos“. Los identificadores URI de los Content Providers se pueden dividir en tres partes: •
Prefijo content://: indica que dicho recurso debe ser tratado por un Content Provider.
•
Identificador del Content Provider (también llamado authority): este campo debe ser único en cada dispositivo Android; por esto, es una buena práctica definir un authority con el nombre de clase java invertido, por ejemplo, en este ejemplo es “es.mentor.ejemplo7.ejemplo“.
•
Esquema o Entidad concreta de datos que queremos que comparta el Content Provider. En este caso indicamos simplemente la tabla de “alumnos“. Un Content Provider puede contener datos de varias entidades distintas en esta última parte del URI. Todo esto es importante, ya que será nuestro Content Provider el encargado de interpretar (parsear) el URI completo para determinar los datos que se le están solicitando. Esto lo veremos un poco más adelante en detalle.
Por último, apuntamos que, en el URI se puede hacer referencia directamente a un registro concreto de la entidad seleccionada. Esto se hace indicando al final del URI de dicho registro. Por ejemplo, el URI “content://es.mentor.unidad7.ejemplo/alumnos/17” hace referencia directa al alumno con _ID = 17. A continuación, vamos a crear el Content Provider de la aplicación. Para esto, hay que extender la clase ContentProvider. Esta clase dispone de los métodos abstractos siguientes , que podemos implementar: 345
•
onCreate(): se usa para inicializar todos los recursos necesarios para el funcionamiento del nuevo Content Provider.
•
query(): permite consultar datos que haya en el Content Provider.
•
insert(): permite insertar datos en el Content Provider.
•
update(): permite actualizar datos del Content Provider.
•
delete(): permite borrar datos del Content Provider.
•
getType(): permite conocer el tipo de dato devuelto por el Content Provider.
Además de implementar estos métodos, también definimos una serie de cadenas constantes en la clase del Content Provider. A continuación, estudiamos por partes la nueva clase ColegioContentProvider que extienda de ContentProvider. En primer lugar definimos el URI con el que se accede al Content Provider de la aplicación: Vamos a usar “content://es.mentor.unidad7.ejemplo/alumnos”:
//Definición del CONTENT_URI private static final String uri = "content://es.mentor.unidad7.ejemplo/alumnos"; public static final Uri CONTENT_URI = Uri.parse(uri);
En todos los Content Providers de Android es necesario encapsular este identificador URI en un objeto estático del tipo Uri que hemos llamado CONTENT_URI. A continuación, definimos varias constantes con los nombres de los campos proporcionados por el Content Provider. Como ya hemos comentado anteriormente, existen columnas predefinidas que deben tener todos los Content Providers, como la columna _ID. Esta columna estándar está definida internamente en la clase BaseColumns, por lo que al añadir los campos (columnas) del Content Provider sólo hay que indicar las nuevas columnas.
//Clase interna para declarar las constantes de las columnas = campos public static final class Alumnos implements BaseColumns { private Alumnos() {} //Nombres de las columnas public static final String COL_NOMBRE = "nombre"; public static final String COL_APELLIDOS = "apellidos"; public static final String COL_CURSO = "curso";
346
Content Providers, servicios y notificaciones
}
Por último, vamos a definir varias cadenas constantes privadas que almacenen información auxiliar con el nombre de la base de datos, su versión y la tabla a la que accede el Content Provider.
private ColegioSqliteHelper colegioBDhelper; private static final String BD_NOMBRE = "BDColegio"; private static final int BD_VERSION = 1; private static final String TABLA_ALUMNOS = "Alumnos";
Lo primero que debe hacer un Content Provider cuando otra aplicación le solicita una operación es interpretar el URI utilizado. Para facilitar esta tarea al programador, Android proporciona la clase llamada UriMatcher que interpreta los patrones en un URI. Esto es muy útil para determinar, por ejemplo, si un URI hace referencia a una tabla genérica o a un registro concreto a través de su ID: •
content://es.mentor.unidad7.ejemplo/alumnos: acceso genérico a la tabla de alumnos.
•
content://es.mentor.unidad7.ejemplo/alumnos/17: acceso directo al alumno con el ID = 17.
Para ello definimos también en esta clase un objeto UriMatcher y dos nuevas constantes que representan los dos tipos de URI que hemos indicado: acceso genérico a la tabla (ALUMNOS) o acceso a un alumno por ID (ALUMNOS_ID). Después, creamos el objeto UriMatcher indicando el formato de ambos tipos de URI de forma que pueda diferenciarlos y devolvernos su tipo (una de las dos constantes definidas, ALUMNOS o ALUMNOS_ID): //Necesario para UriMatcher private static final int ALUMNOS = 1; private static final int ALUMNOS_ID = 2; private static final UriMatcher uriMatcher;
static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("es.mentor.unidad7.ejemplo", "alumnos", ALUMNOS); uriMatcher.addURI("es.mentor.unidad7.ejemplo", "alumnos/#", ALUMNOS_ID); }
347
En el código anterior vemos que mediante el método addUri() indicamos el campo authority del URI, el formato de la entidad que estamos solicitando y el tipo que identifica el formato del dato. Más adelante veremos cómo utilizar esto de forma práctica. Posteriormente, vamos a implementar los métodos internos del Content Provider. El primero de ellos es onCreate(). En este método inicializamos la base de datos mediante la clase ColegioSqliteHelper que creamos anteriormente:
public boolean onCreate() { // Inicializamos el conector con la BD colegioBDhelper = new ColegioSqliteHelper( getContext(), BD_NOMBRE, null, BD_VERSION); return true; }
El método más importante del Content Provider es query(). Este método recibe como parámetros un URI, una lista de nombres de columna, un criterio de selección, una lista de valores para las variables utilizadas en el criterio anterior y un criterio de ordenación. Todos estos parámetros son similares a los que estudiamos cuando tratamos sobre las bases de datos SQLite para Android. El método query() devuelve los datos solicitados según el URI, los criterios de selección y ordenación indicados como parámetros. Así, si el URI hace referencia a un alumno en concreto por su ID, ése debe ser el único registro devuelto. Si se solicita el contenido de la tabla de alumnos, hay que realizar la consulta SQL correspondiente a la base de datos respetando los criterios pasados como parámetros. Para distinguir entre los dos tipos posibles de URI utilizamos el método match() del objeto uriMatcher. Si el tipo devuelto es ALUMNOS_ID, es decir, se ha solicitado información de un alumno en concreto, sustituimos el criterio de selección por uno que busca en la tabla de alumnos sólo el registro con el ID indicado en la URI. Para obtener este ID utilizamos el método getLastPathSegment() del objeto uri, que extrae el último elemento de la URI, en este caso el ID del alumno. Después, hay que realizar la consulta a la base de datos mediante el método query() de SQLiteDatabase. Esto es muy fácil, ya que los parámetros son similares a los empleados en el método query() del Content Provider: public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Accedemos a la base de datos en modo lectura SQLiteDatabase db = colegioBDhelper.getReadableDatabase();
348
Content Providers, servicios y notificaciones
//Si es una consulta a un ID concreto construimos el WHERE String where = selection; if(uriMatcher.match(uri) == ALUMNOS_ID){ // Obtenemos el último segmento del URI where = "_id=" + uri.getLastPathSegment(); } // Hacemos la consulta a la BD Cursor c = db.query(TABLA_ALUMNOS, projection, where, selectionArgs, null, null, sortOrder); return c; }
Podemos observar que los resultados se devuelven en forma de Cursor, tal y como lo hace el método query() de SQLiteDatabase. Por otra parte, los métodos update() y delete() son completamente similares al método anterior. Únicamente se diferencian en que éstos devuelven como resultado el número de registros afectados en lugar de un cursor. Veamos su código:
@Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // Variable temporal int cont; // Accedemos a la base de datos en modo escritura SQLiteDatabase db = colegioBDhelper.getWritableDatabase(); //Si es una actualización a un ID concreto construimos el WHERE String where = selection; if(uriMatcher.match(uri) == ALUMNOS_ID){ where = "_id=" + uri.getLastPathSegment(); } // Actualizamos la tabla cont = db.update(TABLA_ALUMNOS, values, where, selectionArgs); // Devolvemos el nº de registros afectados por la consulta return cont; }
349
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Variable temporal int cont; // Accedemos a la base de datos en modo escritura SQLiteDatabase db = colegioBDhelper.getWritableDatabase(); //Si borramos un ID concreto construimos el WHERE String where = selection; if(uriMatcher.match(uri) == ALUMNOS_ID){ where = "_id=" + uri.getLastPathSegment(); } // Borramos los registros cont = db.delete(TABLA_ALUMNOS, where, selectionArgs); // Devolvemos el nº de registros afectados por la consulta return cont; }
El método insert() se implementa de forma ligeramente distinta. La diferencia en este caso está en que hay que devolver el URI que hace referencia al nuevo registro insertado. Para ello, obtenemos el nuevo ID del elemento insertado y construimos el nuevo URI de respuesta mediante el método auxiliar ContentUris.withAppendedId(), que recibe como parámetro el URI del Content Provider y el ID del nuevo elemento:
public Uri insert(Uri uri, ContentValues values) { // Variable temporal que guarda el ID dado de alta long regId = -1; // Accedemos a la base de datos en modo escritura SQLiteDatabase db = colegioBDhelper.getWritableDatabase(); // Insertamos el registro en la tabla regId = db.insert(TABLA_ALUMNOS, null, values); // Uri con el resultado de la operación Uri newUri = ContentUris.withAppendedId(CONTENT_URI, regId); return newUri; }
350
Content Providers, servicios y notificaciones
Por último, sólo queda implementar el método getType(). Este método se utiliza para identificar el tipo de datos que devuelve el Content Provider. Este tipo de datos se expresa como un MIME Type, tal y como hacen los navegadores Web para determinar qué tipo de datos se está recibiendo al hacer una petición a un servidor. Identificar el tipo de datos que devuelve un Content Provider ayuda a Android a determinar qué aplicaciones son capaces de procesar dichos datos. En este ejemplo, existen dos tipos MIME distintos para cada entidad del Content Provider: el primero se usa cuando se devuelve un registro único concreto y el segundo cuando se devuelven varios registros simultáneamente. Así, podemos utilizar los siguientes patrones para definir uno u otro tipo de datos: •
vnd.android.cursor.item/vnd.xxxxxx: Registro único
•
vnd.android.cursor.dir/vnd.xxxxxx: Listado de registros
En este ejemplo, hemos definido los siguientes tipos: •
vnd.android.cursor.dir/vnd.mentor.alumno
•
vnd.android.cursor.item/vnd.mentor.alumno
Teniendo esto en cuenta, la implementación del método getType() tiene estas sentencias:
@Override public String getType(Uri uri) { // Devolvemos un tipo de dato en función del URI int match = uriMatcher.match(uri); switch (match) { case ALUMNOS: return "vnd.android.cursor.dir/vnd.mentor.alumno"; case ALUMNOS_ID: return "vnd.android.cursor.item/vnd.mentor.alumno"; default: return null;
Se puede observar que utilizamos de nuevo el objeto UriMatcher para determinar el tipo de URI que se está solicitando y en función de éste devolvemos un tipo MIME u otro. Para
finalizar
con
el
Content
Provider,
debemos
declararlo
en
el
fichero
AndroidManifest.xml, para que, al instalar la aplicación en el dispositivo Android, éste conozca la existencia de dicho recurso. Para ello, basta con añadir un nuevo elemento 351
dentro de indicando el nombre del Content Provider y su authority:
...
7.2
Uso de un Content Provider nuevo Una vez completado el Content Provider, vamos a usarlo desde la propia aplicación
del ejemplo que hemos creado. Lo hacemos así para simplificar el ejemplo; de cualquier forma, el código necesario es exactamente el mismo si lo usamos desde otra aplicación distinta. Utilizar un Content Provider ya existente es muy sencillo, sobre todo si lo comparamos con todo el proceso anterior de construcción de uno nuevo. Para ello, vamos a usar la clase ContentResolver de Android que permite realizar acciones (consultas de datos, actualizaciones de información, etcétera) con cualquier Content Provider que esté disponible en el sistema operativo Android. Desde la actividad principal hay que utilizar el método getContentResolver() para obtener la referencia de la aplicación al objeto ContentResolver. Una vez obtenida esta referencia, podemos utilizar sus métodos query(), update(), insert() y delete() para realizar las acciones equivalentes sobre el Content Provider. En la aplicación del ejemplo anterior hay tres botones en la pantalla principal: uno para hacer una consulta de todos los alumnos, otro para insertar registros nuevos y el último para eliminar todos los registros nuevos insertados con el segundo botón. Empecemos por la consulta de alumnos. El procedimiento es prácticamente igual al que hemos estudiado para acceder a bases de datos SQLite. Primero definimos una matriz con los nombres de las columnas de la tabla que queremos recuperar en el resultado de la consulta: ID, nombre, apellidos y curso.
352
Content Providers, servicios y notificaciones
Tras esto, obtenemos una referencia al Content Resolver y utilizamos su método query() para obtener los resultados en forma de Cursor. El método query() se invoca con los parámetros siguientes: el Uri del Content Provider al que queremos acceder, la matriz de columnas que queremos recuperar, el criterio de selección, los argumentos variables y el criterio de ordenación de los resultados. En este caso, para no complicar el ejemplo tan sólo indicamos los dos primeros: CONTENT_URI del Content Provider y la matriz de columnas que acabamos de definir: //Columnas de la tabla String[] columnas = new String[] { Alumnos._ID, Alumnos.COL_NOMBRE, Alumnos.COL_APELLIDOS, Alumnos.COL_CURSO };
// Definimos la Uri que queremos usar Uri alumnosUri =
ColegioContentProvider.CONTENT_URI;
// Acceso al contentresolver de la aplicación ContentResolver cr = getContentResolver();
//Hacemos la consulta Cursor cur = cr.query(alumnosUri, columnas, //Columnas solicitadas null,
//Condición de la query
null, null);
//Argumentos variables de la query //Orden de los resultados
Una vez solicitada la consulta, hay que recorrer el cursor para procesar los registros. Veamos cómo queda el código fuente: // Si obtenemos resultados if (cur.moveToFirst()) { String nombre; String apellidos; String curso;
353
int colNombre = cur.getColumnIndex(Alumnos.COL_NOMBRE); int colApellidos = cur.getColumnIndex(Alumnos.COL_APELLIDOS); int colCurso = cur.getColumnIndex(Alumnos.COL_CURSO); txtResultados.setText("Resultado consulta:\n\n"); // Recorremos todos los registros y los mostramos en pantalla
do { nombre = cur.getString(colNombre); apellidos = cur.getString(colApellidos); curso = cur.getString(colCurso); txtResultados.append(nombre + " " + apellidos + ". Curso: " + curso + "\n"); } while (cur.moveToNext()); // end while }
Insertar nuevos registros se implementa exactamente igual que si tratáramos directamente con bases de datos SQLite. Rellenamos primero un objeto ContentValues con los datos del nuevo alumno y utilizamos el método insert() pasándole como parámetros la URI del Content Provider y los datos del nuevo registro: ContentValues values = new ContentValues(); values.put(Alumnos.COL_NOMBRE, "Jesús"); values.put(Alumnos.COL_APELLIDOS, "Sanz"); values.put(Alumnos.COL_CURSO, "BACHIDERATO"); ContentResolver cr = getContentResolver(); cr.insert(ColegioContentProvider.CONTENT_URI, values); txtResultados.setText("Se ha insertado el alumno. Pulsa el botón 'Consultar' para ver todos los alumnos.");
Por último, la eliminación de registros la hacemos directamente utilizando el método delete() del Content Resolver, indicando como segundo parámetro el criterio de identificación de los registros que queremos eliminar: ContentResolver cr = getContentResolver(); cr.delete(ColegioContentProvider.CONTENT_URI, Alumnos.COL_NOMBRE + " = 'Jesús'", null); txtResultados.setText("Se ha borrado el alumno. Pulsa el botón 'Consultar' para ver todos los alumnos.");
354
Content Providers, servicios y notificaciones
7.3
Uso de un Content Provider ya existente en Android
Hemos visto lo sencillo que resulta acceder a los datos proporcionados por un Content Provider. Mediante este mecanismo podemos utilizar en nuestras aplicaciones muchos datos de la propia plataforma Android. En la documentación oficial del paquete android.provider podemos consultar los datos que están disponibles a través de este mecanismo. Entre ellos encontramos el historial de llamadas, la agenda de contactos, etcétera. Para ver cómo se usan los Content Providers con un tipo de datos definido por Android, en el Ejemplo 2 de esta Unidad vamos a consultar el historial de llamadas del dispositivo, usando el Content Provider android.provider.CallLog. Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Historial de llamadas) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa, en el que hemos utilizado un Content Provider definido por Android. Para poder ver algún dato en este ejemplo, en primer lugar, vamos a registrar varias llamadas en el emulador de Android. Así, los resultados de la consulta al historial de llamadas devolverán algunos registros. A continuación, vamos a simular varias llamadas salientes desde el emulador y varias llamadas entrantes desde el DDMS. Las llamadas salientes son sencillas de realizar usando el emulador como si se tratara de un teléfono normal y corriente. Accedemos al icono teléfono, marcamos un número y descolgamos como si se tratara de un dispositivo físico:
355
Para simular llamadas entrantes debemos acceder desde Eclipse a la vista del DDMS. En esta vista, en la pestaña “Emulator Control” aparece el apartado “Telephony Actions“, donde podemos introducir un número cualquiera de teléfono origen “Incoming number” y pulsar el botón “Call” para que el dispositivo del emulador reciba una llamada entrante. Sin aceptar la llamada en el emulador, pulsaremos “Hang Up” para terminar la llamada simulando así una llamada perdida.
Una vez hemos simulado tanto llamadas entrantes como llamadas salientes, vamos a desarrollar una aplicación que consulte el historial de llamadas. Si consultamos la documentación del Content Provider android.provider.CallLog, veremos que podemos extraer diferentes datos relacionados con la lista de llamadas. En este ejemplo vamos a usar únicamente el número origen o destino de la llamada y el tipo de llamada (entrante, saliente y perdida). Los nombres de estas columnas se almacenan en las constantes Calls.NUMBER y Calls.TYPE respectivamente. A continuación, definimos una matriz con las columnas que vamos a recuperar, obtenemos la referencia al Content Resolver de la aplicación y ejecutamos la consulta llamando al método query(). Por último, recorremos el cursor obtenido y procesamos los resultados. Veamos el código fuente:
356
Content Providers, servicios y notificaciones
// Constantes que definen los campos que consultamos String[] columnas = new String[] {Calls.TYPE, Calls.NUMBER }; // La Uri está predefinida en una constante del sistema Uri llamadasUri =
Calls.CONTENT_URI;
// Cargamos el Content Resolver de la aplicación ContentResolver cr = getContentResolver(); // Hacemos una consulta de las llamadas Cursor cur = cr.query(llamadasUri, columnas, //Columnas a devolver null,
//Condición de la query
null,
//Argumentos variables de la query
null);
//Orden de los resultados
// Si hay llamadas mostramos la información if (cur.moveToFirst()) { int tipo; String tipoLlamada = ""; String telefono; // Obtenemos el índice de las columnas int colTipo = cur.getColumnIndex(Calls.TYPE); int colTelefono = cur.getColumnIndex(Calls.NUMBER); // Limpiamos la etiqueta de resultados txtResultados.setText(""); // Mientras haya datos mostramos la información al usuario do { // Obtenemos la información de las columnas tipo = cur.getInt(colTipo); telefono = cur.getString(colTelefono); // Según el tipo de llamada usamos un texto distinto if(tipo == Calls.INCOMING_TYPE) tipoLlamada = "ENTRADA"; else if(tipo == Calls.OUTGOING_TYPE)
357
tipoLlamada = "SALIDA"; else if(tipo == Calls.MISSED_TYPE) tipoLlamada = "PERDIDA"; // Mostramos la información txtResultados.append(tipoLlamada + " : " + telefono + "\n"); } while (cur.moveToNext()); // end while } else txtResultados.setText("No hay ninguna llamada en el histórico del teléfono. Para que funcione bien esta aplicación debes simular alguna llamada entrante o saliente. En la teoría del curso de esta Unidad se muestra cómo hacerlo.");
Además, en el código fuente anterior decodificamos el valor del tipo de llamada comparando el resultado con las constantes Calls.INCOMING_TYPE (llamada entrante), Calls.OUTGOING_TYPE (llamada saliente) y Calls.MISSED_TYPE (llamada perdida). Para que la aplicación pueda acceder al historial de llamadas del dispositivo hay que incluir en el fichero AndroidManifest.xml el permiso READ_CONTACTS:
Si ejecutas el ejemplo 2, verás que tiene el siguiente aspecto:
358
Content Providers, servicios y notificaciones
7.4
SERVICIOS DE ANDROID Y RECEPTORES DE MENSAJES DE DIFUSIÓN
7.4.1 Servicios (Services) Un Servicio (en inglés service) es un componente de una aplicación Android que se ejecuta en segundo plano, sin interactuar con el usuario (no tiene interfaz de usuario) y realiza operaciones de larga duración. La plataforma Android ofrece una gran cantidad de servicios predefinidos en el sistema a los que podemos acceder a través de las clases de tipo Manager. En una
Actividad
podemos acceder a estos servicios a través del método getSystemService(). Cuando una aplicación Android define sus propios Servicios, deben ser declarados en el fichero AndroidManifest.xml del proyecto. Un componente de una aplicación Android puede iniciar un servicio que seguirá funcionando en segundo plano, incluso si el usuario cambiara a otra aplicación. Además, un componente de la aplicación puede unirse (en inglés bind) al servicio para interactuar con él e incluso realizar comunicaciones entre procesos. Por ejemplo, un servicio podría conectarse a Internet en un segundo plano para descargar noticias, reproducir música, etcétera,. Un servicio puede funcionar de dos modos: •
Autónomo: cuando un componente de la aplicación, por ejemplo, una actividad, inicia el servicio mediante el método StartService(). Una vez arrancado, el servicio puede ejecutarse en segundo plano de forma indefinida, incluso si el componente que lo inició se destruye. Normalmente, un servicio iniciado de esta forma realiza una única operación y no devuelve el resultado al componente que lo inicia. Por ejemplo, puede descargar de Internet un archivo o cargarlo. Cuando la operación finaliza, el servicio debe detenerse.
•
Dependiente o Ligado (en inglés a este modo se le denomina "bind"): cuando un componente de la aplicación se une al servicio mediante el método bindService(). Un servicio ligado ofrece una interfaz de tipo cliente-servidor que permite a los componentes de una aplicación interactuar con él enviando peticiones y recibiendo su resultado. Un servicio ligado sólo se ejecuta mientras otro componente de la aplicación está unido a él. Es posible unir un mismo servicio a varios componentes de una o de varias aplicaciones al mismo tiempo; sin embargo, cuando todos ellos se “desligan”, el servicio se destruye.
Un servicio puede funcionar de las dos formas anteriores simultáneamente, es decir, se puede arrancar en modo Autónomo (de manera indefinida) y también en modo Ligado. 359
Simplemente hay que implementar los métodos onStartCommand() para el modo Autónomo y onBind() para el modo Ligado. Cualquier componente de una aplicación puede iniciar un servicio. Incluso un componente de otra aplicación distinta a la que define el servicio también puede iniciarlo de la misma forma que iniciaríamos una Actividad de otra aplicación mediante Intenciones. También se puede declarar un servicio como privado en la aplicación, en el archivo AndroidManifest.xml, y bloquear el acceso desde otras aplicaciones. Los servicios tienen que ser declarados en el archivo AndroidManifest.xml con la etiqueta y la implementación de la clase debe heredarse de la clase Service. IMPORTANTE: los servicios propios de una aplicación se ejecutan en el hilo principal de su proceso; por lo tanto, para no bloquear el hilo principal o de la interfaz debemos, ejecutar estos servicios con hilos de ejecución, tal y como hemos visto en la Unidad 3. 7.4.2 Servicios propios Una aplicación puede declarar su propio servicio para llevar a cabo operaciones que tarden en ejecutarse y no necesiten interactuar con el usuario o para suministrar una nueva funcionalidad a otras aplicaciones. A continuación, se muestra un esquema con los métodos que invoca Android cuando lanzamos un servicio según su modo de funcionamiento:
360
Content Providers, servicios y notificaciones
Una Actividad puede iniciar un servicio en modo Autónomo a través del método StartService() y detenerlo mediante el método StopService(). Cuando lo hacemos, Android invoca su método onCreate(); después, se invoca el método onStartCommand() con los datos proporcionados por la Intención de la actividad. En el método startService() también podemos indicar como parámetro el comportamiento del ciclo de vida de los servicios: •
START_STICKY: se utiliza para indicar que el servicio debe ser explícitamente iniciado o parado.
•
START_NOT_STICKY: el servicio termina automáticamente cuando el método onStartCommand() finaliza su ejecución.
Si la actividad quiere interactuar con un servicio (modo Dependiente o Ligado) para, por ejemplo, mostrar el progreso de una operación, puede utilizar el método bindService(). Para esto, hay que usar el objeto ServiceConnection, que permite conectarse al servicio y devuelve un objeto de tipo IBinder, que la actividad puede utilizar para comunicar con el servicio. Más adelante veremos en detalle cómo definir servicios en modo Ligado dentro de las aplicaciones Android.
7.4.3 Receptor de mensajes de difución (Broadcast Receiver) Hay casos en los que se usan mensajes de difusión (Broadcast) para comunicar eventos entre servicios. Estos mensajes son, en realidad, Intents. En este caso usamos la clase Receptor de mensajes de difusión (BroadcastReceiver), que debemos declarar en el archivo AndroidManifest.xml. Esta clase puede recibir Intenciones (Intents), es decir, mensajes enviados por otro componente de Android mediante el método sendBroadcast() de la clase Context (contexto de la aplicación). La clase BroadCastReceiver define el único método OnReceive() donde se recibe el mensaje de difusión; por lo tanto, fuera de este método no se puede realizar ninguna operación asíncrona porque el mensaje de difusión ya no está activo.
7.4.4 Intención pendiente (Pending Intent) En este apartado también hacemos uso de las Intenciones pendientes (Pending Intents). Una Intención pendiente es un tipo de Intent (mensaje entre componentes de Android) que permite que otra aplicación ejecute un bloque de código predefinido con los permisos de ejecución de la aplicación que inicia esta Intención pendiente. Este tipo de Intenciones se usa mucho para iniciar aplicaciones como el Administrador de notificaciones (Notification Manager) y Administrador de alarmas (Alarm Manager). 361
Para enviar un mensaje de difusión mediante una Intención pendiente hay que usar su método getBroadcast(). Para iniciar una subactividad mediante una Intención pendiente hay que usar su método getActivity().
7.4.5 Ejemplo de Receptor de mensajes (Broadcast Receiver) A continuación, vamos a definir un receptor de mensajes de difusión (Broadcast Receiver) que escucha los mensajes que lanza Android al resto de componentes del sistema operativo cuando ocurre un cambio en el estado del teléfono. Si el dispositivo recibe una llamada de teléfono, entonces nuestro receptor de mensajes recibirá una notificación y registrará la llamada. Para que la aplicación funcione bien, debemos incluir las siguientes sentencias en el archivo AndroidManifest.xml del proyecto:
...
En las sentencias anteriores hemos declarado al sistema usando la etiqueta que esta aplicación desea recibir los mensajes de difusión del tipo (etiqueta ) estado del teléfono (PHONE_STATE) usando la clase ReceptorLlamadas para gestionarlas. La clase ReceptorLlamadas que implementa el receptor de mensajes de difusión contiene las siguientes sentencias:
public class ReceptorLlamadas extends BroadcastReceiver {
362
Content Providers, servicios y notificaciones
@Override public void onReceive(Context context, Intent intent) { Bundle extras = intent.getExtras(); if (extras != null) { String estado = extras.getString(TelephonyManager.EXTRA_STATE); Log.w("ESTADO TELEFONO", estado); if (estado.equals(TelephonyManager.EXTRA_STATE_RINGING)) { String numeroTelefono= extras .getString(TelephonyManager.EXTRA_INCOMING_NUMBER); Log.w("NUMERO TELEFONO", numeroTelefono); } } } }
Como hemos comentado anteriormente, el mensaje de difusión se recibe en el método onReceive() de la clase BroadcastReceiver. En este método hemos obtenido la información extra de la intención y la hemos mostrado en el Log de mensajes de Eclipse.
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Receptor de mensajes de difusión) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos definido un Receptor de mensajes de difusión. Si ejecutamos la aplicación, usando el DDMS para simular una llamada de teléfono entrante, veremos la siguiente pantalla:
363
7.4.6 Ejemplo de envío y recepción de mensajes internos en una aplicación y uso de servicios por defecto de Android En este Ejemplo 3 vamos a usar el Gestor de alarmas (AlarmManager) y el de vibraciones del teléfono (VibratorManager) para iniciar los servicios por defecto "Alarma" y "Vibración" de Android. Vamos a configurar una alarma en el gestor de alarmas de Android y, cuando termine la cuenta atrás del tiempo que establezca el usuario, el gestor de alertas mandará un mensaje de difusión al receptor de mensajes que hemos definido previamente en la misma aplicación. Para recibir el mensaje de difusión hemos creado el receptor MiBroadcastReceiver a partir de la clase BroadcastReceiver:
public class MiBroadcastReceiver extends BroadcastReceiver { @Override // Definimos el método onReceive para recibir mensajes de difusión public void onReceive(Context context, Intent intent) { Toast.makeText(context, "¡Se ha acabado la cuenta atrás! \nEl teléfono está vibrando", Toast.LENGTH_LONG).show(); // Vibramos el teléfono durante 2 segundos obteniendo el servicio Vibrator de Android Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(2000);
364
Content Providers, servicios y notificaciones
} }
Este receptor de mensajes busca el servicio de Vibración (Vibrator), se conecta a él y le indica que vibre el teléfono durante dos segundos. Para cargar el servicio "Vibración" por defecto de Android hemos usado el método getSystemService(), al que indicamos como parámetro el nombre del servicio al que queremos acceder. Para que Android conozca que tiene disponible un receptor de mensajes de difusión y permita a la aplicación el acceso al servicio de vibración, debemos añadir al fichero AndroidManifest.xml las siguientes líneas:
...
A continuación, solo queda indicar en la actividad principal que se inicie una la cuenta atrás:
public class AlarmaActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); }
public void iniciarCuenta(View view) { // Obtenemos el tiempo de la cuenta atrás EditText texto = (EditText) findViewById(R.id.tiempo); if (texto.getText().equals("")){ Toast.makeText(this, "Al menos debes indicar 1 segundo", Toast.LENGTH_LONG).show(); return; }
365
int i = Integer.parseInt(texto.getText().toString()); // Cargamos el BroadcastReceiver Intent intent = new Intent(this, MiBroadcastReceiver.class); // Lo iniciamos como una Intención pendiente PendingIntent pendingIntent = PendingIntent.getBroadcast( this.getApplicationContext(), 1, intent, 0); // Creamos una alarma AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); // Establecemos el tiempo de la alarma e indicamos el pendingIntent que se debe ejecutar cuando acabe la cuenta alarmManager.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + (i * 1000), pendingIntent); // Mostramos un mensaje indicando que comienza la cuenta atrás Toast.makeText(this, "Inicio de Cuenta atrás de " + i + " segundos", Toast.LENGTH_LONG).show(); } }
Para cargar el servicio "Alarma" por defecto de Android, hemos usado el método getSystemService(), al que indicamos como parámetro el nombre del servicio al que queremos acceder. En el código anterior podemos observar que hemos usado la clase AlarmManager para acceder al servicio de gestión de alarmas. Con su método set() se crea una nueva alarma que salta pasados n segundos y que lanza, a continuación, la intención pendiente (es realidad es una intención que hereda los permisos de la actividad principal). Esta intención pendiente se forma a partir de una intención normal que invoca explícitamente la clase que recibe el mensaje y que transformamos en un mensaje de difusión con el método getBroadcast() de PendingIntent. Si ejecutas el Ejemplo 3 de esta Unidad verás las siguientes pantallas:
366
Content Providers, servicios y notificaciones
7.4.7 Crear un servicio propio En el Ejemplo 4 de esta Unidad vamos a ver cómo definir un servicio privado en modo Ligado dentro de una aplicación Android. Los servicios deben utilizarse para mantener en segundo plano tareas en ejecución de la aplicación, como descargar mensajes de correo de un servidor. Cuando el usuario solicita que se actualice su buzón de correo, la aplicación que ya está ligada (en inglés bind) al servicio, invoca uno de sus métodos para obtener los nuevos mensajes recibidos. Como ya hemos comentado, para crear un servicio debemos definir una clase que se extienda de la clase Service de Android:
public class Servicio extends
Service {
// Variable donde guardamos los datos que devuelve el servicio private ArrayList listado = new ArrayList(); // Constante donde tenemos los datos que vamos a ir cargando cada 5 segundos en la variable anterior private static String[] listadoDatos = { "El comercio internacional ha aumentado un 7%", "Hoy se publica un nuevo libro de Pérez Jiménez", "Benetton retira la foto que irritó al Vaticano", "Diego Rivera vuelve al Nueva York de la crisis",
367
"Facebook reconoce un ataque coordinado", "Bradley Cooper, el hombre más sexy del mundo", "Dimite el responsable en Europa del FMI por 'motivos personales'", "El invierno ya está aquí" }; // Usamos el temporizador para ir añadiendo datos al listado private Timer temporizador = new Timer(); // Cada 5 segundos actualizamos los datos del listado private static final long INTERVALO = 5000; // IBinder que usa la actividad principal para unirse al servicio y obtener información private final IBinder miBinder = new MiBinder(); // Variable que usamos para controlar el último elemento añadido al listado private int indice = 0;
// Debemos definir redefinir el método onCreate public void onCreate() { super.onCreate(); // Iniciamos el temporizado que va cargando datos poco a poco en el listado temporizador.scheduleAtFixedRate(new TimerTask() { @Override public void run() { // Si el listado ya contiene los 7 elementos, quitamos el primer elemento if (listado.size() >= 8) { listado.remove(0); } // Añadimos el listado el elemento siguiente de la matriz constante listado.add(listadoDatos[indice++]); // Si ya hemos llegado al último elemento, volvemos a empezar if (indice >= listadoDatos.length) { indice = 0; } }
368
Content Providers, servicios y notificaciones
}, 0, INTERVALO); }
// Debemos redefinir el método onDestroy @Override public void onDestroy() { super.onDestroy(); // Si el temporizador sigue funcionando, lo liberamos de la memoria if (temporizador != null) { temporizador.cancel(); } }
// Es obligatorio redefinir este método. // Devuelve el canal de comunicación con el servicio. @Override public IBinder onBind(Intent arg0) { return miBinder; }
// Clase que devuelve el contexto del servicio public class MiBinder extends Binder { Servicio getService() { return Servicio.this; } }
// Método del servicio que invoca la actividad principal public List getDatos() { return listado; } }
369
Como vamos a usar el servicio en modo Ligado, hemos definido el método onBind() en el código Java anterior. En el archivo AndroidManifest.xml debemos declarar el nuevo servicio:
En la actividad principal del Ejemplo 4 implementamos cómo usar el servicio en modo Ligado:
public class ServicioActivity extends Activity { // Variable donde almacenamos el servicio private Servicio s; // Matriz que se usa para cargar el adaptador del ListView de la actividad principal private ArrayList matrizAdaptador; // Adaptador del ListView de la actividad principal private ArrayAdapter adaptador;
// Variable que recibe la Conexión al servicio de la aplicación private ServiceConnection miConexion = new ServiceConnection() { // Al conectar al servicio, obtenemos una referencia del mismo y // mostramos un mensaje al usuario public void onServiceConnected(ComponentName className, IBinder binder) { s = ((Servicio.MiBinder) binder).getService(); Toast.makeText(ServicioActivity.this, "Conectado al servicio", Toast.LENGTH_SHORT).show(); } // Desconexión del servicio, liberamos variables public void onServiceDisconnected(ComponentName className) { s = null; } };
370
Content Providers, servicios y notificaciones
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
// Unimos esta actividad al servicio indicando mediante una Intención explícita el nombre del servicio, la variable de conexión que recibe el puntero del servicio y el modo de operación bindService(new Intent(this, Servicio.class), miConexion, Context.BIND_AUTO_CREATE);
// Cargamos referencias ListView de la pantalla principal matrizAdaptador = new ArrayList(); adaptador = new ArrayAdapter(this, android.R.layout.simple_list_item_1, matrizAdaptador); ListView list = (ListView) findViewById(R.id.list); list.setAdapter(adaptador); }
// Método que se invoca cuando el usuario hace clic sobre el botón de la pantalla principal public void buscarDatosServicio(View view) { // Si el servicio está activo if (s != null) { // Obtenemos los nuevos datos del servicio List datos = s.getDatos(); // Limpiamos el adaptador con los nuevos datos matrizAdaptador.clear(); matrizAdaptador.addAll(datos); // Indicamos que los datos del adaptador han cambiado adaptador.notifyDataSetChanged(); } } }
Para conectar con el servicio definido en la clase Servicio, hemos escrito la sentencia: 371
bindService(new Intent(this, Servicio.class), miConexion, Context.BIND_AUTO_CREATE);
El método bindService (Intent service, ServiceConnection conn, int flags) se invoca con los siguientes tres parámetros:
•
service: Intent que identifica el servicio al que queremos conectar. Este Intent puede ser explícito (como en el ejemplo) indicando el nombre de la clase que implementa el servicio o implícito señalando la acción que se define mediante un IntentFilter de un servicio publicado en el sistema.
•
conn: recibe la información del resultado de la clase de conexión ServiceConnection.
•
flags: opciones que podemos indicar al unirnos al servicio. Puede contener 0, BIND_AUTO_CREATE (crea el servicio mientras haya componentes ligados a él), BIND_DEBUG_UNBIND (incluye información de depuración cuando se produce un desligue de los componentes), BIND_NOT_FOREGROUND (no permite que el servicio cambie de hilo de ejecución), BIND_ABOVE_CLIENT (el servicio tiene más prioridad de ejecución que la aplicación que lo inicia), BIND_ALLOW_OOM_MANAGEMENT
(servicio
normal
que
puede
ser
eliminado de memoria si el sistema la necesita) o BIND_WAIVE_PRIORITY (el servicio se trata en segundo plano sin cambio de prioridad), etcétera.
Desde Eclipse puedes abrir el proyecto Ejemplo 4 (Servicio) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos definido un Servicio.
Si ejecutas la aplicación y pulsas el botón “Cargar información del servicio”, verás la siguiente pantalla:
372
Content Providers, servicios y notificaciones
Si pulsas el botón cada 5 segundos, verás que la aplicación recarga datos en la pantalla principal.
7.5
NOTIFICACIONES AL USUARIO EN ANDROID
En Android existen varias formas de notificar mensajes o información al usuario. En la Unidad 3 de este curso ya hemos visto el uso de Diálogos para mostrar al usuario información e, incluso, solicitar que introduzca algún texto. En este apartado vamos a estudiar dos tipos de notificaciones más: •
Mensajes emergentes: en inglés Toast. Aunque ya hemos usado este tipo de mensajes previamente en el curso, vamos a describir con más detalle toda su funcionalidad, ya que son muy útiles en las aplicaciones Android.
•
Mensajes en la barra de estado. Son mensajes que aparecen en forma de icono en la barra de estado en la parte superior del dispositivo:
7.5.1 Mensajes emergentes (Toast) Un mensaje emergente (en inglés Toast) es un mensaje que se muestra en la pantalla del dispositivo Android durante unos segundos y desaparece automáticamente sin requerir ningún tipo de actuación por parte del usuario. 373
Este mensaje no recibe el foco de la aplicación en ningún momento, es decir, no interfiere con las acciones que esté realizando el usuario en ese momento. Por defecto, aparecen en la parte inferior de la pantalla, dentro de un rectángulo gris ligeramente translúcido. Este tipo de notificaciones son perfectas para mostrar mensajes rápidos y sencillos al usuario, puesl no requiere confirmación. Ya hemos visto durante el curso que su utilización es muy sencilla. La clase Toast dispone del método estático makeText(Context context, CharSequence text, int duration) al que debemos pasar como parámetros el contexto de la actividad, el texto del mensaje y el tiempo que de permanecer en la pantalla en milisegundos. En el parámetro duration podemos usar las siguientes constantes definidas por Android: •
Toast.LENGTH_LONG: mensaje de duración larga. Se usa para textos muy largos.
•
Toast.LENGTH_SHORT: mensaje de duración corta. Se usa para mensajes más cortos.
Tras obtener una referencia al objeto Toast a través de este método, usamos el método show() para mostrar el mensaje en la pantalla. En el Ejemplo 5 de esta Unidad vamos a definir distintos tipos de Toast. Desde Eclipse puedes abrir el proyecto Ejemplo 5 (Notificaciones) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos usado distintos tipos de Toast.
Para comenzar, vamos a incluir un botón que muestre un Toast básico cuando hagamos clic sobre él:
// Toast por defecto xDefectoBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // Creamos el mensaje Toast toast1 = Toast.makeText(getApplicationContext(), "Toast por defecto", Toast.LENGTH_SHORT); // Mostramos el mensaje toast1.show();
374
Content Providers, servicios y notificaciones
} });
Si ejecutas la aplicación y pulsas el botón “Toast – Por defecto” verás la siguiente pantalla:
También podemos personalizar este Toast cambiando su posición relativa en la pantalla. Para esto utilizamos su método setGravity(), al que indicamos en qué zona deseamos que aparezca la notificación. Esta zona se marca usando alguna de las constantes definidas en la clase Gravity: CENTER, LEFT, BOTTOM, etcétera, o utilizando una combinación de éstas. En el Ejemplo 5 vamos a colocar el mensaje en la zona central derecha de la pantalla. Para esto, hay un segundo botón en la aplicación que muestra un -Toast con estas características:
// Toast con posicionamiento en pantalla gravityBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { Toast toast2 = Toast.makeText(getApplicationContext(), "Toast con gravity", Toast.LENGTH_SHORT); // Indicamos el posicionamiento toast2.setGravity(Gravity.CENTER|Gravity.RIGHT,0,0); toast2.show();
375
} });
Si volvemos a ejecutar la aplicación y pulsamos el nuevo botón, veremos que el Toast aparece en la zona indicada de la pantalla:
Es posible personalizar por completo el aspecto del mensaje. Android ofrece la posibilidad de definir un fichero de diseño (layout) XML propio para Toast, donde podemos incluir todos los elementos necesarios para adaptar la notificación a las necesidades de la aplicación. Para este Ejemplo 5 hemos definido un layout sencillo con una imagen y una etiqueta de texto sobre un rectángulo gris. Si abres el fichero res/layout/layout_toast.xml podrás ver su diseño:
376
Content Providers, servicios y notificaciones
android:padding="5dip" >
Para asignar este fichero de diseño (layout) a un Toast, hay que proceder de una forma algo distinta a como lo hemos hecho en las anteriores notificaciones. En primer lugar, hay que inflar el layout mediante un objeto LayoutInflater, como ya hemos usado en varias ocasiones a lo largo del curso, para diseñar la interfaz de usuario. Una vez construido el layout, modificamos los valores de los distintos componentes internos de éste para mostrar la información. En este ejemplo, modificamos el mensaje de la etiqueta de texto y asignamos estáticamente una imagen en el layout XML mediante el atributo android:src. Después, establecemos la duración de la notificación con el método setDuration() y asignamos el layout personalizado al Toast mediante el método setView(). El código fuente incluido en el tercer botón del ejemplo tiene este aspecto:
// Toast con diseño layoutBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // Creamos el Toast Toast toast3 = new Toast(getApplicationContext()); // Inflamos el diseño de layout_toast.xml LayoutInflater inflater = getLayoutInflater();
377
View layout = inflater.inflate(R.layout.layout_toast, (ViewGroup) findViewById(R.id.layoutToast)); // Asignamos los componentes del diseño TextView txtMsg = (TextView)layout.findViewById(R.id.mensajeLbl); txtMsg.setText("Toast con diseño personalizado"); // Indicamos la duración corta para el mensaje toast3.setDuration(Toast.LENGTH_SHORT); // Asignamos el diseño al Toast toast3.setView(layout); // Mostramos el Toast toast3.show(); } });
Si ejecutamos ahora la aplicación del ejemplo y pulsamos el botón “Toast – Personalizado”, aparece el Toast con la estructura definida en el archivo de diseño layout personalizado:
7.5.2 Notificaciones en la barra de estado En este apartado vamos a tratar otro tipo de notificaciones más persistentes y complejas de implementar, que son las notificaciones de la barra de estado de Android. Estas notificaciones son las que muestran los dispositivos Android cuando recibimos un mensaje SMS, hay actualizaciones disponibles, está el reproductor de música funcionando en segundo plano, etcétera. Estas notificaciones consisten en un icono y un texto que aparece en la barra de estado superior. Adicionalmente, podemos indicar un mensaje más largo y descriptivo y una marca de fecha/hora que aparece al desplegar la bandeja del sistema. 378
Content Providers, servicios y notificaciones
Por ejemplo, cuando hay una llamada perdida en nuestro teléfono, se muestra en un lado el siguiente icono en la barra de estado:
Arrastrar
Si arrastramos la barra de estado del dispositivo, se despliega la bandeja del sistema con más información. En este ejemplo en concreto se informa del evento producido (“Missed calls“), los números de teléfonos asociados y la fecha/hora del evento. Además, al pulsar sobre la notificación se abre automáticamente el historial de llamadas.
En el Ejemplo 5 de esta Unidad vamos a utilizar este tipo de notificaciones. En este ejemplo hemos añadido un nuevo botón que genera una notificación en la barra de estado con los elementos comentados y con la posibilidad de dirigirnos a la propia aplicación del ejemplo cuando se pulsa sobre la notificación. Para generar notificaciones en la barra de estado del sistema, lo primero que hay que hacer es obtener una referencia al servicio de notificaciones de Android usando la clase NotificationManager. Utilizamos el método getSystemService() indicando como parámetro el identificador del servicio al que queremos conectar, en este caso a Context.NOTIFICATION_SERVICE.
//Obtenemos una referencia al servicio de notificaciones
379
String ns = Context.NOTIFICATION_SERVICE; NotificationManager notManager = (NotificationManager) getSystemService(ns);
Después, configuramos las características de la notificación. En primer lugar, establecemos el icono y el texto que aparece en la barra de estado. También registramos la fecha y hora asociadas a la notificación. Con estos datos construimos un objeto Notification. En este ejemplo, utilizamos un icono predefinido de Android, el mensaje “¡Atención!” y registramos la fecha/hora actual indicada por el método System.currentTimeMillis():
//Configuramos la notificación que va a aparecer en la barra int icono = android.R.drawable.stat_sys_warning; CharSequence textoEstado = "¡Atención!"; long hora = System.currentTimeMillis();
// Creamos la notificación Notification notificacion = new Notification(icono, textoEstado, hora);
A continuación, utilizamos el método setLatestEventInfo() para asociar a la notificación la información que aparece al desplegar la bandeja del sistema (título y descripción) e indicar la actividad que debe iniciarse si el usuario pulsa sobre la notificación. Los dos primeros datos son simples cadenas de texto. Para indicar la actividad que se debe ejecutar si el usuario pulsa sobre la notificación, debemos construir una Intención pendiente PendingIntent, que ya hemos usado en el apartado anterior de esta Unidad. Esta Intención pendiente contiene la información de la actividad asociada a la notificación que será lanzada al pulsar sobre ella. Para esto, definimos un objeto Intent indicando la clase de la actividad concreta que se debe ejecutar. En este ejemplo el objeto es la propia actividad principal (NotificacionesActivity.class). Este Intent lo utilizamos para construir el PendingIntent final mediante el método PendingIntent.getActivity(). Veamos cómo queda esta última parte del código, comentado:
Intent notIntent = new Intent(contexto, NotificacionesActivity.class); // Usamos una PendingIntent para crear la notificación PendingIntent contIntent = PendingIntent.getActivity( contexto, 0, notIntent, 0); // Incluimos la información de la notificación notificacion.setLatestEventInfo(contexto, titulo, descripcion, contIntent);
380
Content Providers, servicios y notificaciones
Es posible indicar opciones adicionales, como, por ejemplo, que la notificación desaparezca automáticamente de la bandeja del sistema cuando se pulsa sobre ella. Esto lo conseguimos
usando
al
atributo
flags
de
la
notificación
con
el
valor
Notification.FLAG_AUTO_CANCEL. También podríamos indicar que, al crearse la notificación, el dispositivo suene, vibre o se encienda el LED de estado presente en muchos dispositivos. Para ello, basta con añadir al atributo defaults de la notificación los valores DEFAULT_SOUND, DEFAULT_VIBRATE o DEFAULT_LIGHTS.
//AutoCancel: cuando se pulsa la notificación desaparece notificacion.flags |= Notification.FLAG_AUTO_CANCEL;
//Para añadir sonido, vibración y luces hay que descomentar estas sentencias //notif.defaults |= Notification.DEFAULT_SOUND; //notif.defaults |= Notification.DEFAULT_VIBRATE; //notif.defaults |= Notification.DEFAULT_LIGHTS;
Existen otras muchas opciones y personalizaciones de estos atributos flags y defaults que se pueden consultar en la documentación oficial de la clase Notification de Android. Para acabar, una vez tenemos definidas las opciones de la notificación, podemos generarla invocando el método notify() y pasando como parámetro un identificador único definido por la aplicación, así como el objeto Notification construido anteriormente.
//Enviamos la notificación notManager.notify(ID_MEN_BARRA_NOTIF, notificacion);
Si volvemos a ejecutar la aplicación y pulsamos de nuevo el botón “Notificación en la barra de estado”, veremos que aparece un icono en la barra de estado del dispositivo virtual:
381
Si desplegamos la bandeja del sistema, podemos verificar el resto de información de la notificación:
Por último, si pulsamos sobre la notificación, se abre automáticamente de nuevo la aplicación de este ejemplo. Además, la notificación desaparece de la bandeja del sistema, ya que lo habíamos configurado en el código Java con la opción FLAG_AUTO_CANCEL:
382
Content Providers, servicios y notificaciones
Desde Eclipse puedes abrir el proyecto Ejemplo 5 (Notificaciones) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos lanzado una notificación a la barra de estado del dispositivo.
7.6
USO DE VIEWPAGER EN APLICACIONES ANDROID
Si has utilizado alguna vez un dispositivo Android, te habrás dado cuenta de que algunas aplicaciones permiten desplazar páginas deslizando el dedo horizontalmente sobre la pantalla. Por ejemplo, en la aplicación del Android Market y en el visor de imágenes podemos cambiar de página dentro de la misma aplicación:
383
Al desplazar el dedo cambia de pantalla.
se
Para desarrollar esta funcionalidad hay que emplear el componente ViewPager de Android que está heredado de la clase ViewGroup. Este componente no forma parte de las clases por defecto del SDK de Android. Está incluido en el paquete externo de Compatibilidad de Android que deberías haber añadido al instalar el SDK de Android en Eclipse. Para comprobar que está bien añadido, haz clic en el botón "Opens the Android SDK Manager" de Eclipse:
Debe aparecer el siguiente paquete como instalado ("Installed"):
Nota: el número de revisión puede ser mayor que 4. Puedes encontrar estas librerías en el directorio 384
Content Providers, servicios y notificaciones
C:\cursos_Mentor\Android\android-sdk-windows\extras\android\support\v4
7.6.1 Cómo se usa el componente ViewPager A continuación, vamos a mostrar en el Ejemplo 6 de esta Unidad cómo utilizar el componente ViewPager en una aplicación Android. Una vez que hemos comprobado que tenemos las librerías extra de compatibilidad de Android, procedemos a incluirlas en el proyecto. En este proyecto hemos creado la carpeta "libs" y copiado dentro el archivo androidsupport-v4.jar del directorio donde se encuentre la librería:
A continuación, añadimos la librería al Build Path haciendo clic con el botón derecho del ratón sobre el archivo de la librería y eligiendo la opción "Build Path->Add to Build Path" del menú desplegable:
385
Para comprobar que hemos incluido la librería correctamente en Eclipse, debe aparecer como Librería referenciada ("Referenced Libraries"):
La aplicación que vamos a desarrollar consta de una Actividad que muestra un visor sencillo de imágenes dentro del ViewPager. Para generar las páginas contenidas en este ViewPager es necesario usar un objeto PagerAdapter, que se encarga de alimentar de páginas al componente ViewPager. Veamos las sentencias comentadas para crear la Actividad principal de la aplicación:
public class ViewPagerActivity extends Activity { // Define el nº de páginas en el ViewPager private static int NUMERO_VIEWS = 10; // Variable de ViewPager private ViewPager vPager; // Adaptador del ViewPager private CustomPagerAdapter vPagerAdapter;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Buscamos el ViewPager en el diseño main.xml vPager = (ViewPager) findViewById(R.id.vPager); // Creamos el adaptador de N Páginas y pasamos el contexto de la aplicación vPagerAdapter = new CustomPagerAdapter(NUMERO_VIEWS, this); // Asignamos el adaptador al ViewPager vPager.setAdapter(vPagerAdapter);
386
Content Providers, servicios y notificaciones
// Definimos el evento de cambio de página en el ViewPager vPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageSelected(int position) { Toast.makeText(getBaseContext(), "Has cambiado a la pantalla " + (position+1), 1).show(); }
@Override public void onPageScrollStateChanged(int arg0) { // No definimos nada en el evento al hacer scroll en la página }
@Override public void onPageScrolled(int arg0, float arg1, int arg2) { // No definimos nada en el evento al hacer scroll en la página } }); // end setOnPageChangeListener } }
En el código anterior no hay nada especial que resaltar. Buscamos en el archivo de diseño el ViewPager y le asignamos su adaptador con el método setAdapter(). Además, usamos el método setOnPageChangeListener() para mostrar un mensaje Toast cada vez que el usuario cambie de página. El archivo de diseño Layout de la actividad principal se implementa así:
387
android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/vPager"/>
Como se trata de una Vista que se define en un paquete extra de Android, es necesario incluir el nombre completo del mismo android.support.v4.view.ViewPager. Luego, creamos el adaptador personalizado a partir de la clase PagerAdapter, para que cree las páginas internas del ViewPager, devolviendo vistas según vamos desplazando el dedo horizontalmente por la pantalla:
public class CustomPagerAdapter extends PagerAdapter{ // Variables donde guardamos el contexto y el número de páginas private Context contexto; private int nViews; // Constructor de la clase public CustomPagerAdapter(int nViews, Context contexto) { this.contexto=contexto; this.nViews=nViews; } @Override // Devuelve el nº de página del Adaptador del ViewPager public int getCount() { return nViews; }
/** *
Crea la página de la position indicada. El adaptador
*
es el responsable de añadir componentes a cada página.
* * @param collection La Vista (View) donde se almacena la página. * @param position Número de página que debemos crear. * @return Devuelve el objeto que representa la página.
388
No tiene por qué
Content Providers, servicios y notificaciones
* ser una Vista, puede contener a su vez otras páginas. */ @Override public Object instantiateItem(View collection, int position) { /* Creamos mediante sentencias Java el diseño de la página. * También podríamos haber guardado el diseño en un archivo * xml y haberlo inflado aquí. */ // Creamos el Layout donde añadimos el resto de Vistas LinearLayout linearLayout = new LinearLayout(contexto); //Orientacion vertical = 1 linearLayout.setOrientation(1); // Definimos una etiqueta de texto TextView tv = new TextView(contexto); tv.setText("Imagen número " + (position+1)); tv.setTextColor(Color.WHITE); tv.setTextSize(30); // Definimos una imagen ImageView imagen = new ImageView(contexto); // Buscamos la imagen en el directorio /res/drawable en función del nº de página int resID = contexto.getResources().getIdentifier("imagen"+ (position+1), "drawable", "es.mentor.unidad7.eje6.viewpager"); // Asignamos la imagen cargada del recurso imagen.setImageResource(resID); // Definimos unos parámetros para alinear la etiwueta superior LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.setMargins(0, 0, 0, 20); params.gravity=Gravity.CENTER; // Añadimos la etiqueta superior al Layout con los parámetros anteriores linearLayout.addView(tv, params); // Añadimos la imagen al Layout linearLayout.addView(imagen);
389
// Añadimos la página a la colección de páginas ((ViewPager) collection).addView(linearLayout,0); // Devolvemos el diseño de la página return linearLayout; } // end instantiateItem
/** *
Destruye el contenido de la página indicada en position. El adaptador
*
es el responsable de borrar los componentes de cada página.
* * @param collection La Vista (View) donde se elimina la página. * @param position Número de página que debemos eliminar. * @return object El mismo objeto creado en {@link #instantiateItem(View, int)}. */ @Override public void destroyItem(View collection, int position, Object view) { ((ViewPager) collection).removeView((LinearLayout) view); }
/** *
Compara si la Vista view está instanciada en el Objeto object. Método necesario para la clase ViewPager
*/ @Override public boolean isViewFromObject(View view, Object object) { return view==((LinearLayout)object); }
/** * Android invoca este método cuando el cambio de una de las páginas se ha completado. */ @Override
390
Content Providers, servicios y notificaciones
public void finishUpdate(View arg0) {}
/** * Método que se invoca cuando Android indica que hay que recuperar el estado de ejecución */ @Override public void restoreState(Parcelable arg0, ClassLoader arg1) {}
/** * Método que se invoca cuando Android indica que hay que guardar el estado de ejecución */ @Override public Parcelable saveState() { return null; }
/** * Android invoca este método cuando se inicia el cambio de una de las páginas. */ @Override public void startUpdate(View arg0) {} }
Los métodos más importantes del código anterior son: •
instantiateItem: crea la página para la posición indicada como parámetro del método. Este adaptador es el responsable de añadir las Vistas a cada página. Creamos el diseño de la Vistas contenidas en la página mediante sentencias Java. También podríamos haber guardado el diseño en un archivo xml y haberlo inflado.
•
destroyItem: destruye la página indicada en el parámetro posición.
Las imágenes que se cargan en el visor de imágenes están almacenadas en el directorio /res/drawable del proyecto. Para cargarlas dinámicamente en función del número de página que el adaptador CustomPagerAdapter debe crear hemos obtenido los recursos del 391
contexto con la orden contexto.getResources(); después, hemos buscado el ID del recurso de la
imagen
usando
el
método
getIdentifier(nombre_recurso,
tipo_recurso,
paquete_recurso). Desde Eclipse puedes abrir el proyecto Ejemplo 6 (ViewPager) de la Unidad 7. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos usado un ViewPager.
Si ejecutamos la aplicación y arrastramos el ratón horizontalmente sobre la pantalla del emulador simulando el efecto de un dedo (puede costar un poco hacerlo con el ratón), veremos las siguientes ventanas:
Arrastrar
Cambio de página
392
Arrastrar
Cambio de página
Content Providers, servicios y notificaciones
Un Proveedor de contenido (en inglés Content Provider) es el mecanismo proporcionado por Android para compartir información entre aplicaciones. Los Proveedores de contenidos los usan muchas aplicaciones estándar de un dispositivo Android, como, por ejemplo, la lista de contactos, la aplicación de SMS mensajes cortos, el calendario, etcétera. Para implementar un Proveedor de contenidos propio, hay que usar la clase ContentProvider de Android. Las tablas de la base de datos SQLite usadas por un Content Provider deben incluir siempre el campo _ID que identifica sus registros de forma unívoca. El acceso a un Content Provider se realiza siempre mediante un identificador URI, que es una cadena de texto parecida a una dirección Web de Internet. La clase ContentResolver de Android permite realizar acciones con cualquier Content Provider que esté disponible en el sistema operativo Android. Un Servicio (en inglés Service) es un componente de una aplicación Android que se ejecuta en segundo plano, sin interactuar con el usuario (no tiene interfaz de usuario), para realizar operaciones de larga duración. La plataforma Android ofrece una gran cantidad de servicios predefinidos en el sistema, a los que podemos acceder a través de la clase de tipo Manager. Un servicio puede funcionar de dos modos: •
Autónomo: el servicio se puede ejecutar en segundo plano de forma indefinida, incluso si el componente que lo inició se destruye.
•
Dependiente o Ligado (bind): ofrece una interfaz de tipo clienteservidor que permite a los componentes de una aplicación interactuar con él enviando peticiones y recibiendo su resultado. Un servicio ligado sólo se ejecuta mientras otro componente de la aplicación está unido a él.
393
Los servicios propios de una aplicación se ejecutan en el hilo principal de su proceso; por lo tanto, para no bloquear el hilo principal o de interfaz, debemos ejecutar estos servicios con hilos de ejecución. Para implementar un servicio propio en una aplicación tenemos que extender la clase Service de Android. Se pueden usar mensajes de difusión (Broadcast) para comunicar eventos entre servicios. Estos mensajes son, en realidad, Intents. La clase Receptor de mensajes de difusión (BroadcastReceiver) se usa para recibir Intenciones (Intents), es decir, mensajes enviados por otro componente de Android. En Android existen varias formas de notificar mensajes o información al usuario. •
Diálogos: muestran o solicitan información al usuario.
•
Mensajes emergentes (en inglés Toast).
•
Mensajes de notificación en la barra de estado del dispositivo.
Un mensaje emergente (en inglés Toast) es un mensaje que se muestra en la pantalla
del
dispositivo
Android
durante
unos
segundos
y
desaparece
automáticamente sin requerir ningún tipo de actuación por parte del usuario. Los mensajes de notificación de la barra de estado de Android se muestran en la barra de estado de los dispositivos Android cuando recibimos un mensaje SMS, hay actualizaciones disponibles, está el reproductor de música funcionando, etcétera. Para generar notificaciones en la barra de estado del sistema, hay que obtener una referencia
al
servicio
de notificaciones
de
Android
usando
la
clase
NotificationManager. El componente ViewPager permite diseñar aplicaciones que incluyen páginas que se pueden desplazar deslizando el dedo horizontalmente sobre la pantalla. Este componente ViewPager no forma parte de las clases por defecto del SDK de Android. Está incluido en el paquete externo de Compatibilidad de Android. 394
ANDROID AVANZADO
ÍNDICE 8.1 INTRODUCCIÓN ................................................................................... 397 8.2 CÓMO DEPURAR APLICACIONES ANDROID CON ECLIPSE ..... 397 8.2.1 Estableciendo Puntos de interrupción (Breakpoints) ............... 399 8.2.2 Iniciar la depuración (Debug) del código .................................... 400 8.2.3 Datos de depuración (Debug) del código................................... 401 8.2.4 Desactivar la depuración de código ............................................ 403 8.2.5 Propiedades de los puntos de interrupción ............................... 404 8.2.6 Puntos de interrupción de excepciones ..................................... 405 8.2.7 Puntos de interrupción de método .............................................. 405 8.2.8 Puntos de interrupción de clase (class) ..................................... 405 8.2.9 Finalizar la Depuración del código .............................................. 406 8.3 USO DE MAPAS EN APLICACIONES ANDROID ............................ 406 8.3.1 Preparación del Entorno de programación ................................ 407 8.3.2 Cómo incluir mapas en las aplicaciones Android ..................... 410 8.4 DESARROLLO DE APLICACIONES SENSIBLES A LA ORIENTACIÓN DEL DISPOSITIVO .................................................... 418 8.4.1 Cambio de orientación automática.............................................. 420 8.4.2 Mantener la información del estado durante el cambio de orientación ...................................................................................... 424 8.4.3 Cambio de orientación Manual ...................................................... 427 8.5 DESPLEGAR APLICACIONES ANDROID EN DISPOSITIVOS VIRTUALES (AVD) O REALES ........................................................... 431 8.6 CÓMO PUBLICAR APLICACIONES EN EL ANDROID MARKET .. 435 8.6.1 Alta de cuenta de desarrollador en el Android Market ............. 435 8.6.2 Recomendaciones sobre aplicaciones para Android Market .. 439 8.6.2.1 Recomendaciones sobre aplicaciones para Android Market .................................................................................... 439
8.6.2.2 Buenas prácticas para el desarrollo de aplicaciones Android .................................................................................. 440 8.6.3 Generar fichero APK con certificado para Android Market ..... 441 8.6.4 Publicar una aplicación Android en el Android Market............. 445
2
Android Avanzado
8.1
INTRODUCCIÓN
En esta Unidad vamos a explicar cómo depurar (debug en inglés) aplicaciones Android con Eclipse. Después, veremos cómo utilizar Mapas en aplicaciones Android mediante la API de Google. Asimismo, veremos cómo cambiar el aspecto de las aplicaciones Android cuando cambia la orientación del dispositivo. Finalmente, conoceremos cómo desplegar aplicaciones en un dispositivo real Android y publicar una aplicación en el Android Market.
8.2
CÓMO DEPURAR APLICACIONES ANDROID CON ECLIPSE
La Depuración de programas es el proceso de identificar y corregir errores de programación en tiempo de ejecución. En inglés se denomina debugging, ya que se asemeja a la eliminación de bichos (bugs), que es como se denominan informalmente los errores de programación Para depurar (en inglés Debug) una aplicación Andriod, vamos a emplear las capacidades disponibles en el entorno de desarrollo Eclipse. Para ello, nos serviremos de la última versión disponible, la 3.7, a fecha de edición de este documento. Para que el alumno o alumna pueda practicar la Depuración de código Android con Eclipse, hemos creado un proyecto Android con las siguientes clases:
public class DepuracionActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main);
// Usamos la clase que ya existe en el otro fichero Contador contador = new Contador(); contador.count(); System.out.println("Hemos contado " + contador.getResultado() + " veces.");
397
Object o = null; o.toString(); } } // Clase sencilla que implementa un contador public class Contador {
// Variable para guardar la cuenta actual private int resultado=0;
public int getResultado() { return resultado; }
// Método que cuenta de 2 en 2 public void count() { for (int i = 0; i < 100; i++) { resultado += i++; } } }
Es recomendable abrir en Eclipse el Ejemplo 1 (Depuración) de la Unidad 8 y practicar los comandos que se muestran a continuación.
Si ejecutas la aplicación tal y como está, verás que aparece el siguiente mensaje de error:
398
Android Avanzado
Si haces clic en el botón "Force close", la aplicación termina. Veamos cómo depurar este programa que provoca un error.
8.2.1
Estableciendo Puntos de interrupción (Breakpoints) En el desarrollo de software, un punto de interrupción (Breakpoint en inglés) es una
marca en el código fuente que indica al depurador del lenguaje en que estemos programando que debe detener o pausar la ejecución del programa para poder evaluar los valores asignados a las variables y permitir al programador detectar errores en tiempo de ejecución. Para establecer puntos de interrupción con Eclipse, hay que hacer clic en la opción "Toggle Breakpoint" del menú desplegable que aparece si pulsamos el botón derecho del ratón sobre el número de línea del código fuente correspondiente. También podemos hacer doble clic en este número de línea para activar o desactivar esta opción:
399
8.2.2
Iniciar la depuración (Debug) del código Para iniciar la depuración del código hay que hacer clic en la opción "Run->Debug" del
menú principal de Eclipse. También podemos usar la tecla rápida [F11] o usar el icono
del
menú principal. Si lo hacemos, a continuación se instalará y se ejecutará la aplicación en el dispositivo virtual. Después, Eclipse muestra el siguiente mensaje:
Contestaremos que sí para cambiar el tipo de Perspectiva a "Debug", muy útil para depurar programas. A continuación, cambiará la perspectiva de Eclipse así:
Y la ejecución del programa se parará en la primera línea del código que tenga un punto de interrupción. Podemos usar los siguientes atajos de teclado para depurar el programa:
400
Android Avanzado
Comando Descripción
F5
La ejecución pasa a la siguiente sentencia del programa. Si la sentencia siguiente es la llamada a un método o función, se continuará con la ejecución de las sentencias de este método o función.
F6
La ejecución pasa a la siguiente sentencia del programa. Si la sentencia siguiente es la llamada a un método o función, se continuará con la ejecución de la sentencia siguiente sin entrar en el código de este método o función.
F7
La ejecución sigue todas las sentencias de todos los métodos o funciones que formen nuestro programa. Es decir, ejecuta en secuencia todas las órdenes que conforman el programa.
F8
El programa se ejecuta hasta que se encuentre otro punto de interrupción o hasta que el usuario lo cierre.
Nota: también existen unos botones de acceso rápido que permiten ejecutar estas órdenes. Observa la imagen siguiente:
8.2.3
Datos de depuración (Debug) del código La vista "Debug" permite ver el contenido de la Pila "Stack" de la aplicación:
En la parte superior derecha de Eclipse podemos ver el contenido de las variables. También podemos usar el menú para cambiar el tipo de variables que han de visualizarse, opción muy útil cuando hemos definido muchas variables:
401
Es posible también usar este menú para cambiar las columnas que han de aparecer en esta vista:
Además, es posible utilizar la opción "New Detail Formater" (menú desplegable con el botón derecho del ratón) para modificar la información mostrada sobre la variable. Por ejemplo, como
el
texto
(posición
de
memoria
de
una
variable)
es.mentor.unidad8.eje1.depuracion.Contador@4051b760 no dice nada, podemos usar la opción "New Detail Formater"
402
Android Avanzado
para invocar un método de una clase y mostrar su resultado:
Ahora ya podemos ver el resultado:
8.2.4
Desactivar la depuración de código Si deseas desactivar temporalmente todos los puntos de interrupción, puedes pulsar el
botón "Skip All Breakpoints":
Si pulsas este botón otra vez, los puntos de interrupción se activarán de nuevo.
403
8.2.5
Propiedades de los puntos de interrupción Después de establecer un punto de interrupción, puedes seleccionar las propiedades
de este punto para, por ejemplo, establecer una condición lógica de parada. En las propiedades se puede, por ejemplo, activar el punto de interrupción y parar la ejecución del programa sólo cuando una variable tenga cierto valor o se cumpla cierta condición. Para acceder a las propiedades del punto de interrupción, hay que hacer clic en la opción "Breakpoint Properties..." del menú desplegable con el botón derecho del ratón sobre el punto de interrupción:
En la ventana emergente podemos establecer la condición de parada del punto de interrupción:
404
Android Avanzado
8.2.6
Puntos de interrupción de excepciones Los puntos de interrupción de excepciones detienen la ejecución de la aplicación si se
inicia una excepción específica. Para definir este tipo de punto de interrupción, hay que hacer clic en el icono de excepción siguiente:
8.2.7
Puntos de interrupción de método Un punto de interrupción de tipo método se define haciendo doble clic en el borde
izquierdo del editor del método correspondiente. Detiene el programa durante al ejecutar el método o, después, al finalizar la ejecución del mismo.
8.2.8
Puntos de interrupción de clase (class) Un punto de interrupción de tipo clase se define haciendo doble clic en el borde
izquierdo del editor de la declaración de la clase correspondiente. Detiene el programa al cargar esta clase Java:
405
8.2.9
Finalizar la Depuración del código Para finalizar la depuración del código basta con cambiar la Perspectiva a "Java" de
nuevo. Cuando hagamos alguna modificación del código fuente, aparecerá el siguiente mensaje para indicar que no se puede sustituir el código de una aplicación ya instalada en el emulador de Android y se pregunta si deseamos desconectar ("Disconnect") el modo Debug:
Nota: en esta Unidad 8 puedes encontrar el vídeo “Cómo depurar aplicaciones Android en Eclipse”, que muestra visualmente cómo llevar a cabo la depuración del Ejemplo 1 de esta Unidad.
8.3
USO DE MAPAS EN APLICACIONES ANDROID
En este apartado vamos utilizar mapas en aplicaciones de Android haciendo uso de la API Android de Google Maps. La mayoría de los dispositivos Android permiten determinar su ubicación geográfica actual a través de un módulo GPS (del inglés Global Positioning System, que se traduce como Sistema de Posicionamiento Global). Android dispone del paquete android.location, que proporciona la API para determinar la posición actual geográfica.
406
Android Avanzado
8.3.1
Preparación del Entorno de programación Antes de empezar a utilizar el servicio de mapas de Google es necesario comprobar
que tenemos instalado el paquete correspondiente a las APIs de Google. Este paquete se llama normalmente “Google APIs by Google, Android API x, revisión y“. Al instalar el SDK de Android en Eclipse deberías haber añadido ya este paquete. Para comprobar que está correctamente instalado, haz clic en el botón "Opens the Android SDK Manager" de Eclipse:
Debe aparecer el siguiente paquete como instalado ("Installed"):
Nota: el número de revisión puede ser mayor que 2. Para poder probar las aplicaciones en el emulador, también es necesario crear un nuevo dispositivo virtual AVD que utilice este paquete como "target". Para ello, pulsamos el botón "Opens the Android Virtual Device Manager":
Y se presenta una ventana, donde pulsamos sobre el botón "New". A continuación, aparece otra nueva ventana, donde rellenamos los campos tal y como aparecen en esta captura:
407
Para acabar de crear el dispositivo virtual, hacemos clic en el botón "Create AVD". Para poder utilizar la API de Google Maps es necesario obtener previamente una clave de uso (API Key) que estará asociada al certificado con el que firmamos digitalmente las aplicaciones. En el apartado "Permisos y Seguridad" de la Unidad 5 ya hemos hablado de estos certificados, necesarios para firmar aplicaciones. Si cambiamos el certificado con el que firmamos nuestra aplicación, algo que normalmente se hace como paso previo a la publicación de la aplicación en el Android Market, tendremos que modificar también la clave de uso de la API. Cuando compilamos una aplicación en Eclipse y la probamos en el emulador de Android, se aplica automáticamente un certificado de depuración creado por defecto. Por lo tanto, para poder depurar en el emulador aplicaciones que hagan uso de Google Maps, hay que solicitar una clave asociada a este certificado de depuración. En primer lugar, hay que localizar el fichero donde se almacenan los datos del certificado de depuración "debug.keystore". Podemos conocer la ruta de este fichero accediendo a las preferencias de Eclipse, sección "Android", apartado "Build":
408
Android Avanzado
En esta ventana copiamos en el portapapeles la ruta que aparece en el campo “Default Debug Keystore“. Observa que hemos borrado intencionalmente la parte de la ruta que será distinta en tu ordenador. Una vez conocemos la ruta del fichero debug.keystore, vamos a acceder a él con la herramienta keytool.exe de Java para obtener el hash MD5 del certificado. Esto lo hacemos desde
una
ventana
de
línea
de
comandos
en
el
directorio
C:\Program
Files
(x86)\Java\jre6\bin (o donde esté instalado Java) mediante la orden:
C:\Program Files (x86)\Java\jre6\bin>keytool -list -alias androiddebugkey "ruta_del_certificado\debug.keystore" -storepass android -keypass android
-keystore
Nota: es necesario usar la versión 6 de Java, pues en la 7 no funciona. Si lo hacemos, veremos la siguiente ventana:
409
A continuación, copiamos en el portapapeles el dato que aparece identificado como “Huella digital de certificado (MD5)”. Después, accedemos a la Web de Google para solicitar una clave de utilización de la API de Google Maps para depurar aplicaciones. En esta Web tendremos que escribir la Huella digital MD5 de nuestro certificado para obtener la clave de uso de la API. En la siguiente imagen se muestra el resultado:
Nota: Observa que hemos borrado intencionalmente parte de la clave, pues, cuando solicites ésta, te darán otra diferente. Ya hemos terminado la preparación del entorno de programación para poder utilizar los servicios de Google Maps dentro de nuestras aplicaciones Android.
8.3.2
Cómo incluir mapas en las aplicaciones Android En el Ejemplo 2 de esta Unidad vamos a desarrollar una aplicación que incluye un
mapa sobre el que podemos hacer unas operaciones sencillas, como cambiar a vista satélite o desplazar el mapa. Para poder ver este proyecto en tu emulador Android es necesario que obtengas la clave de uso de la API de Mapas de Google y la cambies en el fichero de diseño main.xml de la interfaz de usuario. Si no lo haces, arrancará la aplicación del ejemplo pero no se mostrará el mapa, como en la imagen siguiente:
410
Android Avanzado
Hay que tener en cuenta que, a la hora de crear el proyecto Android en Eclipse, tenemos que seleccionar "Google APIs" en el campo "Build Target" en las propiedades del proyecto:
Para incluir un mapa de Google Maps en una aplicación Android, utilizamos el componente MapView. Este componente se puede añadir al diseño de la pantalla como otro componente normal. Sin embargo, para poder usarlo, hay que indicar la clave de uso de Google Maps en el atributo android:apiKey tal y como se muestra a continuación: 411
Además, también hemos establecido el atributo clickable a true, para que el usuario pueda interactuar con el componente si quiere, por ejemplo, desplazar el mapa con el dedo. Los componentes MapView sólo se pueden utilizar desde una actividad de tipo MapActivity. La clase MapActivity se extiende de la clase Activity y permite la gestión del ciclo de vida de la Actividad y de los servicios de visualización de un mapas. De igual forma que ListActivity se usa para mostrar listas, MapActivity se usa para mostrar mapas. En el Ejemplo 2 la Actividad principal hereda la clase MapActivity, tal y como vemos en el siguiente código: public class MapasActivity extends MapActivity
{
// Variables donde se definen los controles de la Actividad private MapView mapa = null; private Button sateliteBtn = null; private Button irBtn = null; private Button animarBtn = null; private Button moverBtn = null; private MapController controlMapa = null; // Constantes que llevan a un punto en el mapa private static Double latitud = 40.6550*1E6; private static Double longitud = -4.7000*1E6;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //Obtenemos una referencia a las Vistas de la Actividad mapa = (MapView)findViewById(R.id.mapa);
412
Android Avanzado
sateliteBtn = (Button)findViewById(R.id.SateliteBtn); irBtn = (Button)findViewById(R.id.IrBtn); animarBtn = (Button)findViewById(R.id.AnimarBtn); moverBtn = (Button)findViewById(R.id.MoverBtn); //Definimos el Controlador del mapa controlMapa = mapa.getController(); // Definimos un nuevo punto de localización GeoPoint loc = new GeoPoint(latitud.intValue(), longitud.intValue()); // Centramos el mapa en este punto controlMapa.setCenter(loc); // Hacemos zoon a 6 (puede tomar el valor de 1 a 21) controlMapa.setZoom(6); //Mostramos los controles de zoom sobre el mapa mapa.setBuiltInZoomControls(true);
// Definimos el evento onClick del botón Satélite sateliteBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // Intercambiamos la capa de tipo satélite en el mapa if(mapa.isSatellite()) mapa.setSatellite(false); else mapa.setSatellite(true); } });
// Definimos el evento onClick del botón Ir a... irBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) {
// Definimos un nuevo punto de localización
413
GeoPoint
loc
=
new
GeoPoint(latitud.intValue(), longitud.intValue());
// Centramos el mapa en este punto controlMapa.setCenter(loc); // Hacemos zoon a 10 (puede tomar el valor de 1 a 21) controlMapa.setZoom(10); } }); // Definimos el evento onClick del botón Animar animarBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // Definimos un nuevo punto de localización GeoPoint
loc
=
new
GeoPoint(latitud.intValue(), longitud.intValue());
// Movemos con animación el mapa en este punto controlMapa.animateTo(loc); // Hacemos zoom sobre esa posición del mapa int zoomActual = mapa.getZoomLevel(); for(int i=zoomActual; i<12; i++) { controlMapa.zoomIn(); } } }); // Definimos el evento onClick del botón Mover moverBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { // Movemos el mapa 1000 píxeles en horizontal y 50 en vertical controlMapa.scrollBy(1000, 50); } });
414
Android Avanzado
}
// Método obligado de la clase que indica si estamos mostrando una ruta @Override protected boolean isRouteDisplayed() { return false; } }
A continuación, vamos a explicar las partes pertinentes del código anterior. Como la Actividad principal se hereda de la clase MapActivity, es obligatorio implementar el método isRouteDisplayed(), que debe devolver el valor true si vamos a mostrar algún tipo de información de ruta sobre el mapa. Según los términos de licencia de uso de la API de Google Maps, debe indicarse cuándo se usan sus mapas para este propósito. En este ejemplo del curso nos limitamos a mostrar un mapa en la pantalla principal de la aplicación, por lo que devolvemos el valor false. Además, en el método onCreate() de la Actividad se invoca el método setBuiltInZoomControls() de la referencia de componente MapView para mostrar los controles de zoom estándar sobre el mapa, de modo que podamos acercar y alejar la vista del mapa. Por defecto, cuando usamos un MapView en una aplicación, se muestra en el modo de mapa tradicional. Sin embargo, este componente también permite cambiar las capas a la vista satélite, ver fotos de la calle con StreetView o mostrar la información del tráfico. Para ello, podemos usar los siguientes métodos de la clase MapView: •
setSatellite(true)
•
setStreetView(true)
•
setTraffic(true)
También existen otros tres métodos para consultar el estado de cada uno de estos modos: isSatellite(), isStreetView() y isTraffic(). En el evento onClick del botón sateliteBtn hemos usado el método setSatellite() para intercambiar el modo satélite y el estándar. Además de los métodos para personalizar el aspecto gráfico del mapa, también disponemos de varios métodos para consultar la información geográfica visualizada en el mismo. Por ejemplo, podemos saber las coordenadas geográficas en las que el mapa está centrado actualmente
mediante el método getMapCenter() y el nivel de zoom que está
aplicando a través del método getZoomLevel(). 415
Como podemos observar en el código anterior, las coordenadas del centro del mapa se obtienen mediante el método getMapCenter() en forma de objeto GeoPoint que encapsula los valores de latitud y longitud expresados en microgrados (grados * 1E6). Los valores en la magnitud
grados
se
pueden
obtener
mediante
los
métodos
getLatitudeE6()
y
getLongitudeE6() respectivamente. El nivel de zoom del mapa contiene un valor entero entre 1 y 21, siendo 21 el que ofrece mayor nivel de detalle en el mapa. Para modificar el centro del mapa, en primer lugar, debemos acceder al controlador del mapa (MapController) mediante el método getController(). Este método devuelve un objeto MapController con el que podemos modificar la posición central del mapa. Para ello, podemos usar los métodos setCenter() y setZoom() a los que podemos indicar las coordenadas centrales del mapa y el nivel de zoom deseado, respectivamente. En este ejemplo hemos incluido un botón irBtn que centra el mapa sobre un punto determinado y hemos aplicado un nivel de zoom (10), que permite distinguir en el mapa algunos detalle. Si pruebas el ejemplo del curso, verás que el desplazamiento a la posición y el zoom al nivel indicados se hacen de forma instantánea sin ningún tipo de animación. Para mejorar la sensación de movimiento en el mapa, la API de Google nos ofrece otra serie de métodos que permiten desplazar el mapa a una posición específica de forma progresiva y aumentar o disminuir el nivel de zoom de forma “animada”. El método animateTo(GeoPoint) desplaza el mapa hasta un punto determinado y los métodos zoomIn() y zoomOut() aumentan o disminuyen de forma progresiva, respectivamente, en 1 el nivel de zoom. En el botón animarBtn hemos usado este método para desplazar de forma animada el mapa. Para acabar, disponemos de otro método que permite desplazar el mapa un determinado número de pixeles en cierta dirección, tal y como puede hacer un usuario con el dedo sobre el mapa. Este método se llama scrollBy() y recibe como parámetros el número de pixeles que queremos desplazarnos en horizontal y en vertical. En el botón moverBtn hemos usado este método para desplazar el mapa automáticamente. Finalmente, ten en cuenta que, para ejecutar la aplicación del ejemplo sobre el emulador de Android, hay que modificar el fichero AndroidManifest.xml. Es necesario especificar que hacemos uso de la API de Google Maps mediante la cláusula y, en segundo lugar, hay que solicitar los permisos de acceso a Internet mediante la cláusula . Veamos el aspecto que tiene este fichero:
416
Android Avanzado
android:versionName="1.0" >
Desde Eclipse puedes abrir el proyecto Ejemplo 2 (Mapas) de la Unidad 8. Estudia el código fuente y ejecútalo para mostrar en el AVD el resultado del programa anterior, en el que hemos utilizado un mapa.
Si ejecutas la aplicación en el emulador de Android, verás que tiene el siguiente aspecto:
417
8.4
DESARROLLO DE APLICACIONES SENSIBLES A LA ORIENTACIÓN DEL DISPOSITIVO
Si has usado alguna vez un teléfono con Android, verás que, al cambiar la orientación del mismo de vertical a horizontal y viceversa, normalmente se modifica el aspecto de la aplicación que estás usando distribuyéndose las Vistas de la interfaz de usuario de forma acorde. Aunque a priori este cambio de orientación del dispositivo parece sencillo, a veces los desarrolladores de aplicaciones Android deben desarrollar complejos códigos para controlarlo. Este apartado describe cómo implementar esta funcionalidad. Por ejemplo, si tenemos abierta la aplicación de Contactos de Android y cambiamos la orientación del teléfono de vertical a horizontal, la aplicación modifica el aspecto de la interfaz del usuario proporcionalmente:
418
Android Avanzado
Hay dos formas de controlar el cambio de orientación del dispositivo Android: •
Automática: dejamos a Android que haga todo la tarea y definimos el fichero de diseño xml que debe aplicar para cada tipo de orientación vertical (portrait) u horizontal (landscape).
•
Manual: controlamos con sentencias Java qué diseño debe cargar en cada momento.
ATENCIÓN Para cambiar la orientación del emulador de Android [BLOQUE_NUM_7], [Ctrl+F11], [BLOQUE_NUM_9], [Ctrl+F12].
podemos
usar
las
teclas
Ten en cuenta que el cambio de orientación puede tardar unos segundos en el emulador dependiendo de la capacidad del PC con el que trabajes.
En el Ejemplo 3 de esta Unidad vamos a mostrar cómo funcionan las dos formas de controlar el cambio de orientación del dispositivo Android.
Nota sobre Android 2.3.3 Hasta ahora en el curso hemos usado la versión 2.3.3 de Android en el emulador de dispositivos. Cuando se ha escrito este texto, esta versión tiene un Bug al cambiar la orientación del emulador de horizontal a vertical (no informa al emulador de la nueva orientación y mantiene la horizontal).
419
Por lo tanto, hay que probar el Ejemplo 3 de esta Unidad en otra versión de Android. Tal y como hemos hecho para la versión 2.3.3 en la Instalación del curso, hay que descargar las librerías de Android 2.2 y crear el dispositivo virtual correspondiente:
También puedes usar la versión de Android del curso teniendo en cuenta que puedes cambiar al modo horizontal, pero no volver de nuevo al vertical.
8.4.1
Cambio de orientación automática Se trata de una forma muy fácil de personalizar la interfaz de usuario en función de la
orientación de la pantalla del dispositivo. Consiste en crear una carpeta de diseño separada (/res/layout) que contenga los archivos XML que determinan la interfaz de usuario en cada tipo de orientación. Para definir el modo horizontal (landscape), hay que crear la carpeta res/layout-land. Esta nueva carpeta contiene también el archivo main.xml:
También se puede aplicar el nombre de extensión -land a la carpeta drawable donde están las imágenes de la aplicación. Por ejemplo, la carpeta res/drawable-land contiene imágenes que se han diseñado teniendo en cuenta el modo horizontal, mientras que los albergados en la carpeta res/drawable están diseñados para el modo vertical:
420
Android Avanzado
El archivo main.xml incluido en la carpeta /res/layout define la interfaz de usuario para el modo vertical del dispositivo, mientras que el archivo main.xml de la carpeta /res/layoutland define la interfaz de usuario en el modo horizontal. A continuación, se muestra el contenido del archivo main.xml de la carpeta /res/layout:
421
Ahora vamos a ver el contenido del archivo main.xml de la carpeta /res/layout-land:
422
Android Avanzado
android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginBottom="10dp" android:layout_marginTop="10dp" android:text="Esta aplicación muestra cómo orientación del dispositivo Android.
controlar
el
cambio
de
\n\nPara cambiar la orientación del emulador de Android puede usar las teclas [BLOQUE_NUM_7], [Ctrl+F11], [BLOQUE_NUM_9], [Ctrl+F12] de tu ordenador" android:textAppearance="?android:attr/textAppearanceMedium" />
423
Si no creas este archivo para el modo horizontal y ejecutas la aplicación, verás que al cambiar al modo horizontal desaparece el componente TextView:
Sin embargo, si creamos el archivo de diseño horizontal, cuando cambiemos la orientación del dispositivo, Android cambiará automáticamente el diseño de la pantalla:
8.4.2
Mantener la información del estado durante el cambio de orientación Si en el ejemplo anterior escribes algo en el TextView y, a continuación, cambias la
orientación del dispositivo virtual, verás que el texto escrito en este componente se mantiene sin añadir nuevo código Java. En el apartado "Guardar y recuperar el estado de una Actividad" de la Unidad 3 hemos estudiado que, cuando cambia la orientación de la pantalla (vertical/horizontal), Android reinicia la Actividad usando el método OnDestroy() e inmediatamente llama de nuevo a onCreate(). Este comportamiento de reinicio está diseñado para que la aplicación se adapte a la nueva configuración de forma automática, y así cambiar la posición de los componentes.
424
Android Avanzado
La mejor manera de manejar un cambio de configuración de este tipo para preservar el estado de la aplicación es usar los métodos onSaveInstanceState() y onCreate(). Lo primero que hay que tener en cuenta es que es imprescindible establecer el atributo android:id de todas las Vistas de la actividad. Este atributo es indispensable para que Android guarde automáticamente el contenido de las Vistas cuando cambia la orientación de la pantalla y se destruye la Actividad. Por ejemplo, si un usuario ha introducido un texto en una Vista de tipo EditText y cambia la orientación del dispositivo, si este EditText tiene asignado un valor al atributo android:id, Android mantendrá el texto existente y lo restaurará de forma automática cuando la actividad se vuelva a recrear. Si, por el contrario, la Vista de tipo EditText no tiene definido el atributo android:id, el sistema no podrá conservar el texto y cuando se recree la actividad, el texto se perderá. Android invoca el método onSaveInstanceState() cuando una Actividad está a punto de ser destruida o va a pasar a un segundo plano. Por ejemplo, cuando se cambia la orientación de la pantalla, se invoca este método para que se pueda guardar el estado actual de la actividad y poder restaurarlo más tarde. Hay otro procedimiento que permite sustituir el evento onSaveInstanceState() para guardar información extra necesaria en la Actividad y restaurarla cuando se recree. Por ejemplo, el siguiente código muestra cómo guardar la orientación actual del dispositivo sin usar el evento onSaveInstanceState():
* Se llama a este evento cuando Android inicia un cambio de orientación. * ¡CUIDADO! Para que el cambio se haga de forma AUTOMÁTICA debemos delegarle * esta funcionalidad. Esto se consigue quitando del archivo * AndroidManifest.xml el atributo android:configChanges="orientation..." * * Si controlamos de forma MANUAL el cambio de orientación, debes comentar * este método. */ @Override public void onSaveInstanceState(Bundle outState) { // Obtenemos la orientación actual del dispositivo String texto=""; // Conectamos con el servicio de ventanas de Android y obtenemos los datos de la pantalla principal Display display = ((WindowManager) getSystemService(WINDOW_SERVICE)).getDefaultDisplay();
425
int orientation = display.getRotation(); if ((orientation==Surface.ROTATION_90) || (orientation==Surface.ROTATION_270)) texto="vertical"; else texto="horizontal";
// Guardamos una información del estado outState.putString("dato", texto); super.onSaveInstanceState(outState); }
Cuando la actividad se vuelve a recrear, Android invoca primero el método OnCreate(), seguido por el método onRestoreInstanceState(). Este último método permite recuperar el estado de ejecución guardado previamente:
* Se llama a este evento cuando Android inicia un cambio de orientación. * ¡CUIDADO! Para que el cambio se haga de forma AUTOMÁTICA debemos delegarle * esta funcionalidad. Esto se consigue quitando del archivo * AndroidManifest.xml el atributo android:configChanges="orientation..." * * Si controlamos de forma MANUAL el cambio de orientación debes comentar * este método. */ @Override public void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // Recuperamos la información del EditText if (savedInstanceState.containsKey("dato")) Toast.makeText(this, "Orientación anterior: " + savedInstanceState.getString("dato"), Toast.LENGTH_SHORT).show(); }
Hemos visto que el método onSaveInstanceState() es útil para guardar la información del estado de ejecución de una Actividad, aunque tiene la limitación de que sólo se puede 426
Android Avanzado
guardar información usando el objeto de tipo Bundle. No permite guardar estructuras de datos más complejas, como objetos. Para estos casos, podemos usar el método onRetainNonConfigurationInstance(). Este método se activa cuando una actividad está a punto de ser destruida debido a un cambio de configuración, como un cambio de orientación de la pantalla. Este método permite guardar una estructura de datos devolviendo un objeto como resultado de su ejecución. Fíjate en el siguiente ejemplo: @Override public Object onRetainNonConfigurationInstance() { // Devolvemos un objeto donde hemos guardado un estado de ejecución return(objeto); }
Fíjate que el método anterior devuelve el tipo objeto (Object), lo que permite prácticamente devolver cualquier tipo de dato. Para extraer los datos guardados se puede usar dentro del método onCreate() el método getLastNonConfigurationInstance(). Por ejemplo, así:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Recuperamos el objeto original Objeto objeto = (Objeto) getLastNonConfigurationInstance(); }
8.4.3 Cambio de orientación Manual Hay casos en los que es necesario controlar el proceso de creación-destrucción de una aplicación cuando se cambia la orientación del dispositivo y no queremos que Android lo haga de manera automática. En este caso, hay que especificar el atributo android:configChanges del elemento en el archivo AndroidManifest.xml:
427
El atributo anterior indica que la Actividad gestiona los cambios de orientación ocultando el teclado cuando este cambio ocurre. Además, cuando este giro del dispositivo ocurre, Android invoca el método onConfigurationChanged(), en el que se puede volver a dibujar la interfaz de usuario de la Actividad:
/* Se llama a este evento cuando Android cuando cambia la orientación * del dispositivo. ¡CUIDADO! Para que este evento se invoque debemos
428
Android Avanzado
* gestionar de forma MANUAL la funcionalidad de cambio de orientación. * Esto se consigue añadiendo en el archivo AndroidManifest.xml el atributo * android:configChanges="orientation..." * * Si controlamos de forma MANUAL el cambio de orientación, ya no son * necesarios los métodos onSaveInstanceState() y onRestoreInstanceState() * y debemos comentarlos. */ @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Si controlamos el cambio, también hay que guardar los contenidos de los componentes visuales String texto = et.getText().toString(); if (newConfig.orientation==Configuration.ORIENTATION_LANDSCAPE) { Toast.makeText(this,
"Cambio
a horizontal", Toast.LENGTH_SHORT).show();
setContentView(R.layout.main); } else { Toast.makeText(this,
"Cambio
a vertical", Toast.LENGTH_SHORT).show();
setContentView(R.layout.main); } //Obtenemos una referencia a las Vistas de la Actividad et = (EditText)findViewById(R.id.editText); //
Escribimos orientación
el
texto
que
tenía
el
EditText
antes
del
cambio
de
et.setText(texto); }
8.4.4 Cambiar la orientación de la pantalla con sentencias Java
429
En ocasiones, es necesario asegurarse de que una aplicación se muestra siempre en una orientación concreta. Por ejemplo, muchos juegos sólo se visualizan bien en modo horizontal. En este caso, mediante sentencias Java, se puede cambiar la orientación de la pantalla con el método setRequestOrientation() de la clase de Activity:
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); //Obtenemos una referencia al EditText de la Actividad et = (EditText)findViewById(R.id.editText); }
Además de utilizar el método setRequestOrientation() para cambiar la orientación de la pantalla, también se puede utilizar el atributo android:screenOrientation dentro del elemento en el archivo AndroidManifest.xml. Fíjate en el siguiente ejemplo:
430
>
Android Avanzado
Desde Eclipse puedes abrir el proyecto Ejemplo 3 (Orientación) de la Unidad 8. Estudia el código fuente y ejecútalo para mostrar en el emulador una aplicación en la que mostramos cómo manejar la orientación de la pantalla de un dispositivo Android.
Nota: por defecto, el Ejemplo 3 funciona en modo Manual. Si quieres cambiar a
modo
automático, debes modificar el archivo AndroidManifest.xml del proyecto. Atención: el emulador de Android no funciona muy bien a la hora de simular el cambio de orientación del dispositivo. Dependiendo de la versión de Android, algunos cambios de orientación no se pueden hacer o, a veces, un giro emulado del dispositivo destruye la Actividad dos veces antes de cambiar la orientación del terminal. Sin embargo, la teoría y funciones aquí expuestas sí son válidas para un dispositivo real que funcionará correctamente según lo esperado.
8.5
DESPLEGAR APLICACIONES VIRTUALES (AVD) O REALES
ANDROID
EN
DISPOSITIVOS
Para poder desplegar aplicaciones compiladas (tienen la extensión .apk), primero debemos conectar un dispositivo real por USB o arrancar un dispositivo virtual desde Eclipse. Esto es muy útil si queremos ver en funcionamiento los modelos de las actividades obligatorias de este curso, ya que únicamente se entregan compiladas.
Para arrancar manualmente un dispositivo virtual desde Eclipse hay que pulsar el siguiente botón de la barra de herramientas:
Desde la ventana de
dispositivos
virtuales
seleccionamos
el
dispositivo
que
deseamos arrancar y pulsamos el botón "Start": 431
A continuación, arrancará el dispositivo virtual. Si queremos instalar la aplicación en un dispositivo real de Android, no es necesario iniciar ningún dispositivo virtual.
Nota: en el caso de algunos dispositivos reales, dependiendo de la marca de dispositivo Android, puede ser necesario instalar los drivers para que el sistema operativo lo reconozca correctamente.
Además, el dispositivo real debe estar configurado para admitir la instalación de aplicaciones sin firmar por el Android Market. Si accedes en Ajustes->Aplicaciones debes marcar la siguiente opción:
432
Android Avanzado
Una vez disponemos de un dispositivo (real o virtual) de Android ejecutándose o conectado por USB al PC, abrimos una consola de Windows (o del sistema operativo correspondiente) y utilizamos la utilidad adb.exe (Android Debug Bridge) situada en la carpeta platform-tools del SDK de Android. En primer lugar, consultamos todos los identificadores de los dispositivos en ejecución mediante el comando "adb devices". Este comando debe devolver todas las instancias con los dispositivos abiertos:
Los dispositivos que aparezcan con la etiqueta "emulator-xxx" son dispositivos virtuales y los que muestren otra etiqueta son dispositivos reales (teléfonos, tablets, etcétera). Además, los dispositivos que aparezcan con la etiqueta "offline" están conectados, pero no están disponibles al ADB (Android Debug Bridge). Para este ejemplo, hemos seleccionado el dispositivo “emulator-5556“ que corresponde al dispositivo virtual con Android 2.3.3 para instalar una aplicación. Puedes ver el "id" del dispositivo en la ventana del emulador:
433
Tras obtener este identificador del emulador, vamos a instalar la aplicación mediante el comando “adb -s identificador-del-emulador install nombre-fichero-apk“. Fíjate en el siguiente ejemplo:
Una vez instala la aplicación, tenemos que ejecutarla en el dispositivo buscando su icono en la pantalla de aplicaciones:
434
Android Avanzado
Hacemos clic en el icono de la aplicación para ver su resultado:
8.6
CÓMO PUBLICAR APLICACIONES EN EL ANDROID MARKET
El Android Market (en español Mercado de Android) es una tienda de software en línea desarrollada por Google para los dispositivos Android. Es una aplicación que está preinstalada en la mayoría de los dispositivos Android y que permite a los usuarios buscar y descargar aplicaciones publicadas por terceros desarrolladores. Los usuarios también pueden buscar y obtener información sobre aplicaciones a través de una página Web. Las aplicaciones en el Android Market pueden ser gratuitas o de pago. En este apartado vamos a tratar cómo publicar una aplicación gratuita. En este apartado vamos a explicar los pasos para publicar una aplicación en el Android Market.
8.6.1
Alta de cuenta de desarrollador en el Android Market El primer paso obligatorio es darse de alta como desarrollador en el Android Market.
Para ello, necesitamos disponer de una cuenta de Google (GMail). Con el navegador de Internet accederemos a la dirección: 435
http://market.android.com/publish En esta página introducimos el usuario y la contraseña de Google:
La primera vez que accedemos a la página se muestra un asistente para dar de alta una nueva cuenta de desarrollador en el Android Market. Introducimos los datos que se solicitan (nombre del desarrollador, correo electrónico, URL del sitio Web y número de teléfono). Después, pulsamos el enlace "Seguir":
Para poder darnos de alta como desarrolladores del Android Market y publicar aplicaciones, hay que abonar 25,00$. Se trata de una cuota única sin caducidad. Para pagar
436
Android Avanzado
esta cuota podemos usar el servicio Google Checkout o pulsar en "Continuar" para pagar con tarjeta de crédito:
A continuación, aparece el detalle de la factura con el artículo "Android - Developer Registration Free for xxx". En esta página introducimos los datos de nuestra tarjeta de crédito para realizar el pago, así como la dirección postal de facturación donde llegará la correspondiente factura por correo ordinario:
437
Si todo está correcto, el asistente mostrará la siguiente ventana, indicando que "Su pedido se ha enviado al Android Market". Para continuar con el proceso, pulsamos en el enlace "Vuelve al sitio de desarrolladores de Android Market para completar el registro":
Después, leemos la licencia de desarrollador para el Android Market. Si estamos de acuerdo, hacemos clic en el enlace "Acepto las condiciones y deseo asociar la tarjeta de crédito y la cuenta que he registrado anteriormente al Acuerdo de distribución para desarrolladores de Android Market". Pulsamos "Acepto. Continuar":
438
Android Avanzado
El asistente indicará que el registro ha concluido, con el mensaje "Se ha aprobado tu registro en Android Market. Ahora puedes subir y publicar aplicaciones de software en Android Market". A partir de este momento ya podremos usar nuestra cuenta para publicar aplicaciones:
8.6.2
Recomendaciones sobre aplicaciones para Android Market Cuando desarrollemos aplicaciones que vamos a publicar en el Android Market,
debemos prestar especial atención a una serie de características. 8.6.2.1
Recomendaciones sobre aplicaciones para Android Market
Antes de empezar a desarrollar aplicaciones Android que vamos a publicar en el Market, hay que saber que cuando un usuario realiza una búsqueda de una aplicación en el Market usando su dispositivo Android, sólo le aparecerán las aplicaciones que cumplan los filtros (de permisos y de características del dispositivo) y el nivel de API (API Level) indicados en el archivo AndroidManifest.xml. El "API Level" es la versión de Android compatible con la aplicación. Por ejemplo, durante el curso hemos usado la versión 2.3.3 de Android que corresponde con el "API Level" 10. Si publicamos una aplicación desarrollada con esta versión de Android, únicamente será visible y sólo podrá instalarse en dispositivos con una versión igual o superior a la 2.3.3 de Android.
439
Los filtros de permisos permiten a una aplicación solicitar acceso a recursos de Android. Ya hemos estudiado que si, por ejemplo, una aplicación requiere acceder a la cámara de fotos, debemos indicarlo en el archivo AndroidManifest.xml: Al Indicar estos permisos, esta aplicación no aparecerá en las búsquedas realizadas desde dispositivos Android que no dispongan de cámara de fotos. Es decir, si solicitamos acceder a un recurso (cámara, wifi, bluetooth, etcétera) que el dispositivo no tiene, la aplicación no será visible en el Market. Además, existen otros filtros con las características del dispositivo en el archivo AndroidManifest.xml que hacen que la aplicación aparezca o no en el Market para un dispositivo determinado: •
: establece el tipo de pantalla (resolución mínima) que necesita la aplicación para funcionar.
•
: especifica el uso de características del dispositivo, por ejemplo: o
Para utilizar Bluetooth:
o
Para usar la cámara:
•
: indica las librerías específicas que requiere la aplicación.
Es importante tener en cuenta que cuanto mayores sean los requisitos de hardware (cámara, bluetooth, GPS, brújula, sensor de movimiento, etcétera), la aplicación será visible e instalable en un menor número de dispositivos Android.
8.6.2.2
Buenas prácticas para el desarrollo de aplicaciones Android
A continuación, mostramos algunas recomendaciones a la hora de desarrollar aplicaciones Android útiles, profesionales y fiables: •
Siempre hay que tener en cuenta que estamos desarrollando aplicaciones para dispositivos con pantalla muy pequeña, si son teléfonos, lo que no ocurre en los tablets, y teclado limitado, por lo que las aplicaciones deberían mostrar pocos campos de texto y opciones reducidas.
•
Antes de desarrollar una aplicación Android, es recomendable buscar en el Market si ya existe una aplicación similar. Si queremos que nuestra aplicación sea útil para los usuarios, debe ser interesante, original y sencilla incorporando funciones que no tengan otras.
440
Android Avanzado •
Hay que procurar, en la medida de lo posible, desarrollar aplicaciones que se puedan instalar en el mayor número posible de dispositivos para que tenga más difusión. Por lo tanto, debemos realizar aplicaciones con la versión de Android mínima y los requisitos de hardware básicos.
•
Las aplicaciones deben ser rápidas. Si es necesario realizar algún proceso que pueda tardar unos segundos, es recomendable avisar al usuario o, incluso, usar hilos de ejecución, servicios, etcétera. El usuario de un dispositivo móvil espera siempre rapidez de respuesta.
8.6.3
Generar fichero APK con certificado para Android Market Cuando compilamos un proyecto Android al hacer "Run" en Eclipse, el fichero .apk
(paquete de instalación de la aplicación Android) generado dentro del directorio /bin no es válido para subirlo directamente al Android Market. Si intentamos subir este fichero directamente aparecerá este mensaje:
Market does not accept apks signed with the debug certificate. Create a new certificate that is valid for at least 50 years. Market requires that the certificate used to sign the apk be valid until at least October 22, 2033. Create a new certificate. Market requires the minSdkVersion to be set to a positive 32-bit integer in AndroidManifest.xml. Uno de los requisitos para poder publicar de aplicaciones en Android Market es que el paquete de instalación APK debe estar firmado con un certificado válido de al menos 25 años. A continuación, explicamos cómo hacerlo. En primer lugar, una vez desarrollada y probada la aplicación Android con Eclipse, hacemos clic con el botón derecho del ratón sobre la carpeta del proyecto y seleccionamos la opción "Export" del menú emergente:
441
Abrimos la carpeta "Android" y seleccionamos "Export Android Application"; después, pulsamos el botón "Next":
En la ventana siguiente, en el campo "Project", podemos seleccionar otro proyecto si nos hemos equivocado. Pulsamos de nuevo el botón "Next":
442
Android Avanzado
A continuación, si no disponemos de una clave, seleccionamos la opción "Create new keystore". Introducimos un directorio y nombre para el almacén de claves, por ejemplo C:\cursos_Mentor\Android\claves.android. Introducimos la contraseña para el almacén de claves:
Si ya disponemos de un almacén de claves, seleccionamos "Use existing keystore" y seleccionamos el certificado escribiendo la clave correspondiente. Pulsamos el botón "Next" para seguir. A continuación, escribimos los datos administrativos de la clave que vamos a crear para certificar nuestras aplicaciones: 443
•
Alias: identificador de la clave.
•
Password: contraseña de la clave, debemos guardarla o recordarla pues la necesitaremos cada vez que vayamos a publicar una nueva aplicación o actualizar una ya existente en el Android Market.
•
Confirm: reescribimos la contraseña anterior.
•
Validity (years): validez del certificado, al menos 25 años.
•
First and Last Name: nombre del desarrollador o de la empresa.
•
Organization Unit: departamento.
•
Organization: nombre de la empresa.
•
City or Locality: ciudad.
•
State or Province: provincia.
•
Country Code: código postal de la ciudad.
Tras introducir los datos pulsamos el botón "Next": A continuación, indicamos la carpeta y el nombre del paquete APK compilado que se firma con el certificado anterior y que será el fichero que finalmente subiremos al Android Market. En es caso hemos seleccionado la carpeta C:\cursos_Mentor\Android\androidmarket del curso:
444
Android Avanzado
Si hemos seguido bien los pasos anteriores, ya dispondremos del fichero APK firmado con el certificado que podemos publicar en el Android Market:
8.6.4
Publicar una aplicación Android en el Android Market Vamos a explicar cómo publicar una aplicación firmada con el certificado para que
aparezca en Android Market y los usuarios puedan descargarla e instalarla. Accedemos a la web de Android Market con la cuenta de desarrollador que hemos dado de alta anteriormente escribiendo en la barra de direcciones del navegador:
https://market.android.com/publish/Home Pulsamos en el enlace "Subir aplicación":
445
Aparecerá una pagina donde podemos seleccionar el fichero APK pulsando en "Examinar" para elegir el fichero APK de nuestra aplicación Android firmada con el certificado:
Pulsamos en el botón "Publicar" para subirla al Android Market. Si el paquete APK está correcto y cumple con todos los requisitos (versión de Android, certificado, compilación, etcétera), el asistente muestra el botón "Guardar" y los datos del APK (nombre de la aplicación, nombre de la versión, código de la versión, permisos que necesita, funciones que necesita, tamaño, nombre de la clase Java). Pulsamos el botón "Guardar" para almacenar la aplicación:
446
Android Avanzado
Tras subirlo, pulsamos en el enlace "Activar" para introducir los datos necesarios para publicar la aplicación en el Android Market. Desde esta página podemos activar o desactivar la publicación de las aplicaciones subidas. Por ejemplo, si hemos detectado algún error y no queremos que los usuarios se descarguen una aplicación hasta solucionar el problema, podremos desactivarla:
Si pulsamos el botón "Activar", a continuación, aparece una ventana donde debemos añadir todos los datos requeridos en la pestaña "Información de producto" para acabar de dar de alta la nueva aplicación: •
Capturas de pantalla de la aplicación: al menos debemos subir dos capturas; es recomendable que tengan buena calidad, para que el usuario se haga una idea del aspecto que tiene la aplicación. 447
•
Icono de la aplicación: la aplicación se identifica con un icono que aparece en la parte izquierda de la pantalla del Android Market cuando los usuarios buscan aplicaciones.
•
Imagen promocional, imagen de funciones y vídeo promocional de Youtube: son datos opcionales que sirven para incluir más información de la aplicación.
•
Si no deseamos que la aplicación se anuncie fuera de Android Market, marcamos la Casilla: "No promocionar mi aplicación salvo en Android Market” y en los sitios web o para móviles propiedad de Google. Asimismo, soy consciente de que cualquier cambio relacionado con esta preferencia puede tardar sesenta días en aplicarse".
•
Podemos elegir varios idiomas para escribir la descripción de las funciones y uso de la aplicación. El inglés es obligatorio. En este punto se solicitan los campos:
Título de la aplicación: nombre que aparece en las búsquedas, no debe ser muy largo (inferior a 30 caracteres).
Descripción: descripción detallada (hasta 4000 caracteres) de la funcionalidad de la aplicación.
Cambios recientes: si se trata de una actualización, podemos indicar aquí las últimas mejoras implementadas.
Si hemos incluido un vídeo promocional, podemos añadir un texto promocional.
Tipo de aplicación: seleccionamos en el desplegable el tipo que más se ajuste a la funcionalidad de la aplicación.
Categoría: seleccionamos en el desplegable la categoría que más se ajuste a la aplicación.
•
Protección contra copias: lo usual es que no esté seleccionada esta opción, ya que, como indica Android Market, esta función quedará obsoleta en breve, siendo sustituida por el servicio de licencias.
•
Clasificación del contenido: marcamos si nuestra aplicación es para todos los públicos o contiene algún tipo de contenido para mayores.
•
Precios: aquí indicamos si la aplicación es gratuita o de pago.
•
Precio predeterminado: si hemos elegido de pago, en este campo introducimos el precio de la aplicación. Pulsando el botón "Autocompletar" hará los ajustes para los diferentes países en los que queramos publicarla.
448
Android Avanzado •
También se indica el número aproximado de modelos de dispositivos Android sobre los que se podrá instalar la aplicación en función de los filtros indicados en el archivo de manifiesto.
•
Información de contacto: o
Sitio web.
o
Correo electrónico.
o
Teléfono.
En la siguiente ventana se muestra parte de los datos que hay que incluir:
Una vez introducidos los datos, pulsamos en el botón "Guardar" de la parte superior derecha. A continuación, se comprueba si los datos son completos y correctos y, si no hay errores, se guardarán los datos asociados al archivo APK. 449
Después, pulsamos en el botón "Publicar" (a la izquierda del botón "Guardar") para publicar definitivamente la aplicación en Android Market:
Tras finalizar la publicación, se mostrará en "Todos los elementos de Android Market" la nueva aplicación con el estado "Publicada". En esta página podemos llevar a cabo un seguimiento del número de instalaciones, posibles errores, comentarios de los usuarios, popularidad, etcétera.
450
Android Avanzado
La Depuración de programas (en inglés Debug) es el proceso de identificar y corregir errores de programación en tiempo de ejecución. Un Punto de interrupción (Breakpoint en inglés) es una marca en el código fuente que pausa la ejecución de un programa, para que el programador pueda evaluar los valores asignados a las variables y detectar errores en tiempo de ejecución. El entorno de desarrollo Eclipse permite llevar a cabo de manera sencilla la Depuración de programas. Es posible incluir mapas en las aplicaciones de Android haciendo uso de la API Android de Google Maps. Para poder utilizar la API de Google Maps, es necesario disponer de una clave de uso (API Key) que estará asociada al certificado con el que firmamos digitalmente las aplicaciones. El Android Market (en español Mercado de Android) es una tienda de software en línea desarrollada por Google para los dispositivos Android. Para poder publicar aplicaciones en el Android Market, es necesario darse de alta y pagar una cuota. El paquete de instalación APK de una aplicación del Android Market debe estar firmado con un certificado válido de al menos 25 años.
451