servicios de la plataforma android - jtech.ua.es

97
Servicios de la plataforma Android Índice 1 Librerías de compatibilidad y servicios........................................................................ 3 1.1 Compatibilidad de la aplicación...............................................................................3 1.2 Fragmentos............................................................................................................... 4 1.3 Loaders................................................................................................................... 11 1.4 Librerías de compatibilidad................................................................................... 14 1.5 Librerías de servicios............................................................................................. 16 2 Ejercicios de fragmentos y compatibilidad................................................................ 19 2.1 Lector de noticias (1,5 puntos)...............................................................................19 2.2 Carga de noticias (1 punto).................................................................................... 20 2.3 Servicios de Google (0,5 puntos)........................................................................... 20 3 Agenda y calendario................................................................................................... 22 3.1 Agenda de contactos.............................................................................................. 22 3.2 Calendario.............................................................................................................. 27 4 Ejercicios de agenda y calendario.............................................................................. 33 4.1 Acceso a la agenda de contactos (1 punto)............................................................ 33 4.2 Agregar contactos a la agenda (1,5 puntos)........................................................... 33 4.3 Creación de eventos del calendario (0,5 puntos)................................................... 33 5 Servicios..................................................................................................................... 35 5.1 Servicios propios.................................................................................................... 35 5.2 Broadcast receiver.................................................................................................. 42 5.3 PendingIntents y servicios del sistema.................................................................. 44 5.4 Comunicación entre procesos................................................................................ 45 6 Ejercicios - Servicios.................................................................................................. 50 6.1 Contador: Servicio con proceso en background (0.6 puntos)................................ 50 6.2 Broadcast Receiver: Captura de llamadas (0.6 puntos)......................................... 50 6.3 Broadcast Receiver: Reenvío de datos (0.6 puntos).............................................. 51 Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Upload: others

Post on 17-Mar-2022

5 views

Category:

Documents


0 download

TRANSCRIPT

Servicios de la plataforma Android

Índice

1 Librerías de compatibilidad y servicios........................................................................3

1.1 Compatibilidad de la aplicación...............................................................................3

1.2 Fragmentos...............................................................................................................4

1.3 Loaders...................................................................................................................11

1.4 Librerías de compatibilidad................................................................................... 14

1.5 Librerías de servicios............................................................................................. 16

2 Ejercicios de fragmentos y compatibilidad................................................................ 19

2.1 Lector de noticias (1,5 puntos)...............................................................................19

2.2 Carga de noticias (1 punto).................................................................................... 20

2.3 Servicios de Google (0,5 puntos)...........................................................................20

3 Agenda y calendario...................................................................................................22

3.1 Agenda de contactos.............................................................................................. 22

3.2 Calendario.............................................................................................................. 27

4 Ejercicios de agenda y calendario.............................................................................. 33

4.1 Acceso a la agenda de contactos (1 punto)............................................................ 33

4.2 Agregar contactos a la agenda (1,5 puntos)........................................................... 33

4.3 Creación de eventos del calendario (0,5 puntos)................................................... 33

5 Servicios..................................................................................................................... 35

5.1 Servicios propios....................................................................................................35

5.2 Broadcast receiver..................................................................................................42

5.3 PendingIntents y servicios del sistema.................................................................. 44

5.4 Comunicación entre procesos................................................................................ 45

6 Ejercicios - Servicios..................................................................................................50

6.1 Contador: Servicio con proceso en background (0.6 puntos)................................ 50

6.2 Broadcast Receiver: Captura de llamadas (0.6 puntos)......................................... 50

6.3 Broadcast Receiver: Reenvío de datos (0.6 puntos).............................................. 51

Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

6.4 Arranque: Iniciar servicio al arrancar el móvil (0.6 puntos)..................................52

6.5 Calculadora: Comunicación con el servicio (0.6 puntos)...................................... 52

7 AppWidgets................................................................................................................54

7.1 AppWidgets........................................................................................................... 54

7.2 Crear un Widget.....................................................................................................55

7.3 Actualización del Widget.......................................................................................59

7.4 Eventos de actualización........................................................................................60

7.5 Servicio de actualización....................................................................................... 62

7.6 Actividad de configuración....................................................................................63

8 Ejercicios - AppWidgets............................................................................................ 65

8.1 IP AppWidget (1.5 puntos).................................................................................... 65

8.2 StackWidget (1.5 puntos).......................................................................................67

9 Notificaciones.............................................................................................................69

9.1 Notificaciones Toast.............................................................................................. 69

9.2 Notificaciones de la Barra de Estado..................................................................... 73

9.3 Cuadros de Diálogo................................................................................................77

10 Ejercicios - Notificaciones........................................................................................82

10.1 Notificaciones con Toast (1 punto)...................................................................... 82

10.2 Servicio con notificaciones: Números primos (1 punto)......................................82

10.3 Notificaciones mediante diálogos (1 punto).........................................................83

11 Depuración y pruebas............................................................................................... 86

11.1 Depuración con Eclipse........................................................................................86

11.2 Pruebas unitarias con JUnit para Android............................................................88

11.3 Pruebas de regresión con Robotium.....................................................................90

11.4 Pruebas de estrés con Monkey............................................................................. 93

12 Ejercicios - Depuración y pruebas............................................................................94

12.1 Caso de prueba con JUnit para Android (3 puntos)............................................. 94

Servicios de la plataforma Android

2Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

1. Librerías de compatibilidad y servicios

Vamos a ver algunas librerías adicionales que podemos añadir a nuestras aplicaciones,para conseguir compatibilidad con dispositivos antiguos o para acceder a serviciosexternos. Por ejemplo, una característica importante aparecida en versiones recientes deAndroid son los fragmentos. Es recomendable construir nuestras aplicaciones utilizandoestos elementos para la interfaz, pero si lo hacemos nuestra aplicación ya no seríacompatible con dispositivos Android 2.2 y 2.3, que son una parte importante del mercadoactual. Vamos a ver cómo podemos resolver esto incluyendo librerías adicionales decompatibilidad. También veremos cómo incluir librerías adicionales que nos den acceso alos servicios de Google Play.

1.1. Compatibilidad de la aplicación

A la hora de desarrollar una aplicación es importante decidir a qué versiones de laplataforma Android está destinada. Las últimas versiones nos dan muchas más facilidadespara crear las aplicaciones, pero es importante dar soporte a versiones antiguas paraabarcar a un mayor número de usuarios. Se recomienda que nuestras aplicacionessoporten al menos el 90% de los dispositivos que hay actualmente en uso. En el momentode la escritura de este texto, esto implicaría dar soporte al menos a partir de Android 2.2.

Para especificar el rango de versiones a las que destinamos nuestra aplicación se utilizanlos atributos minSdkVersion y targetSdkVersion de la etiqueta uses-sdk delAndroidManifest.xml:

<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="17" />

El atributo minSdkVersion indica la versión mínima de Android necesaria para poderutilizar nuestra aplicación. Por debajo de dicha versión nuestra aplicación no funcionará.Sin embargo, este atributo no nos condiciona a utilizar sólo las características compatiblescon la versión mínima. Podemos utilizar características de versiones mayores. La versiónpara la cual hemos diseñado la aplicación se indica con targetSdkVersion. En el códigopodremos utilizar cualquier característica que soporte dicha versión.

Pero, ¿qué ocurre si utilizamos una característica de targetSdkVersion no soportada enminSdkVersion? En ese caso, cuando probemos la aplicación en un dispositivo con laversión mínima fallará. Por lo tanto, es importante que antes de utilizar dichascaracterísticas comprobemos la versión de Android en la que se está ejecutando:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {// Utilizar características de Android 3.0 (Honeycomb)

}

NotaEn el caso de las propiedades de los ficheros XML, si utilizamos atributos que no estabandefinidos en la versión mínima, cuando ejecutemos la aplicación en dicha versión simplemente

Servicios de la plataforma Android

3Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

serán ignorados.

Deberemos llevar cuidado de hacer siempre esta comprobación cuando estemosutilizando características no presentes en la versión mínima. Para asegurar el correctofuncionamiento deberemos probar la aplicación de forma exhaustiva con dispositivos quetengan tanto la versión mínima como la versión para la cual estamos desarrollando.

Sin embargo, existen algunas características, como por ejemplo los fragmentos, que sonfundamentales en la construcción de la aplicación, y no se pueden ignorar mediante lacomprobación anterior. Para estas características veremos a continuación cómo utilizaruna librería de compatibilidad que añada soporte para versiones previas de Android.

1.2. Fragmentos

A partir de Android 3.0 aparece una característica importante a la hora de construir lainterfaz: los fragmentos de actividades. Estos fragmentos nos permiten construir lainterfaz de forma modular. Un fragmento se define como un panel, y una actividad puedeestar compuesta por varios fragmentos. De esta forma podremos reutilizar los fragmentosen diferentes actividades, y podremos construir actividades complejas de forma sencilla.

Una ventaja importante es que nos permiten adaptar de forma sencilla la interfaz adistintos tipos de pantallas, como tabletas, ya que distintos fragmentos que en un móvilestán en distintas pantallas, en una tableta podrían incluirse en una única pantalla,reutilizando su código.

Fragmentos

1.2.1. Creación de fragmentos

Los fragmentos se crean como una subclase de Fragment, casi de la misma forma en laque se define una actividad. Se utilizan los mismos métodos para controlar el ciclo devida, y debemos definir un método adicional onCreateView que especifica la forma en laque se debe generar la interfaz del fragmento. La interfaz se generará normalmente apartir de un layout XML:

Servicios de la plataforma Android

4Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

public class DetalleFragment extends Fragment {@Overridepublic View onCreateView(LayoutInflater inflater,

ViewGroup container, Bundle savedInstanceState) {return inflater.inflate(R.layout.fragmento, container, false);

}}

Además de este método anterior, tenemos disponibles todos los métodos que teníamos enel ciclo de vida de las actividades (onCreate, onStart, onResume, etc). Pero debemostener en cuenta que un fragmento no es una actividad, por lo que por ejemplo noheredamos métodos como findViewById, y tampoco podemos utilizarlo en los métodosque nos piden pasar como parámetro el contexto (Context), donde normalmente siempreindicabamos nuestra actividad. Para poder hacer todo esto deberemos acceder a laactividad en la que está contenido nuestro fragmento. Esto lo haremos con el métodogetActivity():

Button boton = (Button)getActivity().findViewById(R.id.boton);

Al igual que ocurría con las actividades, tenemos diferentes subclases de Fragment paradefinir tipos específicos de fragmentos, como ListFragment, DialogFragment, oPreferencesFragment. Por ejemplo, si queremos definir un fragmento de tipo lista,podemos heredar directamente de ListFragment:

public class PrincipalFragment extends ListFragment {...

}

Por otro lado, DialogFragment nos permitirá crear diálogos. En este caso el contenidodel fragmento se mostrará dentro de la ventana del diálogo.

1.2.2. Ciclo de vida de los fragmentos

Los fragmentos siempre estarán contenidos dentro de una actividad, por lo que su ciclo devida quedará vinculado al estado de la actividad contenedora. En la siguiente figuramostramos los distintos eventos del ciclo de vida de los fragmentos, y la vinculación conel estado de la actividad a la que pertenecen:

Servicios de la plataforma Android

5Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Ciclo de vida

En la anterior figura podemos observar que durante el estado de creación de la actividad,en el fragmento se producen diferentes eventos. En primer lugar el fragmento se vincula ala actividad (onAttach). Tras esto se crea el fragmento (onCreate), pero debemos teneren cuenta que en este punto todavía no contamos con su interfaz, por lo que no podemosinicializarla. Este método será útil por ejemplo para inicializar el adapter que pueble dedatos la lista de un ListFragment, en el que no es necesario crear la interfaz nosotros.Tras este evento, tenemos onCreateView, que es donde deberemos crear la interfaz comohemos visto anteriormente (excepto si estamos en un ListFragment, caso en el que lainterfaz ya viene predefinida). Por último, se ejecutará onActivityCreated una vez hayaterminado de ejecutarse el método onCreate de la actividad contenedora.

Los eventos onStart, onResume, onPause y onStop están directamente vinculados consus equivalentes en la actividad contenedora.

La destrucción será similar a la inicialización pero en orden inverso. En primer lugar seelimina la vista (onDestroyView). Esto puede hacerse para liberar recursos, pero el

Servicios de la plataforma Android

6Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

fragmento podría volver a utilizarse posteriormente. Si esto ocurriese, se volvería a llamara onCreateView para volver a construir la vista. Si el fragmento ya no se va a utilizarmás, se llamará a onDestroy, y tras esto a onDetach en el momento en el que sedesvincula de la actividad.

1.2.3. Añadir el fragmento a una actividad

Una vez definido el fragmento, podemos añadirlo a una o varias actividades. Existen dosformas de añadir los fragmentos:

• Estática: Se añaden directamente en el layout XML de la actividad. Si se hace de estaforma no podremos modificar el conjunto de fragmentos que se muestran en laactividad en tiempo de ejecución (la actividad siempre mostrará los mismosfragmentos dispuestos de la misma forma).

• Dinámica: Los fragmentos se añaden desde código. De esta forma podremos cambiaren cualquier momento el fragmento que se está mostrando en la actividad.Utilizaremos esta forma cuando queramos poder hacer transiciones entre fragmentos.

La definición de fragmentos mediante el método estático se realizaría de la siguienteforma:

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

<fragment android:name="es.ua.jtech.fragments.PrincipalFragment"android:id="@+id/principal_fragment"android:layout_weight="1"android:layout_width="0dp"android:layout_height="match_parent" />

<fragment android:name="es.ua.jtech.fragments.DetalleFragment"android:id="@+id/detalle_fragment"android:layout_weight="2"android:layout_width="0dp"android:layout_height="match_parent" />

</LinearLayout>

Si queremos dar soporte a varios tamaños de dispositivos, simplemente deberíamosdefinir diferentes layouts alternativos con distintos clasificadores (large, xlarge, etc).

Pero, ¿qué ocurre si queremos dar soporte a dispositivos más pequeños en los que los dosfragmentos no caben en la misma pantalla? En este caso podemos utilizar la métododinámico, para mostrar un único fragmento en pantalla y poder cambiar a otro de formadinámica. Para hacer esto deberemos definir un layout alternativo para la actividad (porejemplo con clasificador small o normal) en el que definamos un marco vacío dondepodamos poner el fragmento que queramos directamente (FrameLayout):

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/fragment_container"android:layout_width="match_parent"

Servicios de la plataforma Android

7Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

android:layout_height="match_parent" />

En nuestra actividad, en primer lugar deberemos comprobar si el layout cargado es elestático o el dinámico:

public class MainActivity extends Activity {@Overridepublic void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);setContentView(R.layout.news_articles);

// Comprueba si estamos usando el layout dinámicoif (findViewById(R.id.fragment_container) != null) {

// Si se está restaurando, no hace falta cargar el fragmentoif (savedInstanceState != null) {

return;}

// Creamos el fragmentoPrincipalFragment ppalFragment = new PrincipalFragment();

// Pasamos los extras del intent al fragmentoppalFragment.setArguments(getIntent().getExtras());

// Añadimos el fragmento al contenedorgetFragmentManager().beginTransaction()

.add(R.id.fragment_container, ppalFragment).commit();}

}}

Como podemos observar, para modificar los fragmentos en pantalla debemos obtener unobjeto FragmentManager y a partir de él abrir una transacción (FragmentTransaction).Una vez se haya hecho la operación oportuna (en este caso add), confirmaremos latransacción (commit).

1.2.4. Transiciones entre fragmentos

En el caso en el que hayamos añadido el fragmento de forma dinámica, podremos haceruna transición a otro fragmento utilizando el objeto FragmentTransaction.

DetalleFragment detalleFragment = new DetalleFragment();

// Pasamos parametros al nuevo fragmentoBundle args = new Bundle();args.putInt(PARAM_POSICION, posicionSeleccionada);detalleFragment.setArguments(args);

FragmentTransaction transaction =getFragmentManager().beginTransaction();

transaction.replace(R.id.fragment_container, detalleFragment);transaction.addToBackStack(null);

transaction.commit();

En este caso vemos que tras reemplazar el fragmento, se añade la operación a la backstack, para así poder volver al fragmento anterior pulsando la tecla atrás.

Servicios de la plataforma Android

8Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

1.2.5. Comunicación entre fragmentos

Cuando hacemos una transición entre fragmentos, si un fragmento tiene que pasar datos aotro lo puede hacer en el momento de la transición, como hemos visto en el caso anterior.Pero si todos los fragmentos se muestran de forma estática, necesitaremos podercomunicarlos para que por ejemplo, cuando pulsemos un item de un fragmento lista, enotro fragmento veamos los detalles de dicho item.

La comunicación entre fragmentos siempre lo haremos a través de la actividad a la quepertenecen. En uno de los fragmentos definiremos una interfaz que deba implementar laactividad:

public class PrincipalFragment extends ListFragment {OnItemSelectedListener mCallback;

// La actividad debe implementar esta interfazpublic interface OnItemSelectedListener {

public void onItemSelected(int position);}

@Overridepublic void onAttach(Activity activity) {

super.onAttach(activity);

// Comprueba que la actividad implemente la interfaz definidatry {

mCallback = (OnItemSelectedListener) activity;} catch (ClassCastException e) {

throw new ClassCastException(activity.toString()+ " debe implementar OnItemSelectedListener");

}}

...}

El método onAttach se ejecutará cuando el fragmento se vincule a la actividad. Aquí nosguardamos una referencia a la actividad mediante un campo del tipo del listener recibido,con el que podremos notificar a la actividad cada vez que se haya seleccionado un item.

En el fragmento anterior podemos utilizar esta referencia a la actividad para hacer uncallback en el momento en el que se pulse sobre un item de la lista:

public class PrincipalFragment extends ListFragment {...

@Overridepublic void onListItemClick(ListView l, View v,

int position, long id) {mCallback.onItemSelected(position);

}}

En la actividad deberemos implementar el listener definido, y en él deberemos pasar elmensaje al fragmento con los detalles. Esto se hará de forma distinta según si utilizamosel método estático o dinámico.

Servicios de la plataforma Android

9Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

public static class MainActivity extends Activityimplements PrincipalFragment.OnItemSelectedListener {

...

public void onItemSelected(int position) {

DetalleFragment detalleFragment = (DetalleFragment)getFragmentManager().findFragmentById(R.id.detalle_fragment);

if (detalleFragment != null) {// Tipo estático: actualizamos directamente el fragmentodetalleFragment.setDetalleItem(position);

} else {// Tipo dinámico: hacemos transición al nuevo fragmento

detalleFragment = new DetalleFragment();Bundle args = new Bundle();args.putInt(PARAM_POSICION, position);detalleFragment.setArguments(args);

FragmentTransaction transaction =getFragmentManager().beginTransaction();

transaction.replace(R.id.fragment_container,detalleFragment);

transaction.addToBackStack(null);

transaction.commit();}

}}

En el caso estático, basta con definir un método propio en el fragmento que nos permitaactualizar su contenido. En el caso dinámico, tendremos que hacer una transición alnuevo fragmento pasándole los datos del item seleccionado.

1.2.6. Uso de diálogos

Como hemos visto anteriormente, podemos crear diálogos utilizando fragmentos (dehecho, esta es la forma recomendada de hacerlo). Simplemente deberemos crear una claseque herede de DialogFragment. El fragmento se definirá de la misma forma quecualquiera de los anteriores, cargando su contenido en onCreateView. En este caso,además de cargar la vista, también podremos especificar un título para la ventana deldiálogo con el método getDialog().setTitle():

public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {

View view = inflater.inflate(R.layout.dialog, container);getDialog().setTitle("Título ");

return view;}

Donde encontramos mayores diferencias respecto a los fragmentos anteriores es la formade mostrarlo. En este caso lo haremos de la siguiente forma:

FragmentManager manager = getFragmentManager();

Servicios de la plataforma Android

10Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

MiDialogFragment dialog = new MiDialogFragment();dialog.setArguments(bundle);dialog.show(manager, "fragment_dialog");

Podemos observar que en este caso el propio fragmento tiene un método show que nospermite mostrarlo en pantalla (proporcionando como parámetro el fragment manager yuna etiqueta identificativa opcional para el diálogo que nos permitirá localizarlo másadelante).

Al igual que en casos anteriores, al fragmento se le pueden pasar datos en el momento desu creación proporcionando un bundle.

1.3. Loaders

Otra características aparecida en Android 3.0 son los loaders. Esta característica estádisponible en cualquier fragmento o actividad. Dichos fragmentos o actividadesnormalmente necesitan cargar datos al ejecutarse (por ejemplo de un servicio web, o deuna base de datos SQlite). Los loaders están pensados para realizar esta tarea de formaasíncrona.

Una ventaja de los loaders es que una vez han cargado los datos los retienen aunque laactividad se detenga (onStop), de forma que no será necesario volverlos a cargar cuandola actividad se vuelva a poner en marcha (onStart). Además, el loader estará pendientede los cambios que se produzcan en su fuente de datos. Si detecta un cambio, obtendrá losnuevos datos y se los volverá a proporcionar a nuestra actividad o fragmento de formaautomática.

La clase principal que deberemos utilizar es LoaderManager, que nos permitirá manejarvarios loaders. Normalmente pondremos en marcha los loaders en el método onCreate

de nuestra actividad o en el método onActivityCreated de un fragmento:

getLoaderManager().initLoader(0, null, this);

El primer parámetro es un código identificador del loader que queremos poner en marcha.Si ya existe un loader activo con dicho código, se reutilizará dicho loader. Si no existe, secreará uno nuevo. Para crear un loader necesita que se le proporcione un objeto queimplemente la interfaz LoaderManager.LoaderCallbacks (como tercer parámetro delmétodo anterior debemos indicar dicho objeto). Normalmente haremos que sea la propiaactividad o fragmento quien la implemente (this en el ejemplo anterior), lo cual nosobligará a definir los siguientes métodos:

public class MiListFragment extends ListFragmentimplements LoaderManager.LoaderCallbacks<Tipo> {

...

public Loader<Tipo> onCreateLoader(int id, Bundle args) { ... }

public void onLoadFinished(Loader<Tipo> loader, Cursor data) { ... }

public void onLoaderReset(Loader<Tipo> loader) { ... }

Servicios de la plataforma Android

11Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}

Podemos observar que en el callback hay que especificar el tipo de datos que maneja elloader, es decir, el tipo de datos que queremos cargar con él, mediante el uso degenéricos.

1.3.1. Creación de un loader

Cuando solicitemos iniciar un loader que todavía no está en funcionamiento se llamará almétodo onCreateLoader en el que deberemos especificar la forma de crearlo. Estemétodo recibe como parámetro el identificador del loader que debemos crear.

Podemos crear el loader mediante alguna clase derivada de Loader. Habitualmenteutilizaremos CursorLoader para cargar datos de proveedores de contenidos, oAsyncTaskLoader para cargar datos de forma personalizada mediante una async task.

Por ejemplo, para crear un loader de tipo cursor que lea los datos de un proveedor decontenidos, haremos lo siguiente:

public Loader<Cursor> onCreateLoader(int id, Bundle args) {return new CursorLoader(getActivity(), baseUri, proyeccion,

seleccion, args, orden);}

1.3.2. Obtención de resultados

Cuando el loader haya terminado de cargar los datos (puede que esto sea instantáneo si yaestaban cargados de antemano), se llamará al método onLoaderFinished. En estemétodo deberemos utilizar los datos obtenidos, por ejemplo para mostrarlos en pantalla.Por ejemplo, si contamos con un CursorAdapter podemos proporcionarle el nuevocursor:

public void onLoadFinished(Loader<Cursor> loader, Cursor data) {mAdapter.swapCursor(data);

}

Es importante no cerrar el cursor anterior, ya que de esto se encargará el loader.

1.3.3. Reinicio del loader

Si queremos que el loader vuelva a cargar los datos (por ejemplo por haber cambiadoalgún criterio de búsqueda), podemos utilizar el método restartLoader, de la mismaforma en la que se utilizaba initLoader:

getLoaderManager().restartLoader(0, null, this);

En este caso se deberán eliminar los datos cargados anteriormente, y comenzar una nuevacarga. En el método onLoadReset deberemos indicar la forma de eliminar estos datos

Servicios de la plataforma Android

12Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

(por ejemplo, eliminando el cursor del CursorAdapter):

public void onLoaderReset(Loader<Cursor> loader) {mAdapter.swapCursor(null);

}

1.3.4. Loader personalizado

Si queremos realizar la carga de datos de forma personalizada, lo más sencillo será crearuna subclase de AsyncTaskLoader en la que programaremos cómo se realiza la descargaen segundo plano.

Se define de forma parecida a la clase AsyncTask. En este caso el método principal quedeberemos definir es loadInBackground, que será el que se ejecutará en segundo planopara realizar la carga de datos, pero además deberemos sobrescribir otros métodos paracontrolar el proceso de descarga. A parte de loadInBackground, al menos deberemosdefinir onStartLoading que deberá comprobar si ya tenemos disponibles los datosactualizados, o si por el contrario debemos volverlos a cagar. Si ya dispusiésemos deellos, los devolveremos llamando a deliverResult(datos), y en caso contrariollamaremos a forceLoad() para hacer que se vuelvan a descargar (esto provocará lallamada a loadInBackground desde un hilo en segundo plano). Es importante destacarque si no sobrescribimos onStartLoading, siempre considerará por defecto que yadispone de los datos y nunca se realizará la descarga.

En el siguiente ejemplo almacenamos los datos descargados en una variable de instancia,y con ella controlaremos si es necesario descargarlos (si es null) o si ya disponemos deellos y podemos devolverlos. Para ello sobrescribimos también deliverResult, para quetras obtener los datos antes de devolverlos se retengan en dicha variable de instancia.También definimos el método onStopLoading (que detiene el proceso de carga) yonReset (que detiene el proceso y además elimina los datos ya descargados para que lapróxima vez se vuelvan a descargar).

static class MiLoader extends AsyncTaskLoader<Tipo> {

Tipo mDatos = null;

public MiLoader(Context context) {super(context);

}

@Overridepublic Tipo loadInBackground() {

return cargarDatos();}

@Overridepublic void deliverResult(Tipo data) {

mDatos = data;super.deliverResult(data);

}

@Overrideprotected void onStartLoading() {

super.onStartLoading();

Servicios de la plataforma Android

13Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

if(mDatos != null) {deliverResult(mDatos);

} else {forceLoad();

}}

@Overrideprotected void onStopLoading() {

super.onStopLoading();cancelLoad();

}

@Overrideprotected void onReset() {

super.onReset();onStopLoading();mDatos = null;

}}

Una vez finalizada la carga de datos, le proporcionará al callback los datos obtenidos.Definiremos el callback de la siguiente forma:

public class MiFragmento implements LoaderManager.LoaderCallbacks {...

public Loader<Tipo> onCreateLoader(int id, Bundle args) {return new MiLoader(getActivity());

}

public void onLoadFinished(Loader<Tipo> loader, Tipo data) {mAdapter.clear();for(Item item: data) {

mAdapter.add(item);}

}

public void onLoaderReset(Loader<Tipo> loader) {mAdapter.clear();

}

}

Si detectásemos que ha habido algún cambio en nuestra fuente de datos, podemos llamaral método onContentChanged() del objeto AsyncTaskLoader para forzar que vuelva adescargar los datos y se los proporcione a nuestra actividad o fragmento.

1.4. Librerías de compatibilidad

Los fragmentos están disponibles sólo a partir de Android 3.0, y todavía existen en elmercado numerosos dispositivos con versiones anteriores. Sin embargo, el uso defragmentos se ha convertido en una práctica muy recomendable en el diseño deaplicaciones, por lo que deberíamos utilizarlos. Para permitir que los desarrolladoresutilicen esta nueva característica, pero no se pierda el soporte para dispositivos antiguos,se proporciona una librería de compatibilidad que incorpora las características másimportantes de las nuevas versiones de Android a dispositivos con versiones anteriores

Servicios de la plataforma Android

14Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

(da soporte a dispositivos desde Android 1.6). Vamos a ver cómo utilizar esta librería.

En primer lugar, deberemos descargar la librería con el Android SDK Manager (si no latenemos ya). La encontraremos en la carpeta Extras > Android Support.

Librería de soporte

Una vez descargada, podremos encontrarla en el directorio:

$ANDROID_SDK/extras/android/support/v4/android-support-v4.jar

Para incluirla en nuestro proyecto tendremos que crear en él un directorio libs y copiarahí dicha librería. Android reconocerá de forma automática todas las libreríasintroducidad en libs.

Con esta librería ya podemos indicar en el fichero AndroidManifest.xml que la versiónmínima a la que damos soporte es la 1.6 (nivel 4):

<uses-sdk android:minSdkVersion="4" android:targetSdkVersion="17" />

Ahora deberemos adaptar nuestra aplicación para que use la librería de compatibilidadpara acceder a los fragmentos y loaders. Deberemos hacer una serie de modificaciones:

• Ahora todos los import referentes a fragmentos deberemos cogerlos del paqueteandroid.support.v4.app correspondiente a la librería de compatibilidad. De estaforma tendremos:import android.support.v4.app.Fragment;import android.support.v4.app.FragmentManager;import android.support.v4.app.FragmentTransaction;import android.support.v4.app.LoaderManager;

• La actividad contenedora de fragmentos ahora deberá heredar de FragmentActivity,en lugar de heredar simplemente de Activity:import android.support.v4.app.FragmentActivity;

public class MainActivity extends FragmentActivity {...

Servicios de la plataforma Android

15Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}

• Para obtener el objeto FragmentManager ahora utilizaremos el métodogetSupportFragmentManager, y de la misma forma para obtener LoaderManagerutilizaremos getSupportLoaderManager:FragmentManager manager = getSupportFragmentManager();LoaderManager manager = getSupportLoaderManager();

Con los cambios anteriores conseguiremos que nuestra aplicación funcione correctamenteen cualquier versión de Android desde la 1.6.

1.5. Librerías de servicios

Google proporciona una serie de servicios que pueden ser integrados en las aplicacionesAndroid, como Google Maps y Google+. En los dispositivos Android normalmenteencontramos incluidas una serie de librerías de Google que nos permiten integrar GoogleMaps en nuestras aplicaciones. Incluir esta API en el propio sistema operativo deldispositivo hace que sea poco flexible a la hora de actualizar las aplicaciones de Google,ya que lo más común es que las actualizaciones del sistema operativo se proporcionensólo durante un periodo limitado de tiempo.

Por este motivo Google ha pasado a incluir sus librerías de forma externa. Ahora suslibrerías deben descargarse de Google Play, y se actualizan continuamente desde esatienda. De esta forma la mayor parte de los dispositivos van a tener soporte para lasúltimas versiones de estos servicios.

Actualmente la librería de servicios de Google (que soporta Google Maps v2 y Google+)soporta todos los dispositivos Android a partir de la versión 2.2 (API de nivel 8). El únicorequerimiento es descargar las librerías de Google desde Google Play y mantenerlasactualizadas.

1.5.1. Configuración de las librerías de Google para el desarrollo

Para utilizar las librerías de Google en nuestros proyectos, deberemos descargarlas yañadirlas como proyecto a Eclipse. Seguiremos los siguientes pasos:

1. Descargamos las librerías desde Android SDK Manager. Las encontraremos en lacarpeta Extras > Google Play services.

2. Una vez descargadas las podremos encontrar en la siguiente ruta, que contiene unproyecto Eclipse con dichas librerías:$ANDROID_SDK/extras/google/google_play_services/

3. Importamos en Eclipse el proyecto que se encuentra en la ruta anterior, con File >Import ... > Android > Existing Android Code into Workspace

Con esto tendremos las librerías de Android disponibles para ser utilizadas en nuestrosproyectos. Para hacer que uno de nuestros proyectos las utilice deberemos añadir en él

Servicios de la plataforma Android

16Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

una referencia al proyecto con las librerías. Para ello:

1. Pulsaremos con el botón derecho sobre el proyecto y seleccionaremos Properties.2. Seleccionamos el apartado Android.3. En el apartado Library pulsamos el botón Add ...4. Seleccionamos el proyecto con las librerías de Google y pulsamos Ok.5. Pulsamos sobre el botón Apply y tras esto Ok para cerrar el diálogo.

En nuestra aplicación deberemos tener en cuenta que es posible que los servicios deGoogle no estén disponibles en el dispositivo. Esto podemos comprobarlo con elsiguiente método:

int status = GooglePlayServicesUtil.isGooglePlayServicesAvailable();

if(status == ConnectionResult.SUCCESS) {// Los servicios están disponibles

}

1.5.2. Configuración de la aplicación

Una vez configurada la librería de servicios de Google, podemos utilizarla para integrar laversión 2 de los mapas. Para ello en primer lugar deberemos generar una clave dedesarrollador asociada a nuestra aplicación y a nuestro certificado:

https://developers.google.com/maps/documentation/android/start#the_google_maps_api_key

Una vez obtenida la clave, deberemos configurar el fichero AndroidManifest.xml. Enprimer lugar añadiremos una serie de permisos necesarios:

<uses-permission android:name="android.permission.INTERNET"/><uses-permission android:name=

"android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name=

"com.google.android.providers.gsf.permission.READ_GSERVICES"/><uses-permission android:name=

"android.permission.ACCESS_COARSE_LOCATION"/><uses-permission android:name=

"android.permission.ACCESS_FINE_LOCATION"/>

Hay que añadir un permiso adicional propio (en lugar de es.ua.jtech deberemos ponerel paquete de nuestra aplicación):

<permissionandroid:name="es.ua.jtech.permission.MAPS_RECEIVE"android:protectionLevel="signature"/>

<uses-permission android:name="es.ua.jtech.permission.MAPS_RECEIVE"/>

La versión 2 de los mapas de Google necesita que los dispositivos soporten Open GL ES2.0, por lo que esto también debe ser indicado en el manifest:

<uses-featureandroid:glEsVersion="0x00020000"android:required="true"/>

Servicios de la plataforma Android

17Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

También deberemos especificar la clave de desarrollador, introduciendo la siguienteetiqueta justo antes de </application>:

<meta-dataandroid:name="com.google.android.maps.v2.API_KEY"android:value="pon_aqui_tu_clave"/>

1.5.3. Integración de los mapas

Una vez tenemos configurada la aplicación, podremos integrar en ella los mapas deGoogle. Para ello podemos añadir un fragmento con el mapa a alguna de nuestrasactividades:

<?xml version="1.0" encoding="utf-8"?><fragment xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/map"android:layout_width="match_parent"android:layout_height="match_parent"class="com.google.android.gms.maps.MapFragment"/>

Con esto podremos ver el mapa integrado en nuestra actividad. En caso de queestuviésemos utilizando la librería de compatibilidad el código anterior no funcionará. Enese caso deberemos utilizar la clase SupportMapFragment:

<?xml version="1.0" encoding="utf-8"?><fragment xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/map"android:layout_width="match_parent"android:layout_height="match_parent"class="com.google.android.gms.maps.SupportMapFragment"/>

Servicios de la plataforma Android

18Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

2. Ejercicios de fragmentos y compatibilidad

Antes de empezar a crear los proyectos, debes descargarte las plantillas desde bitbucket.Para ello:

1. Entraremos en nuestra cuenta de bitbucket.org, seleccionaremos el repositorio gitexpertomoviles/serv-android-expertomoviles (del que tendremos únicamentepermisos de lectura), y haremos un Fork de dicho repositorio en nuestra cuenta, paraasí tener una copia propia del repositorio con permisos de administración.

2. Para evitar que bitbucket nos dé un error por sobrepasar el número de usuariospermitidos, debemos ir al apartado Access management de las preferencias delrepositorio que acabamos de crear y eliminar los permisos de lectura para el grupoEstudiantes (tendremos estos permisos concedidos si al hacer el Fork hemosespecificado que se hereden los permisos del proyecto original). Los únicos permisosque debe tener nuestro repositorio deben ser para el propietario (owner) y para elusuario Experto Moviles.

3. Una vez tenemos nuestra copia del repositorio con las plantillas correctamenteconfiguradas en bitbucket, haremos un clone en nuestra máquina local:git clone https://[usr]:bitbucket.org/[usr]/serv-android-expertomoviles

4. De esta forma se crea en nuestro ordenador el directorioserv-android-expertomoviles y se descargan en él las plantillas para los ejerciciosdel módulo y un fichero .gitignore. Además, ya está completamente configurado yconectado con nuestro repositorio remoto, por lo que lo único que deberemos hacerserá subir los cambios conforme realicemos los ejercicios, utilizando los siguientescomandos:git add .git commit -a -m "[Mensaje del commit]"git push origin master

2.1. Lector de noticias (1,5 puntos)

Vamos a crear una aplicación que nos permita leer una serie de noticias obtenidas de unRSS. En primer lugar crearemos la interfaz utilizando fragmentos, para así adaptarla ateléfonos y tablets. En la aplicación encontramos dos fragmentos: MainFragment, quecontiene una lista de noticias, y DetailFragment, que contiene los detalles de la noticiaseleccionada. Deberemos:

a) En primer lugar vamos a definir el contenido del fragmento DetailFragment. En sumétodo onCreateView deberemos cargar su contenido del layoutdetail_fragment.xml.

b) Vamos a crear el layout de la actividad para tablets. En layout-large editaremos elcontenido de activity_main.xml para introducir en él los dos fragmentos definidos enla aplicación. El fragmento principal se introducirá al comienzo del LinearLayout

Servicios de la plataforma Android

19Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

principal, con peso (layout-weight) 1. El fragmento con los detalles se introducirá en elLinearLayout secundario, con altura wrap_content. Con esto podremos probar laaplicación en un simulador de tipo tablet para ver la composición que hemos creado.

c) Vamos a hacer ahora que la aplicación también funcione en móviles. Para ello, en elmétodo onCreate de MainActivity comprobaremos si estamos utilizando el layout demóviles (miraremos si existe el contenedor R.id.fragment_container) y en tal casocrearemos el fragmento principal y lo añadiremos al contenedor.

d) Por último, vamos a comunicar los dos fragmentos. La comunicación será distintasegún el tipo de dispositivo. En un móvil haremos una transición, mientras que en tabletscomunicaremos los dos fragmentos que ya se muestran en pantalla a través de laactividad. En el método onNoticiaSelected de MainActivity comprobaremos enprimer lugar si el fragmento con los detalles está accesible (en ese caso sabremos queestamos en un tablet). En caso de que exista, simplemente deberemos actualizar en él lanoticia llamando a su método updateNoticia. En caso contrario, deberemos crear unnuevo fragmento de detalles, le pasaremos un parámetro "noticia" de tipoSerializable en el bundle con la noticia seleccionada, y lo mostraremos en pantalla.

2.2. Carga de noticias (1 punto)

Vamos a actualizar el lector de noticias para que ahora lea las noticias a través de Internetutilizando para ello un loader. Deberemos:

a) En MainFragment definimos una clase interna de tipo AsyncTaskLoader que seencargue de cargar las noticias (puede utilizar para ello el métodoDataSource.loadNoticias()). Este loader debe proporcionar objetos de tipoList<Noticia>.

b) Hacer que dicha clase implemente el listener LoaderCallbacks, e implementar losmétodos necesarios. Al crearse el loader se deberá proporcionar una instancia de la claseAsyncTaskLoader definida en el punto anterior. Al obtenerse datos tendremos queintroducirlos en el adapter (vaciándolo previamente), y al reiniciar el loader simplementevaciaremos el adapter.

c) Por último, modificaremos el método onCreate de la clase anterior para que ya nointroduzca ningún dato manualmente en el adapter, y que en su lugar lo que haga seaponer en marcha el loader. Con esto la aplicación deberá descargar las noticias del RSSproporcionado.

2.3. Servicios de Google (0,5 puntos)

En este ejercicio vamos a añadir al proyecto anterior un mapa de Google utilizando losservicios de Google Play. Para ello deberemos:

a) Descargar la librería de servicios de Google Play utilizando SDK Manager. Importar la

Servicios de la plataforma Android

20Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

librería descargada en Eclipse, y añadir una dependencia de nuestro proyecto a ella.

b) Obtenemos la clave de la API para acceder a Google Maps v2 accediendo a lasiguiente dirección:

https://developers.google.com/maps/documentation/android/start

c) Introducimos en el fichero AndroidManifest.xml la información necesaria:

• Indicar que es necesario contar con OpenGLES 2.0.• Solicitar los permisos necesarios.• Indicar la clave de la API obtenida.

d) Lo último que haremos será añadir el mapa a nuestra aplicación. Simplemente loañadiremos como fragmento al fichero activity_main.xml de layout-large

(utilizando la librería de soporte). En caso de usar una tablet, veremos el mapa bajo losdetalles de la noticia.

NotaLos servicios de Google Play no funcionan en el emulador, sólo en un móvil real. Lo único quepodremos ver en el emulador es un botón que nos invita a instalarnos Google Play, pero nopodremos hacerlo.

Servicios de la plataforma Android

21Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

3. Agenda y calendario

En esta sesión vamos a ver cómo utilizar proveedores de contenidos que nos den acceso ainformación personal del usuario manejada por el dispositivo, como es el caso de suagenda de contactos y sus calendarios. La forma de acceder a esta información ha idovariando a lo largo de las diferentes versiones de Android, hasta estandarizarsecompletamente en ICS (Ice Cream Sandwich, Android 4.0). Veremos cómo mantener lacompatibilidad con versiones anteriores.

3.1. Agenda de contactos

El proveedor de la agenda de contactos se encuentra estructurado en tres tablas de datos:

• Contacts: Contiene la lista de contactos únicos. Un contacto puede tener variascuentas (Google, Twitter, etc). Esta tabla unifica todas esas cuentas en una únicaentrada.

• RawContacts: En esta tabla tenemos entradas para cada cuenta concreta de uncontacto. Una única entrada en Contacts puede estar relacionada con varias entradasen RawContacts, para cada cuenta diferente del usuario.

• Data: Esta es la tabla donde realmente están almacenados los datos de cada cuenta deusuario. Para cada cuenta (almacenada en RawContacts) tendremos un conjunto dedatos almacenados en la tabla Data.

Servicios de la plataforma Android

22Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Tablas de contactos

El acceso al proveedor de la agenda de contactos se hace mediante una serie de constantesdefinidas en las siguientes subclases de ContactsContract, cada una de ellas referida auna de las tablas anteriores:

• ContactsContract.Contacts

• ContactsContract.RawContacts

• ContactsContract.Data

En primer lugar, para que nuestra aplicación pueda acceder a los contactos (leerlos y/omodificarlos) deberemos solicitar el permiso correspondiente en elAndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CONTACTS"><uses-permission android:name="android.permission.WRITE_CONTACTS">

3.1.1. Carga de contactos

Para leer los contactos almacenados en el dispositivos utilizaremos la claseContentsResolver, al igual que para cualquier otro tipo de contenidos. Por ejemplo,podríamos leer todos los contactos de la siguiente forma:

ContentResolver cr = getContentResolver();Cursor cursor = cr.query(ContactsContract.Contacts.CONTENT_URI,

null, null, null, null);

Servicios de la plataforma Android

23Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

En lugar de seleccionar todos los datos podemos indicar la proyección o selección quenos interese mediante constantes de ContactsContract.Contacts.

mProjection = new String[] {ContactsContract.Contacts._ID,ContactsContract.Contacts.DISPLAY_NAME_PRIMARY

};

mProfileCursor =getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,

mProjection, null, null, null);

En lugar de cargar el cursor directamente, también podríamos utilizar un loader con unCursorAdapter para cargar los datos del proveedor de contenidos.

@Overridepublic Loader<Cursor> onCreateLoader(int id, Bundle args) {

String[] projection = {ContactsContract.Contacts._ID,ContactsContract.Contacts.DISPLAY_NAME_PRIMARY

};

String sortOrder = ContactsContract.Contacts.DISPLAY_NAME_PRIMARY +" ASC";

return new CursorLoader(getApplicationContext(),ContactsContract.Contacts.CONTENT_URI,projection, null, null, sortOrder);

}

El loader CursorLoader se encarga de cargar los datos de un proveedor de contenidos deforma asíncrona (internamente utilizará ContentResolver).

NotaEn versiones anteriores a Android 2.0 (API 5) el acceso al proveedor de contenidos de la agendade contactos se hacía mediante constantes de la clase Contacts.People. Si queremos haceruna aplicación compatible deberemos tener esto en cuenta y utilizar una u otra en función de laversión actual.

3.1.2. Acceso a datos de los contactos

Una vez tenemos el identificador de un contacto, podríamos obtener todas sus cuentasasociadas (raw contacts). Utilizaremos para ello las constantes definidas enContactsContract.RawContacts:

Cursor c = getContentResolver().query(ContactsContract.RawContacts.CONTENT_URI,new String[] { ContactsContract.RawContacts._ID,

ContactsContract.RawContacts.ACCOUNT_TYPE,ContactsContract.RawContacts.ACCOUNT_NAME },

ContactsContract.RawContacts.CONTACT_ID + "=?",new String[] { String.valueOf(contactId) },null);

Servicios de la plataforma Android

24Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Podemos obtener los datos un contacto utilizando constantes deContactsContract.Data para el acceso a la URI y a datos genéricos, y a constantes declases internas de ContactsContract.CommonDataKinds para acceder a tipos de datoscomunes. Por ejemplo, podemos leer los números de teléfono asociados a una cuentadada:

Cursor c = getContentResolver().query(ContactsContract.Data.CONTENT_URI,new String[] { ContactsContract.Data._ID,

ContactsContract.CommonDataKinds.Phone.NUMBER,ContactsContract.CommonDataKinds.Phone.TYPE,ContactsContract.CommonDataKinds.Phone.LABEL},

ContactsContract.Data.RAW_CONTACT_ID + "=?" + " AND "+ ContactsContract.Data.MIMETYPE + "='" +ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'",

new String[] { String.valueOf(rawContactId) },null);

También podemos obtener los teléfonos de todas las cuentas de un contacto dado:

Cursor c = getContentResolver().query(ContactsContract.Data.CONTENT_URI,new String[] { ContactsContract.Data._ID,

ContactsContract.CommonDataKinds.Phone.NUMBER,ContactsContract.CommonDataKinds.Phone.TYPE,ContactsContract.CommonDataKinds.Phone.LABEL},

ContactsContract.Data.CONTACT_ID + "=?" + " AND "+ ContactsContract.Data.MIMETYPE + "='" +ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE + "'",

new String[] { String.valueOf(contactId) },null);

3.1.3. Inserción de contactos

No podemos añadir un contacto directamente, sino que deberemos añadir una cuenta (rawcontact). Si al añadir la cuenta ya existe un contacto con el identificador proporcionado,la cuenta quedará asociada a dicho contacto. En caso de no ser así, el contacto se crearáde forma automática.

Podemos añadir una cuenta de la siguiente forma:

ContentValues values = new ContentValues();values.put(ContactsContract.RawContacts.ACCOUNT_TYPE, tipo);values.put(ContactsContract.RawContacts.ACCOUNT_NAME, nombre);Uri rawContactUri = getContentResolver()

.insert(ContactsContract.RawContacts.CONTENT_URI, values);

Tras crear la cuenta, obtendremos una URI que nos dará acceso a ella. Podemos extraerde ella el identificador de la cuenta que se acaba de insertar:

long rawContactId = ContentUris.parseId(rawContactUri);

Tras insertar la cuenta, podremos añadir a ella distintos elementos de datos:

values.clear();values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);values.put(

Servicios de la plataforma Android

25Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);

values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,"Pepe García");

getContentResolver().insert(ContactsContract.Data.CONTENT_URI,values);

Sin embargo, la forma recomendada de realizar las inserciones es metiante una operaciónen batch que realice todas las operaciones de forma conjunta. Para ello crearemos unalista de objetos ContentProviderOperation, que definirán las operaciones que vamos arealizar en batch:

ArrayList<ContentProviderOperation> ops =new ArrayList<ContentProviderOperation>();

ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI).withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, accountType).withValue(ContactsContract.RawContacts.ACCOUNT_NAME, accountName).build());

ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI).withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0).withValue(

ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)

.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,"Pepe García")

.build());

getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);

Podemos ver que como la segunda operación depende del identificador generado por laprimera (RAW_CONTACT_ID), este valor se lo pasamos con el métodowithValueBackReference. Con ello le indicamos que como valor tome el generado poruna operación anterior. Para indicar de qué operación queremos obtener el valor generadole proporcionamos como segundo parámetro el índice de dicha operación en la lista (en elcaso anterior se le proporciona 0 porque nos interesa el resultado de la primeraoperación).

En versiones anteriores a la 2.0 (API 5) la inserción de contactos se realiza de la siguienteforma:

// Inserta contacto (previo a Android 2.0)ContentValues cv = new ContentValues();cv.put(Contacts.People.NAME, nombre);Uri uri = getContentResolver().insert(Contacts.People.CONTENT_URI, cv);

// Añade un teléfonoUri phoneUri = Uri.withAppendedPath(uri,

Contacts.People.Phones.CONTENT_DIRECTORY);cv.clear();cv.put(Contacts.People.Phones.TYPE, tipo);cv.put(Contacts.People.Phones.NUMBER, telefono);getContentResolver().insert(phoneUri, cv);

Servicios de la plataforma Android

26Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

// Añade un e-mailUri emailUri = Uri.withAppendedPath(uri,

Contacts.People.ContactMethods.CONTENT_DIRECTORY);cv.clear();cv.put(Contacts.People.ContactMethods.KIND, Contacts.KIND_EMAIL);cv.put(Contacts.People.ContactMethods.DATA, email);cv.put(Contacts.People.ContactMethods.TYPE,

Contacts.People.ContactMethods.TYPE_WORK);getContentResolver().insert(emailUri, cv);

3.2. Calendario

El acceso al calendario no se ha estandarizado en Android hasta la versión 4.0 (Ice CreamSandwich). Anteriormente se debían especificar las URI y los campos del proveedor sinayuda de ninguna constante. Vamos a ver las dos formas de acceder, para poder mantenerla compatibilidad con versiones anteriores.

Con Android 4.0 el acceso a calendarios se realizará mediante clases internas deCalendarContract. En ellas podemos acceder a las distintas URIs que nos dan acceso alas tablas que contienen los datos de los calendarios. En versiones anteriores deberemosescribir las URIs directamente:

Versión URI

Hasta Android 2.1 "content://calendar/"

A partir de Android 2.2 "content://com.android.calendar/"

A partir de Android 4.0 CalendarContract.CONTENT_URI

Para poder acceder a los calendarios antes deberemos solicitar los permisoscorrespondiente en AndroidManifest.xml:

<uses-permission android:name="android.permission.READ_CALENDAR" /><uses-permission android:name="android.permission.WRITE_CALENDAR" />

El proveedor de calendarios nos da acceso a multiples calendarios, almacenados en latabla Calendars. Cada calendario contiene una serie de eventos, contenidos en la tablaEvents, y estos eventos pueden contener recordatorios, que se almacenan en la tablaReminders.

Servicios de la plataforma Android

27Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Tablas de calendarios

3.2.1. Selección del calendario

En el sistema podemos tener acceso a varios calendarios, por lo que lo primero quedeberemos hacer es seleccionar el calendario con el que queramos trabajar. Para ello apartir de Android 4.0 tenemos la clase CalendarContract.Calendars, que contiene lasconstantes necesarias para acceder a la lista de calendario, como por ejemploCONTENT_URI.

En versiones anteriores deberemos especificar la URI manualmente:

Versión URI

Hasta Android 2.1 "content://calendar/calendars"

A partir de Android 2.2 "content://com.android.calendar/calendars"

A partir de Android 4.0 CalendarContract.Calendars.CONTENT_URI

En Android 4.0 podremos acceder a los calendarios con:

Cursor cursor = getContentResolver().query(Uri.parse(contentUri),new String[] { CalendarContract.Calendars._ID,

CalendarContract.Calendars.CALENDAR_DISPLAY_NAME },null, null, null);

Sin embargo, en versiones anteriores deberemos especificar los campos manualmentetambién:

Servicios de la plataforma Android

28Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Cursor cursor = getContentResolver().query(Uri.parse(contentUri),new String[] { "_id", "displayname" }, null, null, null);

AdvertenciaHay que destacar que los nombres de los campos cambian en Android 4.0, por lo que el códigode versiones antiguas dejará de funcionar. Por ejemplo, en lugar de "displayname" se utiliza"calendar_displayName".

Si queremos conseguir una aplicación compatible con todas las versiones, deberemosdetectar la versión que se está utilizando y en función de ésta ajustar los nombres de loscampos:

Uri calendarsUri;String calendarsId;String calendarsName;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {calendarsUri = CalendarContract.Calendars.CONTENT_URI;calendarsId = CalendarContract.Calendars._ID;calendarsName = CalendarContract.Calendars.CALENDAR_DISPLAY_NAME;

} else {if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {

calendarsUri =Uri.parse("content://com.android.calendar/calendars");

} else {calendarsUri = Uri.parse("content://calendar/calendars");

}calendarsId = "_id";calendarsName = "displayname";

}

De esta forma podemos obtener la lista de calendarios, y dejar que el usuario seleccioneuno de ellos. Una vez seleccionado, podremos acceder a él y modificarlo, para porejemplo añadir nuevos eventos.

3.2.2. Añadir eventos al calendario

La tabla de eventos tiene las siguientes URIs, dependiendo de la versión de Android:

Versión URI

Hasta Android 2.1 "content://calendar/events"

A partir de Android 2.2 "content://com.android.calendar/events"

A partir de Android 4.0 CalendarContract.Events.CONTENT_URI

Para crear un evento deberemos proporcionar la fecha y hora de inicio y de fin (enmilisegundos), su título y descripción, la zona horaria, y el identificador del calendario alque lo queremos añadir:

Date dtStart = // Fecha y hora de inicioDate dtEnd = // Fecha y hora de fin

Servicios de la plataforma Android

29Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

ContentResolver cr = getContentResolver();ContentValues values = new ContentValues();values.put(CalendarContract.Events.DTSTART, dtStart.getTime());values.put(CalendarContract.Events.DTEND, dtEnd.getTime());values.put(CalendarContract.Events.TITLE, "Reunión");values.put(CalendarContract.Events.DESCRIPTION, "Preparación proyecto");values.put(CalendarContract.Events.CALENDAR_ID, calID);values.put(CalendarContract.Events.EVENT_TIMEZONE, "Europe/Madrid");Uri uri = cr.insert(CalendarContract.Events.CONTENT_URI, values);

En caso de utilizar versiones anteriores de Android, se hará de la siguiente forma:

Date dtStart = // Fecha y hora de inicioDate dtEnd = // Fecha y hora de fin

ContentResolver cr = this.getContentResolver();ContentValues values = new ContentValues();values.put("dtstart", dtStart.getTime());values.put("dtend", dtEnd.getTime());values.put("title", "Reunión");values.put("description", "Preparación proyecto");values.put("calendar_id", calID);values.put("eventTimezone", "Europe/Madrid");Uri newEvent = cr.insert(contentUri, values);

3.2.3. Eventos recurrentes

En muchas ocasiones nos interesa agregar un evento que se repite semanalmente duranteun periodo de tiempo (por ejemplo las clases de una asignatura). En este caso seráconveniente añadirlo como evento recurrente, en lugar de añadirlos como eventosindependientes. De esta forma si queremos eliminarlo podremos eliminar la serie enteramediante una única operación.

Para definir un evento recurrente deberemos:

• En DTSTART pondremos la fecha y la hora de inicio del primer evento de la serie, peroen este caso ya no utilizaremos DTEND.

• En lugar de DTEND, deberemos especificar la duración de los eventos de la serie enDURATION. Se especificará mediante el formato RFC5545. Por ejemplo, una duraciónde 90 minutos se especifica con "P90M", y una duración de de dos semanas con"P2W".http://tools.ietf.org/html/rfc5545#section-3.8.2.5

• Por último, deberemos especificar la regla de recurrencia en RRULE. En elladeberemos especificar la frecuencia con la que se repite y el número de repeticiones ofecha de finalización. Por ejemplo, si ponemos "FREQ=WEEKLY;COUNT=10" se repetirá10 veces semanalmente. Si queremos fijar una fecha concreta de finalización, loharemos con el formato "FREQ=WEEKLY;UNTIL=20130525T235959Z". Másinformación sobre el formato de las reglas:http://tools.ietf.org/html/rfc5545#section-3.8.5.3

A continuación mostramos un ejemplo en el que se añade un evento recurrente para lasclases de una asignatura que se imparte semanalmente hasta el fin del cuatrimestre (24 de

Servicios de la plataforma Android

30Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

mayo de 2013):

Date dtStart = // Fecha y hora de inicio del primer eventoDate dtEnd = // Fecha y hora de fin del primer evento

// Duración de cada evento (formato RFC5545)long duracionMillis = dtEnd.getTime() - dtStart.getTime();int duracionMinutos = (int) (duracionMillis / (1000 * 60));String duracion = "P" + duracionMinutos + "M";

// Reglas de la serie de eventosString until = "20130525T235959Z"; // Formato: yyyyMMddThhmmssZString rrule = "FREQ=WEEKLY;UNTIL=" + until;

ContentResolver cr = this.getContentResolver();ContentValues values = new ContentValues();values.put(CalendarContract.Events.CALENDAR_ID, calID);values.put(CalendarContract.Events.TITLE, "Programación I");values.put(CalendarContract.Events.DESCRIPTION, "Asignatura troncal");values.put(CalendarContract.Events.EVENT_LOCATION, "Aula L18");values.put(CalendarContract.Events.EVENT_TIMEZONE, "Europe/Madrid");values.put(CalendarContract.Events.DTSTART, dtStart.getTime());values.put(CalendarContract.Events.DURATION, duracion);values.put(CalendarContract.Events.RRULE, rrule);

// Inserta el evento en el calendarioUri event = cr.insert(CalendarContract.Events.CONTENT_URI, values);

AtenciónEs importante no indicar el campo DTEND, ya que es incompatible con DURATION. Si ponemoslos dos al mismo tiempo, obtendremos un error.

Con versiones anteriores de Android esto mismo se haría de la siguiente forma:

Date dtStart = // Fecha y hora de inicio del primer eventoDate dtEnd = // Fecha y hora de fin del primer evento

// Duración de cada evento (formato RFC5545)long duracionMillis = dtEnd.getTime() - dtStart.getTime();int duracionMinutos = (int) (duracionMillis / (1000 * 60));String duracion = "P" + duracionMinutos + "M";

// Reglas de la serie de eventosString until = "20130525T235959Z"; // Formato: yyyyMMddThhmmssZString rrule = "FREQ=WEEKLY;UNTIL=" + until;

ContentResolver cr = this.getContentResolver();ContentValues values = new ContentValues();values.put("calendar_id", idCalendario);values.put("title", "Programación I");values.put("description", "Asignatura troncal");values.put("eventLocation", "Aula L18");values.put("eventTimezone", "Europe/Madrid");values.put("dtstart", dtStart.getTime());values.put("duration", duracion);values.put("rrule", rrule);

// Inserta el evento en el calendarioUri event = cr.insert(contentUri, values);

Servicios de la plataforma Android

31Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

3.2.4. Añadir recordatorios a los eventos

Puede interesarnos también añadir recordatorios a los eventos, para recibir un aviso en elmomento del evento o con cierta antelación. Estos recordatorios se añadirán a la tablareminders. En primer lugar necesitaremos el identificador del evento al que vamos aañadir el recordatorio. Podemos extraerla de la URI obtenida al crear el evento:

long id = ContentUris.parseId(event);

Añadiremos el recordatorio proporcionando:

• Un método (alerta = 1, alarma = 2, e-mail = 3, sms = 4)• Una antelación en minutos, respecto a la hora en la que está programado el evento.

Las URIs para la tabla de recordatorios son los siguientes:

Versión URI

Hasta Android 2.1 "content://calendar/reminders"

A partir de Android 2.2 "content://com.android.calendar/reminders"

A partir de Android 4.0 CalendarContract.Reminders.CONTENT_URI

Podemos añadir el recordatorio de la siguiente forma:

ContentValues values = new ContentValues();values.put(CalendarContract.Reminders.EVENT_ID, id);values.put(CalendarContract.Reminders.METHOD,

CalendarContract.Reminders.METHOD_ALERT);values.put(CalendarContract.Reminders.MINUTES, 30);

cr.insert(contentUri, values);

La forma de hacer esto en versiones anteriores es la siguiente:

ContentValues values = new ContentValues();values.put("event_id", id);values.put("method", 1);values.put("minutes", 30);

cr.insert(contentUri, values);

Servicios de la plataforma Android

32Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

4. Ejercicios de agenda y calendario

4.1. Acceso a la agenda de contactos (1 punto)

Vamos a crear una aplicación que lea nuestro listado de contactos y lo muestre en unalista utilizando para ello un loader. Podemos encontrar en las plantillas la aplicaciónAgendaContactos que utilizaremos como base. Deberemos:

a) En el método onCreate de ContactosFragment creamos un adapter que muestre en lalista el nombre de los contactos (debemos especificar el nombre del campo adecuado delos contactos al crear el adapter).

b) En el método onCreateLoader creamos y devolvemos un CursorLoader que obtengadel proveedor de contactos el identificador y el nombre de todos los usuarios, y nos losdevuelva ordenados por nombre.

c) Cuando termine la carga de contactos, cambiamos el cursor del adapter por el cursorobtenido tras la carga. En caso de que se reinicie el loader, eliminamos el cursor deladapter.

4.2. Agregar contactos a la agenda (1,5 puntos)

En la aplicación anterior, en MainActivity se define un menú de opciones con una únicaopción que nos permite añadir un contacto a la agenda. En el métodoonOptionsItemSelected, al pulsar la opción anterior, vamos a hacer que añada elcontacto a la agenda con los siguientes datos:

• Nombre: Especialista Móviles• E-mail: [email protected], de tipo WORK (consultar las constantes de

CommonDataKinds.Email)• Teléfono: 965903900, de tipo WORK (consultar las constantes de

CommonDataKinds.Phone)• Tipo de cuenta: Gmail• Haremos que el contacto aparezca como favorito (Atributo STARRED de RawContact)

Todas las operaciones de inserción se deben realizar en batch. Tras ejecutar el códigoanterior, si vamos a la aplicación de contactos podremos ver el contacto que acabamos deañadir en la sección Favoritos.

4.3. Creación de eventos del calendario (0,5 puntos)

En el proyecto CalendarioAsignaturas tenemos una aplicación que muestra un listadode asignaturas, y nos permite añadir sus horarios a nuestro calendario. Vamos a añadir el

Servicios de la plataforma Android

33Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

código que realiza esta tarea:

a) En la clase DialogAsignaturaFragment tenemos el método agregarEvento que seencargará de agregar un evento al calendario. En primer lugar deberemos seleccionar elcalendario en el que añadir los eventos. Para ello obtendremos el listado de todos loscalendarios, y cogeremos automáticamente el primero de la lista (si hay al menos uno).

b) Una vez hayamos visto que selecciona el calendario correctamente, vamos a añadir a élun evento con la primera clase de la asignatura seleccionada. Por ahora añadiremos sóloun único evento sencillo con estos datos.

c) Modificaremos el código anterior para que añada el evento como evento recurrente,con todas las clases de la asignatura (no solo la primera).

d) Finalmente, haremos que los eventos de la asignatura tengan programado unrecordatorio que nos avise con una alarma 30 minutos antes de cada clase.

NotaPara poder probar el acceso a calendario necesitaremos contar con una cuenta Exchange con laque podamos sincronizar calendario en el móvil. En caso de dispositivos reales, nos servirátambién una cuenta de Google en la que tengamos calendarios. En caso del emulador,necesitaremos utilizar una cuenta Exchange, y un emulador ICS o superior.

Servicios de la plataforma Android

34Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

5. Servicios

En Android un Service es un componente que se ejecuta en segundo plano, sininteractuar con el usuario. A parte de los servicios ya disponibles en la plataformaAndroid, cualquier desarrollador puede crear sus propios Service en su aplicación.Cuentan con soporte multitarea real en Android, ya que pueden ejecutar en su propioproceso, a diferencia de los hilos de las Activity, que hasta cierto punto estánconectados con el ciclo de vida de las actividades de Android.

Para comunicarse con los servicios se utiliza el mecanismo de comunicación entreprocesos de Android, Inter-Process Communication (IPC). La interfaz pública de estacomunicación se describe en el lenguaje AIDL.

5.1. Servicios propios

Podemos crear un servicio propio para realizar tareas largas que no requieran lainteracción con el usuario, o bien para proveer de determinado tipo de funcionalidad aotras aplicaciones. Los servicios propios se deben declarar en el AndroidManifest.xmlde la aplicación:

<serviceandroid:name=".MiServicio"android:process=":mi_proceso"android:icon="@drawable/icon"android:label="@string/service_name">

</service>

donde "MiServicio" es el nombre de la clase Java que crearemos con el código delservicio.

Para pedir permiso para utilizar un servicio desde una clase diferente a la que se declaróel servicio tenemos que definir el atributo uses-permission.

Se puede especificar que un servicio debe ejecutarse en un proceso aparte a través delatributo android:process. De esta manera cuenta con su propio hilo y memoria ycualquier operación costosa que pueda tener no afectará al hilo principal donde estaría lainterfaz gráfica. Si no lo declaramos con el atributo android:process, debemos utilizarhilos o AsyncTask para las tareas con procesamiento intensivo.

En el código java se debe crear una clase que herede de la clase Service de forma directao a través de alguna clase hija. Una actividad puede iniciar un servicio a través delmétodo startService() y detenerlo a través de stopService(). Si la actividad necesitainteractuar con el servicio se utiliza el método bindService() del servicio. Esto requiereun objeto ServiceConnection que permite conectar con el servicio y que devuelve unobjeto IBinder. Este objeto puede ser utilizado por la actividad para comunicarse con elservicio.

Servicios de la plataforma Android

35Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

A continuación se muestra el ciclo de ejecución de una actividad iniciada constartService() y de otra creada utilizando bindService():

El ciclo de vida de un servicio de Android.

Al iniciar el servicio se invoca su método onCreate(), y a continuación se invoca elmétodo onStartCommand(Intent, int, int) con la información del Intent

proporcionado por la actividad. Para los servicios que se lanzan desde la aplicaciónprincipal, el método onStartCommand() se ejecuta en ese mismo hilo, el de la interfazgráfica. Es bastante común crear un hilo nuevo y lanzarlo desde el onStartCommand()para realizar el procesamiento en segundo plano. Si se lanza ese hilo, la ejecución sedevuelve rápidamente al método onStartCommand() dejándolo terminar en muy pocotiempo. A través del valor retorno de onStartCommand() podemos controlar elcomportamiento de reinicio.

El método startService() también permite definir una bandera para determinar elcomportamiento del ciclo de vida de los servicios. De esta forma, START_STICKY seutiliza para los servicios que se inician o detienen de forma explícita. Los serviciosiniciados con START_NOT_STICKY se finalizan automáticamente después de que setermina de ejecutar el método onStartCommand(). O START_REDELIVER_INTENT, que es

Servicios de la plataforma Android

36Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

una combinación de los dos anteriores. Debes tomar en cuenta que un servicio se inicia enel hilo (thread) principal de la aplicación, por lo tanto, todas las tareas en tiempo deejecución deben llevarse a cabo en segundo plano. A continuación se detalla elfuncionamiento de cada uno de estos flags:

• Service.START_STICKY es el comportamiento estándar, en este caso el métodoonStartCommand() será invocado cada vez que el servicio sea reiniciado tras serterminado por la máquina virtual. Nótese que en este caso, al reiniciarlo, el Intent quese le pasa por parámetro será null. Service.START_STICKY se utiliza típicamente enservicios que controlan sus propios estados y que se inician y terminan de maneraexplícita con startService() y stopService(). Por ejemplo, servicios quereproduzcan música u otras tareas de fondo.

• Service.START_NOT_STICKY se usa para servicios que se inician para procesaracciones específicas o comandos. Normalmente utilizarán stopSelf() paraterminarse una vez completada la tarea a realizar. Cuando la máquina virtual termineeste tipo de servicios, éstos serán reiniciados sólo si hay llamadas de starService()

pendientes, de lo contrario el servicio terminará sin pasar por onStartCommand().Este modo es adecuado para servicios que manejen peticiones específicas, tales comoactualizaciones de red o polling de red.

• Service.START_REDELIVER_INTENT es una combinación de los dos anteriores demanera que si el servicio es terminado por la máquina virtual, se reiniciará solamentesi hay llamadas pendientes a starService() o bien si el proceso fue matado antes dehacer la llamada a stopSelf(). En este último caso se llamará a onStartCommand()

pasándole el valor inicial del Intent cuyo procesamiento no fue completado. ConService.START_REDELIVER_INTENT nos aseguramos de que el comando cuyaejecución se ha solicitado al servicio sea completado hasta el final.

En versiones anteriores a la 2.0 del Android SDK (nivel 5 de API) había que implementarel método onStart() y era equivalente a sobrecargar onStartCommand() y devolverSTART_STICKY.

El segundo parámetro del método onStarCommand(Intent, int, int) es un entero quecontiene flags. Éstos se utilizan para saber cómo ha sido iniciado el servicio, sus valorespueden ser:

• Service.START_FLAG_REDELIVERY indica que el Intent pasado por parámetro es unreenvío porque la máquina virtual ha matado el servicio antes de ocurrir la llamada astopSelf().

• Service.START_FLAG_RETRY indica que el servicio ha sido reiniciado tras unaterminación anormal. Sólo ocurre si el servicio había sido puesto en el modoService.START_STICKY.

De forma que nos sirven para por ejemplo comprobar si el servicio había sido reiniciado:

public class MiServicio extends Service {@Overridepublic void onCreate() {

Servicios de la plataforma Android

37Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}

@Overridepublic void onDestroy() {

}

@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {

if((flags & START_FLAG_RETRY) != 0){// El servicio se ha reiniciado

} else {// Iniciar el proceso de fondo

}return Service.START_STICKY;

}

@Overridepublic IBinder onBind(Intent arg0) {

return null;}

}

El método onBind() debe ser sobrecargado obligatoriamente y se utiliza para lacomunicación entre procesos.

5.1.1. Iniciar un servicio

Como hemos mencionado, para iniciar un servicio de forma explícita se utiliza el métodostartService() con un Intent, y para detenerlo se utiliza stopService(). Acontinuación se muestra un ejemplo:

ComponentName servicio = startService(new Intent(getApplicationContext(),

MiServicio.class));// ...

stopService(new Intent(getApplicationContext(),servicio.getClass()));

donde la clase ComponentName es una clase genérica que guarda un identificador de uncomponente dado (que puede ser de diferente tipo: Activity, Service, BroadcastReceiver, oContentProvider). Este identificador posteriormente es utilizado para detener el servicio.

A continuación vamos a ver otras dos formas de iniciar servicios: al arrancar el sistemaoperativo o arrancarlos como servicios prioritarios.

5.1.1.1. Iniciar al arrancar

Hay aplicaciones que necesitan registrar un servicio para que se inicie al arrancar elsistema operativo. Para ello hay que registrar un BroadcastReceiver al evento delsistema android.intent.action.BOOT_COMPLETED. En el AndroidManifest tendríamos:

<uses-permissionandroid:name="android.permission.RECEIVE_BOOT_COMPLETED" />

Servicios de la plataforma Android

38Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

<application android:icon="@drawable/cw"android:label="@string/app_name">

<receiver android:name=".OnBootReceiver"><intent-filter>

<actionandroid:name="android.intent.action.BOOT_COMPLETED" />

</intent-filter></receiver>

</application>

La actividad sobrecargaría el método onReceive():

public class MiReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {

Intent servicio = new Intent(context,MiServicio.class);

context.startService(servicio);}

}

Si la aplicación está instalada en la tarjeta SD, entonces no estará disponible en cuantoarranque el sistema. En este caso hay que registrarse para el eventoandroid.intent.action.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE. A partir deAndroid 3.0 el usuario debe haber ejecutado la aplicación al menos una vez antes de queésta pueda recibir el evento BOOT_COMPLETED.

5.1.1.2. Servicios prioritarios

Es posible arrancar servicios con la misma prioridad que una actividad que esté en elforeground para evitar que Android pueda matarlos por necesidad de recursos. Esto espeligroso porque si hay muchos servicios en foreground se degrada el rendimiento delsistema. Por esta razón al iniciar un servicio en el foreground se debe avisar al usuario.Este tipo de servicios se suelen utilizar solamente cuando el servicio es crítico y no sepuede detener. Por ejemplo, para una aplicación de reproductor de música que sigareproduciendo el sonido aun cuando se cambie a otra aplicación.

Los servicios de foreground se inician realizando desde dentro del servicio una llamada almétodo startForeground():

// 1º Creamos la notificación para avisar al usuarioint NOTIFICATION_ID = 1;Intent intent = new Intent(this, MiActividad.class);PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, intent,0);Notification notification = new Notification(R.drawable.icon,

"Servicio prioritario iniciado",System.currentTimeMillis());

notification.setLatestEventInfo(this,"Servicio", "Servicio iniciado", pendingIntent);

notification.flags = notification.flags |Notification.FLAG_ONGOING_EVENT; //Mientras dure

// 2ª Pasamos el servicio al foreground y avisamos al usuariostartForeground(NOTIFICATION_ID, notification);

Servicios de la plataforma Android

39Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Esta notificación debe durar mientras el servicio esté en ejecución. Se puede volver delforeground con el método stopForeground(). En general los servicios no deben seriniciados en el foreground.

5.1.2. Servicios y AsyncTask

Las operaciones lentas de un servicio deben realizarse en un hilo aparte. Esto evitaráralentizar la actividad o hilo principal. La manera más cómoda suele ser a través de unaAsyncTask. En el siguiente ejemplo un servicio utiliza una AsyncTask para realizar unacuenta desde 1 hasta 100.

public class MiCuentaServicio extends Service{

MiTarea miTarea; // Puntero al AsyncTask

@Overridepublic int onStartCommand(Intent intent,

int flags, int startId) {Log.i("SRV", "onStartCommand");miTarea.execute(); // Iniciamos la tareareturn Service.START_STICKY;

}

@Overridepublic void onCreate() {

super.onCreate();Toast.makeText(this,"Servicio creado ...",

Toast.LENGTH_LONG).show();Log.i("SRV","onCreate");miTarea = new MiTarea(); // Creamos el AsyncTask

}

@Overridepublic void onDestroy() {

super.onDestroy();Toast.makeText(this,"Servicio destruido ...",

Toast.LENGTH_LONG).show();Log.i("SRV","Servicio destruido");miTarea.cancel(true); // Detenemos la tarea

}

@Overridepublic IBinder onBind(Intent arg0){

return null;}

// Clase privada para la tarea...// param type: [input, progress report, result type]private class MiTarea

extends AsyncTask<String, String, String>{

private int i;boolean cancelado;

@Overrideprotected void onPreExecute() {

super.onPreExecute();i = 1;cancelado = false;

Servicios de la plataforma Android

40Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}

@Overrideprotected String doInBackground(String... params) {

for(; i<100; i++){Log.i("SRV",

"AsyncTask: Cuento hasta "+i);publishProgress(""+i); //para actualizar

onProgressUpdatetry {

Thread.sleep(5000);} catch (InterruptedException e) {

e.printStackTrace();}if(cancelado)

break;}return null;

}

@Overrideprotected void onProgressUpdate(String... values) {

// Actualizar las notificaciones UIToast.makeText(getApplicationContext(),

"Cuento hasta "+values[0],Toast.LENGTH_SHORT).show();

}

@Overrideprotected void onCancelled() {

super.onCancelled();cancelado = true;

}

}}

Para completar el ejemplo, el código de una actividad con dos botones que inicien ydetenga este servicio tendría el siguiente aspecto:

public class Main extends Activity{

Main main; // Puntero al contexto

@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.main);main = this;

((Button)findViewById(R.id.ButtonStart)).setOnClickListener(new OnClickListener() {

@Overridepublic void onClick(View v) {

startService(new Intent(main,MiCuentaServicio.class));

}});

((Button)findViewById(R.id.ButtonStop)).setOnClickListener(new OnClickListener() {

@Overridepublic void onClick(View v) {

stopService(new Intent(main,MiCuentaServicio.class));

Servicios de la plataforma Android

41Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}});

}}

5.2. Broadcast receiver

Un BroadcastReceiver es una especie de receptor de los eventos que produce el sistemaoperativo Android. Típicamente, un Broadcast Receiver se utiliza para mostrarnotificaciones de los eventos que ocurren en nuestro teléfono móvil, como por ejemplocuando se encuentra una red wifi, se agota la batería, etc.

5.2.1. Declaración y registro en el Manifest

Un receptor de broadcast es una clase que recibe Intents generados a través del métodoContext.sendBroadcast(). La clase debe heredar de BroadcastReceiver y debeimplementar el método onReceive(). Sólo durante la ejecución de este método el objetoestará activo, por lo tanto no se puede utilizar para hacer ninguna operación asíncrona.Concretamente, no se podría mostrar un diálogo ni realizar un bind a un servicio. Lasalternativas serían usar el NotificationManager en el primer caso yContext.startService() en el segundo.

La clase que herede de BroadcastReceiver tiene que estar declarada en elAndroidManifest.xml. También se declaran los Intent que la clase recibirá:

<application><!-- [...] -->

<receiverandroid:name=".paquete.MiBroadcastReceiver">

<intent-filter><action

android:name="android.intent.ACTION_TIMEZONE_CHANGED" /><action android:name="android.intent.ACTION_TIME" />

</intent-filter></receiver>

</application>

Siguiendo con el ejemplo, el código de la clase Java "MiBroadcastReceiver" que hemosañadido al manifest, recibiría un intent al cambiar la hora (debido a un ajuste del reloj) oal cambiar la zona horaria:

public class MiBroadcastReceiver extends BroadcastReceiver{

@Overridepublic void onReceive(Context context, Intent intent){

String action = intent.getAction();if (action.equals(Intent.ACTION_TIMEZONE_CHANGED)

|| action.equals(Intent.ACTION_TIME_CHANGED)){

// Ejemplo: Actualizar nuestro// Widget dependiente de la hora

}

Servicios de la plataforma Android

42Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}}

Otro ejemplo típico es el de recibir el intent de una llamada de teléfono entrante. Habríaque declarar el intent android.intent.action.PHONE_STATE en elAndroidManifest.xml y declarar la clase Java BroadcastReceiver con el siguientecódigo:

public class LlamadaReceiver extends BroadcastReceiver{

@Overridepublic void onReceive(Context context, Intent intent) {

Bundle extras = intent.getExtras();if (extras != null) {

String state =extras.getString(TelephonyManager.EXTRA_STATE);

if(state.equals(TelephonyManager.EXTRA_STATE_RINGING)) {

String phoneNumber = extras.getString(TelephonyManager.EXTRA_INCOMING_NUMBER);

Log.i("DEBUG", phoneNumber);}

}}

}

Para probar este tipo de eventos en el emulador podemos abrir la vista "EmulatorControl" (que está en Window > Show view > Other..., y en la ventana que se abreelegimos "Emulator Control" dentro de la sección "Android"). Este nos permite, entreotras opciones, realizar llamadas, enviar mensajes al emulador, o indicarle la red o laposición GPS actual.

Hay intents para toda clase de eventos, como por ejemplo pulsar el botón de la cámara,batería baja, o incluso cuando se instala una nueva aplicación. Los componentes propiostambién pueden enviar intents de tipo Broadcast. A continuación se incluyen algunos delos eventos que podemos capturar:

• android.provider.Telephony.SMS_RECEIVED Evento de mensaje recibido.• android.intent.action.PHONE_STATE Evento de llamadas recibidas.• android.intent.action.AIRPLANE_MODE Evento modo vuelo.• android.intent.action.BATTERY_LOW Evento batería baja.• android.intent.action.BOOT_COMPLETED Evento de inicio del sistema operativo.• android.intent.action.SCREEN_OFF Evento bloqueo de pantalla.• android.intent.action.SCREEN_ON Evento desbloqueo de pantalla.• android.bluetooth.intent.action.DISCOVERY_STARTED Evento comienzo de

escáner Bluetooth.• android.bluetooth.intent.action.ENABLED Evento Bluetooth habilitado.

5.2.2. Registro dinámico

Un BroadcastReceiver también se puede registrar para capturar un determinado tipo deintent de forma dinámica, en lugar de hacerlo a través del AndroidManifest.xml

Servicios de la plataforma Android

43Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

utilizando la sección <intent-filter>. La forma de realizar esto es la siguiente:

MiBroadcastReceiver intentReceiver = new MiBroadcastReceiver();

IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CAMERA_BUTTON);intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); // Añadimos otroevento

// Activamos el registroregisterReceiver(intentReceiver, intentFilter);

// Posteriormente para eliminar el registro...unregisterReceiver(intentReceiver);

Es recomendable registrar el receiver en el método onResume() y desregistrarlo en elmétodo onPause() de la actividad.

5.3. PendingIntents y servicios del sistema

Al pasar un PendingIntent a otra aplicación, por ejemplo, el Notification Manager, elAlarm Manager o aplicaciones de terceros, se le está dando permiso para ejecutardeterminado código que nosotros definimos en nuestra propia aplicación.

Por ejemplo, vamos a colocar el código a ejecutar en un BroadcastReceiver:

public class MiBroadcastReceiver extends BroadcastReceiver{

@Overridepublic void onReceive(Context context, Intent intent) {

Toast.makeText(context,"Otra aplicación es la causa de esta tostada.",

Toast.LENGTH_LONG).show();// Código que queremos ejecutar// a petición de la otra aplicación// ...

}}

Sin olvidar declararlo en el AndroidManifest.xml, antes de cerrar la etiqueta de</application>:

<receiver android:name="MiBroadcastReceiver">

En este ejemplo se utiliza un PendingIntent para que el Alarm Manager pueda ejecutarel código de nuestro BroadcastReceiver. Este código pertenecería a algún método de laActivity:

Intent intent = new Intent(this, MiBroadcastReceiver.class);PendingIntent pendingIntent = PendingIntent.getBroadcast(

this.getApplicationContext(), 0, intent, 0);AlarmManager alarmManager = (AlarmManager)

getSystemService(ALARM_SERVICE);alarmManager.set(AlarmManager.RTC_WAKEUP,

System.currentTimeMillis()+5000, pendingIntent);

Android ofrece una serie de servicios predefinidos a los que se accede a través del métodogetSystemService(String). La cadena debe ser una de las siguientes constantes:

Servicios de la plataforma Android

44Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

WINDOW_SERVICE, LAYOUT_INFLATER_SERVICE, ACTIVITY_SERVICE, POWER_SERVICE,ALARM_SERVICE, NOTIFICATION_SERVICE, KEYGUARD_SERVICE, LOCATION_SERVICE,SEARCH_SERVICE, VIBRATOR_SERVICE, CONNECTIVITY_SERVICE, WIFI_SERVICE,INPUT_METHOD_SERVICE, UI_MODE_SERVICE, DOWNLOAD_SERVICE.

En general los servicios obtenidos a través de esta API pueden estar muy relacionadoscon el contexto (Context) en el cuál fueron obtenidos, por tanto no conviene compartirloscon contextos diferentes (actividades, servicios, aplicaciones, proveedores).

5.3.1. AlarmManager para programar servicios

AlarmManager no sólo se puede utilizar para programar otros servicios sino que en lamayoría de los casos se debe utilizar. Un ejemplo sería el de programar un servicio quecomprueba si hay correo electrónico o RSS. Se trata de una tarea periódica y el serviciono tiene por qué estar en ejecución todo el tiempo. De hecho un servicio que sólo se iniciacon startService() y nunca se finaliza con stopService() se considera un "antipatrónde diseño" en Android.

Si un servicio cumple ese antipatrón, es posible que Android lo mate en algún momento.Si un servicio de verdad requiere estar todo el tiempo en ejecución, como por ejemplo,uno de voz IP (require estar conectado y a la escucha todo el tiempo), entonces habría queiniciarlo como servicio foreground para que Android no lo mate nunca.

El AlarmManager es la analogía del cron de Unix. La diferencia importante es que cronsiempre continúa con su anterior estado mientras que AlarmManager empieza en blancocada vez que arranca. Por tanto estamos obligados a volver a registrar nuestros serviciosdurante el arranque.

5.4. Comunicación entre procesos

La comunicación entre procesos se puede realizar tanto invocando métodos comopasando objetos con información. Para pasar información de una actividad a un serviciose pueden utilizar los BroadcastReceiver, pasando información extra (Bundle) en elIntent que se utiliza para iniciar un servicio, o bien haciendo un binding al servicio.

Vamos a empezar por el binding de una actividad a un servicio:

5.4.1. Atar actividades a servicios (binding)

Un tipo posible de comunicación es el de invocar directamente los métodos de un servicioa través de una referencia a este.

Atar o enlazar (to bind) una actividad a un servicio consiste en mantener una referencia ala instancia del servicio, permitiendo a la actividad realizar llamadas a métodos delservicio igual que se harían a cualquier otra clase accesible desde la actividad.

Servicios de la plataforma Android

45Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Para que un servicio de soporte a binding hay que implementar su método onBind(). Éstedevolverá una clase binder que debe implementar la interfaz IBinder. La implementaciónnos obliga a definir el método getService() del binder. En este método devolveremos lainstancia al servicio. A continuación se incluye un ejemplo de un servicio de este tipo:

public class MiServicio extends Service {

private final IBinder binder = new MiBinder();

@Overridepublic IBinder onBind(Intent intent){

return binder;}

public class MiBinder extends Binder {MiServicio getService() {

return MiServicio.this;}

}

// ...}

La conexión entre el servicio y la actividad es representada por un objeto de claseServiceConnection. En la actividad que va a realizar el binding, hay que implementaruna nueva clase hija, sobrecargando el método onServiceConnected() yonServiceDisconnected() para poder obtener la referencia al servicio una vez que seha establecido la conexión:

private MiServicio servicio; //La referencia al servicio

private ServiceConnection serviceConnection =new ServiceConnection()

{@Overridepublic void onServiceConnected(

ComponentName className, IBinder service) {servicio = ((MiServicio.MiBinder)service).getService();

}@Overridepublic void onServiceDisconnected(ComponentName className) {

servicio = null;}

};

Para realizar el binding hay que pasarle el intent correspondiente al métodobindService(). El intent servirá para poder seleccionar qué servicio devolver.Tendríamos el siguiente código en el método onCreate() de nuestra actividad:

Intent intent = new Intent(MiActividad.this,MiServicio.class);

bindService(intent, serviceConnection,Context.BIND_AUTO_CREATE);

Una vez establecido el binding, todos los métodos y propiedades públicos del servicioestarán disponibles a través del objeto servicio del ejemplo. Podríamos realizar lo quequeramos con él, bien iniciarlo con startService y controlarlo con métodos a través delbinding, o simplemente llamarlo para utilizar sus métodos.

Servicios de la plataforma Android

46Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Para finalizar tendríamos que "desatar" el servicio al finalizar (en el método "onDestroy")pasándole la variable que hemos utilizado para la conexión, en nuestro ejemplo sería"serviceConnection", de la forma:

@Overrideprotected void onDestroy() {

unbindService(connection);}

5.4.2. Inter Process Communication (IPC)

En Android cada aplicación se ejecuta en su propia "caja de arena" y no comparte lamemoria con otras aplicaciones o procesos. Para comunicarse entre procesos Androidimplementa el mecanismo IPC (Inter Process Communication).

El protocolo IPC requiere codificar y descodificar los distintos tipos de datos. Parafacilitar esta parte, Android ofrece la posibilidad de definir los tipos de datos con AIDL(Android Interface Definition Language). De esta manera para comunicarse entre dosaplicaciones o procesos hay que definir el AIDL, a partir de éste implementar un Stubpara la comunicación con un servicio remoto y por último dar al cliente acceso al servicioremoto.

La interfaz de datos se define en un archivo ".aidl". Por ejemplo, el siguiente archivo decódigo estaría almacenado en "/src/es/ua/jtech/IServicio.aidl":

package es.ua.jtech;

interface IServicio {// Los valores pueden ser: in, out, inout.String saludo(in String nombre, in String apellidos);

}

Una vez creado el anterior archivo, la herramienta AIDL generará un archivo ".java", queen nuestro ejemplo sería "/src/es/ua/jtech/IServicio.java". Esta herramienta seencuentra en la carpeta "platform-tools" del SDK. Es posible que el compilador nos digaque la clase AIDL ya existe (pues está en el fichero .aidl y el .java), para solucionar estosimplemente podemos renombrar el fichero ".aidl" a ".aidl.backup", puesto que ya no noshace falta.

En el servicio tenemos que implementar el método onBind() para que devuelva un stubque cumpla la interfaz anteriormente definida (en el IServicio), por ejemplo:

public class Servicio extends Service{

@Overridepublic IBinder onBind(Intent intent){

return new IServicio.Stub() {public int saludo(String nombre, String apellidos)

throws RemoteException {return "Hola " + nombre + " " + apellidos;

}};

Servicios de la plataforma Android

47Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

}// ...

}

Ahora hay que obtener acceso al servicio desde la aplicación a través deServiceConnection. Dentro del código de nuestra Activity, tendríamos:

public class MiActividad extends Activity{

IServicio servicio;MiServicioConnection connection;

class MiServicioConnection implements ServiceConnection{

public void onServiceConnected(ComponentName name,IBinder service) {

servicio = IServicio.Stub.asInterface((IBinder) service);

}

public void onServiceDisconnected(ComponentName name) {

servicio = null;}

}

@Overridepublic void onCreate(Bundle savedInstanceState){

super.onCreate(savedInstanceState);setContentView(R.layout.main);

connection = new MiServicioConnection();

// El intent se crea así porque la clase// podría estar en otra aplicación:Intent intent = new Intent();intent.setClassName("es.ua.jtech",

es.ua.jtech.Servicio.class.getName());

bindService(intent, connection,Context.BIND_AUTO_CREATE);

String saludo = servicio.saludo("MiNombre", "MiApellido");

// ...}

@Overrideprotected void onDestroy() {

unbindService(connection);}

}

Los tipos que se permiten en AIDL son:

• Valores primitivos como int, float, double, boolean, etc.• String y CharSequence

• java.util.List y java.util.Map

• Intefaces definidos en AIDL, requiere definir el import.• Clases Java que implementen la interfaz Parcelable que Android usa para permitir la

Servicios de la plataforma Android

48Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

serialización. También requiere definir el import.

Si el servicio está en otro proyecto necesitaremos declararlo con un intent-filter en suManifest:

<service android:export="true" name="es.ua.jtech.Servicio"><intent-filter>

<action android:name="es.ua.jtech.IServicio"/></intent-filter>

</service>

y crear el intent con ese nombre de acción:

Intent intent = new Intent("es.ua.jtech.IServicio");

5.4.3. Otras formas de comunicación

5.4.3.1. Broadcast privados

Enviar un Intent a través del método sendBroadcast() es sencillo pero por defectocualquier aplicación podría tener un BroadcastReceiver que obtenga la información denuestro intent. Para evitarlo se puede utilizar el método Intent.setPackage() querestringirá el broadcast a determinado paquete.

5.4.3.2. PendingResult

Otra manera de que un servicio envíe información a una actividad es a través del métodocreatePendingResult(). Se trata de un método que permite a un PendingIntent

disparar el método Activity.onActivityResult. Es decir, el servicio remoto llamaría asend() con un PendingIntent con la información, de manera análoga al setResult()que ejecuta una actividad que fue llamada con startActivityForResult(). Dentro deActivity.onActivityResult se procesaría la información del Intent.

Este es un mecanismo que sólo funciona con actividades.

Servicios de la plataforma Android

49Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

6. Ejercicios - Servicios

6.1. Contador: Servicio con proceso en background (0.6 puntos)

Los servicios se utilizan para ejecutar algún tipo de procesamiento en background. Eneste ejercicio vamos a crear nuestro propio proceso asociado a un servicio que ejecutaráuna determinada tarea. En este caso el proceso asociado contará desde 1 hasta 100deteniéndose 5 segundos antes de cada incremento. Cada vez que se cambie de valor semostrará un Toast que nos informe de la cuenta.

En las plantillas tenemos el proyecto android-av-serviciocontador que ya incluye ladeclaración del servicio en el manifest, la actividad que inicia y detiene el servicio, y elesqueleto del servicio MiCuentaServicio.

• En el esqueleto que se proporciona, viene definida una extensión de AsyncTask

llamada MiTarea. Los métodos onPreExecute, doInBackground,onProgressUpdate y onCancelled están sobrecargados pero están vacíos. Se pideimplementarlos, el primero de ellos tiene que inicializar el campo i que se utiliza parala cuenta, el segundo tiene que ejecutar un bucle desde 1 hasta 100, y en cadaiteración pedir que se muestre el progreso (publishProgress) y hacer una pausa de 5segundos con Thread.sleep(5000). El tercer método, onProgressUpdate mostraráel Toast con el progreso, y por último el método de cancelación pondrá el valormáximo de la cuenta para que se salga del bucle.

• En los métodos del servicio, onCreate, onStartCommand y onDestroy,introduciremos la creación de la nueva MiTarea, su ejecución (método execute() dela tarea) y la cancelación de su ejecución (método cancel() de la tarea).

El servicio deberá seguir funcionando aunque se salga de la aplicación y podrá ser paradoentrando de nuevo en la aplicación y pulsando Stop.

6.2. Broadcast Receiver: Captura de llamadas (0.6 puntos)

Se pide crear un proyecto android-av-broadcast que capture los números de teléfonode las llamadas entrantes y los muestre a través del LogCat.

El proyecto tendrá un único fichero de código Java (llamado MiBroadcastReceiver) queimplementará un BroadcastReceiver. En primer lugar añadiremos esta clase al Manifest, yle asignaremos un intent-filter que capture las acciones de tipo:"android.intent.action.PHONE_STATE". Además es necesario solicitar en el Manifestpermisos para leer el estado del teléfono, para esto tenemos que añadir:"<uses-permission android:name="android.permission.READ_PHONE_STATE"/>".

En el método "onReceive" de nuestra clase "MiBroadcastReceiver" comprobaremos queel evento recibido es del tipo esperado:

Servicios de la plataforma Android

50Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

String action = intent.getAction();if(action.equals("android.intent.action.PHONE_STATE") {

\\...}

Si es de este tipo obtendremos el número de teléfono de la llamada a partir de los extrasque recibimos en el intent (revisar la sección correspondiente de teoría) y lo mostraremosen el Log.

Al ejecutar esta aplicación Eclipse nos mostrará los siguientes avisos: "No Launcheractivity found!", "The launch will only sync the application package on the device!". Estoes normal debido a que no hay una actividad principal que pueda mostrar, sin embargo síque instalará la aplicación.

Para comprobar que tu aplicación funciona correctamente abre la vista "EmulatorControl" (Window > Show view > Other... > "Emulator Control") y realiza una llamadade prueba.

NotaA partir de la versión 3.1 de Android es necesario que se incluya una actividad visible (ademásde la clase con el BroadcastReceiver) para que sea efectuado el registro del receiver. En estaactividad podríamos poner algún texto explicativo como: "Se ha registrado el servicio".

6.3. Broadcast Receiver: Reenvío de datos (0.6 puntos)

Vamos a ampliar el ejercicio anterior para que al recibir una llamada envíe datosmediante un SMS a un número de teléfono. El servicio podría, por ejemplo, enviar laposición GPS cuando recibe la llamada de un número específico. En este ejercicio, porsimplificar, enviaremos un SMS cuando se reciba una llamada desde cualquier número ycomo texto del SMS se incluirá el propio número que ha llamado.

En primer lugar modificamos el Manifest de la aplicación anterior para solicitar permisospara el envío de SMSs:

<uses-permission android:name="android.permission.SEND_SMS"/>

En el fichero de código del broadcast ("MiBroadcastReceiver") añadimos la siguientefunción para el envío de SMSs:

public void send( String phoneNumber, String message ){

SmsManager sms = SmsManager.getDefault();sms.sendTextMessage( phoneNumber, null, message, null, null );Log.i( "SendSMS", "Enviando SMS a " + phoneNumber + ": \"" +

message + "\"" );}

Por último modificamos el código del método "onReceive" para que además de mostrar elnúmero de la llamada por el Log lo envíe por SMS.

Servicios de la plataforma Android

51Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Para probar este ejercicio tenemos que abrir dos emuladores. En uno de ellos instalamosel servicio y utilizamos el segundo emulador para realizar la llamada y comprobar que serecibe el SMS correctamente. Para realizar una llamada de un emulador a otrosimplemente hay que marcar el número del puerto que aparece en el título de la ventanade cada emulador.

6.4. Arranque: Iniciar servicio al arrancar el móvil (0.6 puntos)

En el proyecto android-av-onboot tenemos un servicio que se lanza al iniciar laactividad, mostrando Toasts del 1 al 5 y finalizando. Puedes comprobar ejecutando esteproyecto como al arrancar o instalar la actividad se muestran estos Toast de aviso. Se pidemodificar el código para que el servicio se inicie cuando el móvil haya terminado dearrancar.

Para ello añadiremos el siguiente permiso al AndroidManifest.xml:

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

También registraremos un receptor de broadcast MiBroadcastReceiver con unintent-filter para android.intent.action.BOOT_COMPLETED.

A continuación implementaremos un BroadcastReceiver (llamadoMiBroadcastReceiver) y en su método onReceive() introduciremos el código:

if( "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())){

ComponentName comp = new ComponentName(context.getPackageName(),MiServicio.class.getName());

ComponentName service = context.startService(new Intent().setComponent(comp));

if (null == service){Log.e(MiBroadcastReceiver.class.toString(),

"Servicio no iniciado: " + comp.toString());}

} else {Log.e(MiBroadcastReceiver.class.toString(),"Intent no esperado " + intent.toString());

}

Tras ejecutar el proyecto desde Eclipse habrá que reiniciar el emulador para observar queel servicio se inicia al finalizar el arranque. El botón de apagar del emulador nofuncionará, lo mejor es cerrar el emulador y volver a abrirlo.

6.5. Calculadora: Comunicación con el servicio (0.6 puntos)

En este ejercicio vamos a practicar con los métodos de comunicación con un servicio.Para esto vamos a implementar una calculadora que a partir de un par de números deentrada realice su suma, resta, multiplicación o división utilizando un servicio.

Servicios de la plataforma Android

52Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

En primer lugar creamos el proyecto y en la interfaz de la aplicación insertamos doscampos de edición (donde el usuario escribirá los operandos), un campo de texto vacío(donde se mostrará el resultado) y cuatro botones (para la suma, resta, multiplicación ydivisión).

A continuación añadiremos un servicio, llamado "MiServicioBind", en el cualimplementaremos la clase onBind para poder realizar el binding. Además le añadiremoscuatro métodos públicos que a partir de dos números decimales devuelvan su suma, resta,multiplicación o división respectivamente. Además recordad que tenéis que añadir elservicio al Manifest del proyecto.

En el código de la actividad principal, además de obtener referencias a los campos deedición y los botones e implementar sus métodos onClick, tendremos que atar el servicioque hemos creado. En los listener de los botones llamaremos al método correspondiente ymostraremos el resultado en el textView. Por último, tendremos que desatar el servicio aldestruir la actividad.

Servicios de la plataforma Android

53Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

7. AppWidgets

En los escritorios de Android es posible colocar un tipo especial de aplicacionesdenominadas Widgets. Estos Widgets permiten mostrar al usuario una información (o unaespecie de interfaz reducida) sin necesidad de abrir una aplicación. Ocupan undeterminado tamaño en el escritorio, por lo que es posible tener en el mismo escritoriocolocados a la vez varios de estos elementos. Son muy útiles para mostrar informaciónrápida al usuario, como por ejemplo la hora, los últimos correos, últimos eventos delcalendario, etc., y que al pulsar sobre ellos se abra la aplicación completa para poderinteractuar.

7.1. AppWidgets

Los widgets, que desde el punto de vista del programador son AppWidgets, son pequeñosinterfaces de programas Android que permanecen en el escritorio del dispositivo móvil.Para añadir alguno sobra con hacer una pulsación larga sobre un área vacía del escritorioy seleccionar la opción "widget", para que aparezca la lista de todos los que hayinstalados y listos para añadir. En la versión 4 de Android para añadir widgets hay que iral menú de aplicaciones y desde la sección de widgets seleccionar uno y pulsarloprolongadamente para arrastrarlo a la zona deseada de la pantalla de inicio.

Seleccionar un (App) Widget

Este es un ejemplo de un widget en el que se muestra un reloj, incluido de serie conAndroid:

Servicios de la plataforma Android

54Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

AppWidget reloj de Android

7.2. Crear un Widget

Para crear un widget se necesitan varios elementos:

• Una definición o configuración de los metadatos del Widget en XML.• Un layout para definir su interfaz gráfica.• La implementación de la funcionalidad del Widget (un IntentReceiver).• Declaración del Widget en el Android Manifest de la aplicación.

7.2.1. Definición XML del Widget

Los AppWidgets ocupan un determinado tamaño y se refrescan con una determinadafrecuencia, estos son metadatos que hay que declarar en el XML que define el widget. Sepuede añadir como nuevo recurso XML de Android, y seleccionar el tipo Widget. Secoloca en la carpeta res/xml/. A continuación se incluye la definición XML guardada en"res/xml/miwidget_conf.xml" de nuestro ejemplo:

<?xml version="1.0" encoding="utf-8"? ><appwidget-provider

xmlns:android="http://schemas.android.com/apk/res/android"android:minWidth="146dip"android:minHeight="72dip"android:updatePeriodMillis="4320000"android:label="Mi Widget"android:initialLayout="@layout/miwidget_layout"/>

Las propiedades que hemos definido en este XML son las siguientes:

• minWidth y minHeight: ancho y alto mínimo que ocupará el Widget en el escritorio.• updatePeriodMillis: frecuencia de actualización del Widget en milisegundos.• label: nombre del Widget que se mostrará en el menú de selección de Android.• initialLayout: referencia al layout XML con la interfaz del Widget.•La pantalla de inicio de Android por defecto está dividida en una matriz de 4x4 celdasdonde se pueden colocar aplicaciones, accesos directos y Widgets. Los Widgets, comohemos dicho, pueden ocupar más de una celda. Estas celdas tienen una dimensión de74x74 dip (píxeles independientes del dispositivo). Para indicar el número de celdas queva a ocupar el Widget tenemos que calcular el número de "dip" que va a ocupar. Lafórmula para calcular esto es muy sencilla:

• ancho_minimo = (num_celdas_ancho * 74) - 2• alto_minimo = (num_celdas_alto * 74) - 2

Servicios de la plataforma Android

55Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Los 2 dip que se le restan son por el borde. Por ejemplo, en nuestro caso, para que ocupe2 celdas de ancho por 1 celda de alto tenemos que indicar una dimensiones de 146dip x72dip. En los dispositivos donde las dimensiones mínimas que establezcamos nocoincidan exactamente con las celdas de la pantalla, el tamaño de nuestro widget seráextendido para llenar lo que queda libre de las celdas.

La frecuencia de actualización del Widget (updatePeriodMillis) tiene un mínimopermitido: 30 minutos (1800000 milisegundos). Además, si el dispositivo está en reposocuando se realiza la actualización éste se despertará, por lo que puede que se incrementeel gasto de batería si se realiza con mucha frecuencia. Es aconsejable, para este tipo deprácticas, utilizar en su lugar intents programados mediante un AlarmManager (comoveremos más adelante).

7.2.2. Layout del Widget

En el XML de configuración de ejemplo, en el campo "initialLayout", se indica que ellayout del widget se encuentra en "res/layout/miwidget_layout.xml". Este es ellayout que define la interfaz gráfica del Widget, su diseño es análogo al del interfaz decualquier aplicación normal, pero con unas pequeñas restricciones. Por cuestiones dediseño de Android, relacionadas con seguridad y eficiencia, los únicos componentesgráficos permitidos en el layout del widget son:

• Los layouts: FrameLayout, LinearLayout y RelativeLayout.• Los views: TextView, Button, ImageButton, ImageView, ProgressBar,

AnalogClock y Chronometer.

Es interesante el hecho de que los EditText no están permitidos en el layout del widget.Para conseguir este efecto lo que se suele hacer es utilizar una imagen que imite este viewy al hacer click, abrir una actividad semi-transparente por encima que sea la que nospermita introducir el texto.

Desde la versión 3.0 de Android los widget cuentan con nuevas características y mayorinteractividad. Los nuevos componentes que se pueden utilizar son GridView, ListView,StackView, ViewFlipper, AdapterViewFlipper. Por ejemplo StackView no estabapresente entre los componentes de las versiones anteriores y es un view que permitemostrar "tarjetas" e ir pasándolas hacia delante o hacia atrás, como se muestra acontinuación:

Servicios de la plataforma Android

56Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Widget con StackView

Un ejemplo de layout podría ser el siguiente, en el que se coloca un reloj analógico y uncampo de texto al lado:

<?xml version="1.0" encoding="utf-8"?><LinearLayout android:id="@+id/miwidget"xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content">

<AnalogClockandroid:id="@+id/analogClock1"android:layout_width="45dp"android:layout_height="45dp" />

<TextView android:text=""android:id="@+id/TextView01"android:textSize="9dp"android:layout_width="wrap_content"android:layout_height="wrap_content"android:shadowColor="#000000"android:shadowRadius="1.2"android:shadowDx="1"android:shadowDy="1">

</TextView></LinearLayout>

7.2.3. Implementación de la funcionalidad del Widget

Los AppWidgets no necesitan ninguna actividad, sino que se implementan comoIntentReceivers con IntentFilters que detectan intents con acciones de actualizaciónde widget como AppWidget.ACTION_APPWIDGET_UPDATE, DELETED, ENABLED,

DISABLED, así como con acciones personalizadas.

Servicios de la plataforma Android

57Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Android provee la clase AppWidgetProvider (clase derivada de BroadcastReceiver) quees una alternativa proporcionada por Android para encapsular el procesamiento de losIntent y proveer de handlers para la actualización, borrado, habilitación y deshabilitacióndel widget.

En esta clase deberemos implementar los mensajes a los que vamos a responder desdenuestro widget, entre los que destacan:

• onEnabled(): lanzado cuando se añade al escritorio la primera instancia de un widget.• onUpdate(): lanzado periódicamente cada vez que se debe actualizar un widget.• onDeleted(): lanzado cuando se elimina del escritorio una instancia de un widget.• onDisabled(): lanzado cuando se elimina del escritorio la última instancia de un

widget.

En la mayoría de los casos, tendremos que implementar como mínimo el eventoonUpdate(). El resto de métodos dependerán de la funcionalidad de nuestro widget. Ennuestro ejemplo de momento solo vamos a implementar el método "onUpdate", aunque lodejaremos vacío. A continuación se incluye un ejemplo del código de nuestro Widget:

public class MiWidget extends AppWidgetProvider {@Overridepublic void onUpdate(Context context,

AppWidgetManager appWidgetManager,int[] appWidgetIds) {

// Actualizar el Widget...}

}

7.2.4. Manifest

El último paso es declarar el Widget dentro del AndroidManifest.xml de nuestraaplicación. No necesitamos declarar una actividad principal, sino que solamentetendremos que declarar el receptor de intents de nuestro widget:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"

android:versionCode="1"android:versionName="1.0"package="es.ua.jtech.av.appwidget">

<uses-sdk android:minSdkVersion="7" /><application android:icon="@drawable/icon"

android:label="@string/app_name">

<receiver android:name=".MiWidget"android:label="Frases Widget">

<intent-filter><action

android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter><intent-filter>

<action android:name="es.ua.jtech.av.ACTION_WIDGET_CLICK"/>

</intent-filter><meta-data android:name="android.appwidget.provider"

Servicios de la plataforma Android

58Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

android:resource="@xml/miwidget_conf" /></receiver>

</application>

</manifest>

En el ejemplo anterior de manifest se declara nuestro Widget como un elemento"receiver" dentro de la sección "application". En la etiqueta "name" se indica el nombrede la clase java con el código del receptor. Además se añaden dos secciones importantes,una sección "meta-data" donde en "resource" se tiene que indicar el fichero XML quecontiene la descripción del Widget; y otra sección de "intent-filter" para la captura deeventos. En este ejemplo se capturará el evento de actualización del widget y un eventopersonalizado "es.ua.jtech.av.ACTION_WIDGET_CLICK" (que veremos más adelante).

Llegados a este punto ya es posible probar el Widget, añadir instancias al escritorio,desplazarlo por la pantalla o eliminarlo.

7.3. Actualización del Widget

Para realizar la actualización del Widget tenemos que modificar su fichero de código paracompletar el método "onUpdate" que habíamos dejado vacío en el ejemplo anterior. Esimportante tener en cuenta que es posible que hayan varias instancias de un mismo widgetañadidas al escritorio, por lo que tendremos que actualizarlas todas. El método"onUpdate" recibe como parámetro un array con la lista de Widgets que hay queactualizar. Por lo que de forma genérica podríamos realizar la actualización de la forma:

@Overridepublic void onUpdate(Context context,

AppWidgetManager appWidgetManager,int[] appWidgetIds)

{//Iteramos la lista de widgets en ejecuciónfor (int i = 0; i < appWidgetIds.length; i++){

//ID del widget actualint widgetId = appWidgetIds[i];//Actualizamos el widget actualactualizarWidget(context, appWidgetManager, widgetId);

}}

private void actualizarWidget(Context context,AppWidgetManager appWidgetManager, int widgetId)

{// Actualizar un Widget...

}

Al extraer la actualización a un método estático independiente podemos reutilizar estecódigo para llamarlo desde otras partes de la aplicación, como por ejemplo para realizarla actualización inicial.

Para realizar la actualización de las vistas de nuestro Widget necesitamos utilizar unaclase especial llamada RemoteViews. A continuación se explica este proceso de

Servicios de la plataforma Android

59Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

actualización.

7.3.1. RemoteViews

La clase RemoteViews se utiliza para definir y manipular una jerarquía de views que seencuentra en el proceso de una aplicación diferente. Permite cambiar propiedades yejecutar métodos. En el caso de los widgets, los views están alojados en un proceso(normalmente perteneciente a la pantalla del Home) y hay que actualizarlos desde elIntentReceiver que definimos para controlar el widget.

La actualización de los valores de los views se realiza por medio de los métodos de laclase RemoteViews, tales como .setTextViewText(), .setImageViewBitmap(), etc. Enprimer lugar crearemos un objeto de tipo RemoteViews al que le pasaremos elidentificador del layout de nuestro Widget, con este objeto ya podemos actualizar lasvistas de este layout, por ejemplo:

RemoteViews updateViews = new RemoteViews(context.getPackageName(),R.layout.miwidget_layout);

updateViews.setTextViewText(R.id.TextView01, "Mensaje de prueba");

RemoteViews proporciona métodos para acceder a propiedades como setInt(...),setBoolean(...), setBitmap(...), etc, y métodos para acceder a Views específicos,como setTextViewText(...), setTextColor(...), setImageViewBitmap(...),setChronometer(...), setProgressBar(...), setViewVisibility(...).

Una vez definido el cambio a realizar por medio de RemoteViews, tenemos que aplicarestas modificaciones utilizando el appWidgetManager que recibimos como parámetro.Esto es importante, ya que de no hacerlo la actualización de los controles no se reflejarácorrectamente en la interfaz del widget. En el siguiente código se muestra un ejemplocompleto en el que se actualiza el campo de texto del Widget con la hora actual:

private void actualizarWidget(Context context,AppWidgetManager appWidgetManager, int widgetId)

{RemoteViews updateViews = new RemoteViews(

context.getPackageName(),R.layout.miwidget_layout);

//Obtenemos la hora actualCalendar calendario = new GregorianCalendar();String hora = calendario.getTime().toLocaleString();

//Actualizamos la hora en el control del widgetupdateViews.setTextViewText(R.id.TextView01, hora);

//Notificamos al manager de la actualización del widget actualappWidgetManager.updateAppWidget(widgetId, updateViews);

}

7.4. Eventos de actualización

Servicios de la plataforma Android

60Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

A los controles utilizados en los widgets de Android, que ya sabemos que son del tipoRemoteView, no podemos asociar eventos de la forma tradicional. Sin embargo, tenemosla posibilidad de asociar a un evento (por ejemplo la realización de un click) unadeterminada acción de tipo broadcast (PendingIntent) que será lanzada cada vez que seproduzca dicho evento.

Podemos configurar el propio widget para que capture esos mensajes (ya que comoindicamos no es más que un componente de tipo broadcast receiver). Para esto tenemosque implementar su método "onReceive()" con las acciones necesarias a ejecutar trascapturar el mensaje. De esta forma "simulamos" la captura de eventos sobre controles deun widget.

En primer lugar hacemos que se lance un intent broadcast cada vez que se pulse sobre elwidget o algún elemento del mismo. Para ello, en el método "actualizarWidget()"construiremos un nuevo Intent asociándole una acción personalizada, que en nuestro casollamaremos "es.ua.jtech.av.ACTION_WIDGET_CLICK". Como parámetro del nuevoIntent insertaremos mediante putExtra() el ID del widget actual de forma que más tardepodamos saber el widget concreto que ha lanzado el mensaje. Por último crearemos elPendingIntent mediante el método getBroadcast() y lo asociaremos al evento onClick

del control utilizando setOnClickPendingIntent() y pasándole el ID del control.Veamos cómo queda todo esto:

// Asociamos los 'eventos' al widgetIntent intent = new Intent("es.ua.jtech.av.ACTION_WIDGET_CLICK");intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);

PendingIntent pendingIntent =PendingIntent.getBroadcast(context, widgetId,

intent, PendingIntent.FLAG_UPDATE_CURRENT);

updateViews.setOnClickPendingIntent(R.id.miwidget, pendingIntent);

Es importante que la acción de este intent se añada al Manifest, dentro de la sección"receiver" del Widget, tendremos que declarar otro "intent-filter" para que capture estetipo de eventos y responda a ellos:

<intent-filter><action android:name="es.ua.jtech.av.ACTION_WIDGET_CLICK" />

</intent-filter>

Por último tenemos que implementar el evento "onReceive()" del widget para actuar encaso de recibir nuestro mensaje de actualización personalizado. Dentro de este eventocomprobaremos si la acción del mensaje recibido es la nuestra, y en ese casorecuperaremos el ID del widget que lo ha lanzado, obtendremos una referencia al "widgetmanager", y por último llamamos a nuestro método estático de actualización pasándoleestos datos:

@Overridepublic void onReceive(Context context, Intent intent){

if(intent.getAction().equals("es.ua.jtech.av.ACTION_WIDGET_CLICK"))

Servicios de la plataforma Android

61Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

{//Obtenemos el ID del widget a actualizarint widgetId = intent.getIntExtra(

AppWidgetManager.EXTRA_APPWIDGET_ID,AppWidgetManager.INVALID_APPWIDGET_ID);

//Obtenemos el widget manager de nuestro contextoAppWidgetManager widgetManager =

AppWidgetManager.getInstance(context);

//Actualizamos el widgetif (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID)

actualizarWidget(context, widgetManager,widgetId);}

Con esto ya hemos finalizado la emisión y captura de eventos al pulsar sobre el Widget (oalgún view de este Widget). Dentro del método "actualizarWidget" realizaremos lasacciones de actualización, que dependerán del tipo de widget en cuestión que estemosimplementando.

Para actualizaciones periódicas (con una frecuencia inferior a 30 min) se puede utilizar elAlarmManager que podría ser programado para forzar la actualización del widget,enviando periódicamente un broadcast con la acción que hayamos definido para tal fin.

7.4.1. Abrir una aplicación

Al pulsar sobre un widget, en lugar de lanzar un intent tipo broadcast, también podríamoslanzar un intent que abra un actividad determinada, simplemente realizando:

//Comportamiento al pulsar el widget: lanzar alguna actividadIntent defineIntent = new Intent(context, Actividad.class);PendingIntent pendingIntent = PendingIntent.getActivity(

context, 0, defineIntent, 0);updateViews.setOnClickPendingIntent(R.id.miwidget, pendingIntent);

7.5. Servicio de actualización

Otra alternativa para refrescar la interfaz gráfica es el uso de un servicio que actualice elwidget. Para ello habría que declararlo en el manifest y declarar la clase del servicioUpdateService extends Service dentro de la clase MiWidget.

A continuación se muestra un ejemplo de la clase MiWidget que implementa un widget:

public class MiWidget extends AppWidgetProvider{

@Overridepublic void onUpdate(Context context, AppWidgetManager

appWidgetManager, int[] appWidgetIds) {// Inicio de nuestro servicio de actualización:context.startService(

new Intent(context, UpdateService.class));}

Servicios de la plataforma Android

62Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

public static class UpdateService extends Service{

@Overridepublic int onStartCommand(Intent intent,

int flags, intstartId) {

RemoteViews updateViews = new RemoteViews(getPackageName(),

R.layout.miwidget_layout);

//Aqui se actualizarían todos los tipos de Views:updateViews.setTextViewText(R.id.TextView01,

"Valor con el que refrescamos");// ...

//Y la actualización a través del updateViewscreado:

ComponentName thisWidget = new ComponentName(this, MiWidget.class);

AppWidgetManager.getInstance(this).updateAppWidget(thisWidget, updateViews);

return Service.START_STICKY;}

@Overridepublic IBinder onBind(Intent intent) {

return null;}

}}

7.6. Actividad de configuración

Algunos widgets muestran una actividad de configuración la primera vez que sonañadidos a la pantalla. Cuando el sistema solicita la configuración de un widget se recibeun broadcast intent con la acción android.appwidget.action.APPWIDGET_CONFIGURE,por tanto definiremos en el manifest nuestra actividad con el intent-filter

correspondiente, que la ejecutará al recibir la acción:

<activity android:name=".MiConfigurationActivity"><intent-filter>

<actionandroid:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>

</intent-filter></activity>

Esta actividad es una actividad normal, que podemos crear utilizando todos los conceptosque hemos visto, recoger los datos del usuario y almacenarlos de alguna forma (porejemplo utilizando "SharedPreferences").

Para finalizar la actividad podemos colocar botones para aceptar o cancelar. En el caso deque se cancele tendríamos que realizar lo siguiente:

setResult(RESULT_CANCELED);finish();

Servicios de la plataforma Android

63Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Si se presiona el botón de aceptar la actividad debe devolver un Intent que incluya unextra con el identificador del App Widget, usando la constante EXTRA_APPWIDGET_ID (sepuede obtener a partir del Intent que lanza la actividad). A continuación se muestra elcódigo que realizaría esta acción:

//Obtenemos el Intent que ha lanzado esta ventana//y recuperamos sus parámetrosIntent intentOrigen = getIntent();Bundle params = intentOrigen.getExtras();

//Obtenemos el ID del widget que se está configurandoint widgetId = params.getInt(

AppWidgetManager.EXTRA_APPWIDGET_ID,AppWidgetManager.INVALID_APPWIDGET_ID);

//Desde el botón aceptar...Intent resultado = new Intent();resultado.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId);setResult(RESULT_OK, resultado);finish();

Esta actividad de configuración también debe ser declarada en el xml que contiene losmetadatos del widget:

<appwidget-provider...

android:updatePeriodMillis="3600000"android:configure="es.ua.jtech.av.appwidget.MiConfigurationActivity"

/>

La actividad será mostrada solamente al añadir el widget a la pantalla por primera vez.

Servicios de la plataforma Android

64Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

8. Ejercicios - AppWidgets

8.1. IP AppWidget (1.5 puntos)

Vamos abrir el proyecto android-av-appwidget para construir un AppWidget deAndroid, que nos muestre una frase célebre y la hora.

En el proyecto pulsamos con el botón derecho y añadimos un nuevo "Android XMLFile", de tipo "AppWidget Provider", que se llame miwidget_conf.xml. El editor nospermite pulsar sobre el "AppWidget Provider" y editar sus atributos. Ponemos lossiguientes:

android:minWidth="146dip"android:minHeight="72dip"android:updatePeriodMillis="1800000"android:initialLayout="@layout/miwidget_layout"

El layout miwidget_layout.xml no lo tenemos que crear porque ya está incluido en elproyecto.

Creamos una clase Java llamada MiWidget que herede de AppWidgetProvider, en elpaquete es.ua.jtech.av.appwidget. Sobrecargamos su método onUpdate(...) y sumétodo onReceive(...). En este último comprobaremos si se ha recibido un intent conla acción personalizada "es.ua.jtech.av.ACTION_WIDGET_CLICK":

public class MiWidget extends AppWidgetProvider{

public static final String ACTION_WIDGET_CLICK ="es.ua.jtech.av.ACTION_WIDGET_CLICK";

@Overridepublic void onUpdate(Context context, AppWidgetManager

appWidgetManager,int[] appWidgetIds) {//TODO actualizar

}

@Overridepublic void onReceive(Context context, Intent intent) {

super.onReceive(context, intent);if(intent.getAction().equals(ACTION_WIDGET_CLICK)){

//TODO actualizar}

}}

Tanto desde el método "onUpdate()" como desde "onReceive()" tenemos queactualizar la vista de la misma forma, por lo que vamos a crear un método privado de laclase "private void actualizarWidget(Context context, AppWidgetManager

appWidgetManager, int widgetId)" que llamaremos desde ambos métodos tal y comohemos visto en la teoría.

Servicios de la plataforma Android

65Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

El método privado utilizará los RemoteViews para cambiar el campo de texto. Loactualizará con una frase aleatoria de entre las definidas dentro de un array de strings enel recurso strings.xml que viene incluido en el proyecto de las plantillas. También seasignará un comportamiento al hacer click sobre el widget que consistirá en enviar unbroadcast intent con la acción que hemos definido:

RemoteViews updateViews = new RemoteViews(context.getPackageName(),R.layout.miwidget_layout);

//Seleccionar frase aleatoriaString frases[] = context.getResources().getStringArray(R.array.frases);updateViews.setTextViewText(R.id.TextView01,frases[(int)(Math.random()*frases.length)]);

//Comportamiento del botón//On-click listener que envía un broadcast intentIntent intent = new Intent(ACTION_WIDGET_CLICK);PendingIntent pendingIntent = PendingIntent.getBroadcast(

context, widgetId,intent,

PendingIntent.FLAG_UPDATE_CURRENT);updateViews.setOnClickPendingIntent(R.id.miwidget, pendingIntent);

//Notificamos al manager de la actualización del widget actualappWidgetManager.updateAppWidget(widgetId, updateViews);

En el AndroidManifest.xml, dentro de <application> declararemos el "receiver" denuestro widget con dos "intent filters", uno para la acción del sistemaandroid.appwidget.action.APPWIDGET_UPDATE, y otro para la que utilizamos paraforzar la actualización desde la clase MiWidget:

<receiver android:name=".MiWidget" android:label="Frases Widget"><intent-filter>

<actionandroid:name="android.appwidget.action.APPWIDGET_UPDATE" />

</intent-filter><intent-filter>

<action android:name="es.ua.jtech.av.ACTION_WIDGET_CLICK"/>

</intent-filter><meta-data android:name="android.appwidget.provider"

android:resource="@xml/miwidget_conf" /></receiver>

Ejecutamos el widget desde Eclipse, como aplicación Android, y comprobamos que noocurra ningún error en la consola de Eclipse. Ya se puede añadir el widget en elescritorio, efectuando una pulsación larga sobre una porción de área libre del escritorio, yseleccionando nuestro widget.

En Android 4.0 la pulsación larga no nos muestra la opción de añadir widget sino que hayque ir al menú de aplicaciones y seleccionar la pestaña de widgets. Para insertar uno setiene que pulsar prolongadamente para arrastrarlo a una zona libre del escritorio.Añadimos el widget y observamos el resultado:

Servicios de la plataforma Android

66Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Widget que muestra una frase aleatoria

La frase se tiene que actualizar al pulsar sobre el Widget. Prueba a añadir dos Widgets delmismo tipo y a pulsar sobre cada uno de ellos para ver como cambian de formaindependiente.

8.2. StackWidget (1.5 puntos)

Vamos a probar una de las características novedosas de los widgets a partir de Android3.0, se trata del StackView que nos permite pasar "tarjetas" hacia delante y hacia atrás.Ejecuta el proyecto android-av-stackwidget de las plantillas y comprueba quefunciona correctamente en un emulador con alguna de las últimas versiones de Android.Si probamos a ejecutarlo en una versión antigua nos aparecerá un error.

Vamos a modificar el widget para que muestre las frases del ejercicio anterior. Para ellotenemos que copiar y pegar el array de strings del recurso strings.xml del ejercicioanterior. A continuación editamos la clase StackWidgetService y le añadimos un campo"private String [] frases;". Este array lo podemos inicializar en en el constructor:

frases = context.getResources().getStringArray(R.array.frases);

En el método onCreate sustituiremos el bucle con las llamadas amWidgetItems.add(new WidgetItem(...)) por un bucle que añada todas las cadenasde frases. El resultado debe quedar así:

Servicios de la plataforma Android

67Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

StackWidget con frases

Servicios de la plataforma Android

68Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

9. Notificaciones

En esta sección vamos a tratar las distintas formas que tenemos de enviar notificaciones alos usuarios, desde notificaciones en la misma pantalla mediante ventanas de diálogo oavisos denominados Toast, hasta notificaciones más avanzadas que serán mostradas en labarra de estado del sistema (o bandeja del sistema) y que podrán ser emitidas aun cuandola aplicación no esté en primer plano.

9.1. Notificaciones Toast

Un toast es un mensaje que se muestra en pantalla durante unos segundos al usuario paraluego volver a desaparecer automáticamente sin requerir ningún tipo de actuación por suparte, y sin recibir el foco en ningún momento (o dicho de otra forma, sin interferir en lasacciones que esté realizando el usuario en ese momento). Aunque son personalizables,aparecen por defecto en la parte inferior de la pantalla, sobre un rectángulo grisligeramente translúcido. Por sus propias características, este tipo de notificaciones sonideales para mostrar mensajes rápidos y sencillos al usuario, pero por el contrario, al norequerir confirmación por su parte, no deberían utilizarse para hacer notificacionesdemasiado importantes.

Para crear un mensaje de este tipo haremos uso de la clase Toast. Esta clase dispone deun método estático makeText() al que deberemos pasar como parámetro el contexto de laactividad, el texto a mostrar, y la duración del mensaje, que puede tomar los valoresLENGTH_LONG (3.5 segundos) o LENGTH_SHORT (2 segundos), dependiendo del tiempo quequeramos que la notificación aparezca en pantalla. Tras obtener una referencia al objetoToast a través de este método, ya sólo nos quedaría mostrarlo en pantalla mediante elmétodo show(). A continuación se incluye un ejemplo de uso:

btnDefecto.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) {

Toast toast1 = Toast.makeText(getApplicationContext(),"Toast por defecto", Toast.LENGTH_SHORT);

toast1.show();}

});

Si ejecutamos esta sencilla aplicación en el emulador y pulsamos el botón veremos comoen la parte inferior de la pantalla aparece el mensaje "Toast por defecto", que tras variossegundos desaparecerá automáticamente.

Servicios de la plataforma Android

69Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Toast

Como hemos comentado, éste es el comportamiento por defecto de las notificacionestoast, sin embargo también podemos personalizarlo cambiando su posición en la pantalla.Para esto utilizaremos el método setGravity(), al que podremos indicar en que zonadeseamos que aparezca la notificación. La zona deberemos indicarla con alguna de lasconstantes definidas en la clase Gravity: CENTER, LEFT, BOTTOM, ... o con algunacombinación de éstas. Como ejemplo vamos a colocar la notificación en la zona centralizquierda de la pantalla. Para ello, añadamos un segundo botón a la aplicación de ejemploque muestre un toast con estas características:

btnGravity.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) {

Toast toast2 = Toast.makeText(getApplicationContext(),"Toast con gravity", Toast.LENGTH_SHORT);

toast2.setGravity(Gravity.CENTER|Gravity.LEFT,0,0);toast2.show();

}});

Si volvemos a ejecutar la aplicación y pulsamos el nuevo botón veremos como el toastaparece en la zona indicada de la pantalla:

Servicios de la plataforma Android

70Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Toast

Android nos permite personalizar por completo el aspecto de la notificación definiendoun layout XML para el toast. En este layout podremos incluir todos los elementosnecesarios para adaptar la notificación a nuestras necesidades. Vamos a ampliar elejemplo incluyendo un layout sencillo, con una imagen y una etiqueta de texto sobre unrectángulo gris:

<?xml version="1.0" encoding="utf-8"?><LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"android:id="@+id/lytLayout"android:layout_width="fill_parent"android:layout_height="fill_parent"android:orientation="horizontal"android:background="#555555"android:padding="5dip" >

<ImageView android:id="@+id/imgIcono"android:layout_height="wrap_content"android:layout_width="wrap_content"android:src="@drawable/marcador" />

<TextView android:id="@+id/txtMensaje"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_vertical"android:textColor="#FFFFFF"android:paddingLeft="10dip" />

</LinearLayout>

Servicios de la plataforma Android

71Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Guardaremos este layout con el nombre "toast_layout.xml" en la carpeta "res\layout" denuestro proyecto.

Para asignar este layout a nuestro toast en primer lugar tenemos que inflar el layoutmediante un objeto LayoutInflater. Una vez construido el layout modificaremos losvalores de los distintos controles para mostrar la información que queramos. En nuestrocaso, tan sólo modificaremos el mensaje de la etiqueta de texto, ya que la imagen ya laasignamos de forma estática en el layout XML mediante el atributo android:src. Trasesto, sólo nos quedará establecer la duración de la notificación con setDuration() yasignar el layout personalizado al toast mediante el método setView(). A continuaciónse muestra como queda el código incluyendo todos estos cambios:

btnLayout.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View arg0) {

Toast toast3 = new Toast(getApplicationContext());LayoutInflater inflater = getLayoutInflater();View layout = inflater.inflate(R.layout.toast_layout,

(ViewGroup) findViewById(R.id.lytLayout));TextView txtMsg =

(TextView)layout.findViewById(R.id.txtMensaje);txtMsg.setText("Toast Personalizado");toast3.setDuration(Toast.LENGTH_SHORT);toast3.setView(layout);toast3.show();

}});

Si ejecutamos ahora la aplicación de ejemplo y pulsamos el nuevo botón, veremos comoel toast aparece con la estructura definida en nuestro layout personalizado.

Servicios de la plataforma Android

72Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Toast

9.2. Notificaciones de la Barra de Estado

Otro mecanismo de comunicación con el usuario son las notificaciones de la barra deestado o de la bandeja del sistema. Estas notificaciones son las que se muestran cuandorecibimos un mensaje SMS, cuando tenemos actualizaciones disponibles, cuando tenemosel reproductor de música abierto en segundo plano, etc. Constan de un icono y un textomostrado en la barra de estado superior, y adicionalmente un mensaje algo másdescriptivo y una marca de fecha/hora que podemos consultar desplegando la bandeja delsistema. Por ejemplo, cuando tenemos una llamada perdida en nuestro terminal se nosmuestra por un lado un icono en la barra de estado:

Notificación

Y cuando desplegamos la bandeja del sistema podemos ver un mensaje con másinformación, donde en este caso concreto se nos informa del evento que se ha producido("Missed call"), el número de teléfono asociado, y la fecha/hora del evento. Además, alpulsar sobre la notificación se nos dirige automáticamente al historial de llamadas.

Servicios de la plataforma Android

73Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Notificación

Como ejemplo vamos a construir una aplicación lo más sencilla posible para podercentrarnos en lo importante, nuestra aplicación tendrá un único botón que generará unanotificación de ejemplo en la barra de estado, con todos los elementos comentados y conla posibilidad de dirigirnos a la propia aplicación de ejemplo cuando se pulse sobre ella.

Para generar notificaciones en la barra de estado del sistema, lo primero que debemoshacer es obtener una referencia al servicio de notificaciones de Android, a través de laclase NotificationManager. Utilizaremos para ello el método getSystemService()

indicando como parámetro el identificador del servicio correspondiente, en este casoContext.NOTIFICATION_SERVICE.

Seguidamente configuraremos las características de nuestra notificación. En primer lugarestableceremos el icono y el texto a mostrar en la barra de estado, y registraremos lafecha/hora asociada a nuestra notificación. Con estos datos construiremos un objetoNotification. En nuestro caso de ejemplo, vamos a utilizar un icono predefinido deAndroid, el mensaje de ejemplo "Alerta!", y registraremos la fecha/hora actual, devueltapor el método System.currentTimeMillis():

//Obtenemos una referencia al servicio de notificacionesString ns = Context.NOTIFICATION_SERVICE;NotificationManager notManager = (NotificationManager)getSystemService(ns);

//Configuramos la notificaciónint icono = android.R.drawable.stat_sys_warning;long hora = System.currentTimeMillis();Notification notif = new Notification(icono, "Alerta!", hora);

El segundo paso será utilizar el método setLatestEventInfo() para asociar a nuestranotificación la información a mostrar al desplegar la bandeja del sistema (título ydescripción) e indicar la actividad a la cual debemos dirigir al usuario automáticamente siéste pulsa sobre la notificación. Los dos primeros datos son simples cadenas de texto,para el tercer dato, la actividad a la que se redirigirá, tenemos que utilizar un objetoPendingIntent. En primer lugar definimos un objeto Intent, indicando la clase de laactividad concreta a lanzar, que en nuestro caso será la propia actividad principal deejemplo (AndroidNotificacionesActivity.class). Este intent lo utilizaremos paraconstruir el PendingIntent final mediante el método PendingIntent.getActivity().

Servicios de la plataforma Android

74Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Veamos cómo quedaría el código de esta última parte:

//Configuramos el IntentContext contexto = getApplicationContext();CharSequence titulo = "Mensaje de Alerta";CharSequence descripcion = "Ejemplo de notificación.";

Intent notifIntent = new Intent(contexto,AndroidNotificacionesActivity.class);

PendingIntent contIntent = PendingIntent.getActivity(contexto, 0, notifIntent, 0);

notif.setLatestEventInfo(contexto, titulo, descripcion, contIntent);

Como opciones adicionales, también podemos indicar por ejemplo que nuestranotificación desaparezca automáticamente de la bandeja del sistema cuando se pulsasobre ella. Esto lo hacemos asignando al atributo flags de nuestra notificación el valorNotification.FLAG_AUTO_CANCEL.

También podríamos indicar que al generarse la notificación el dispositivo debe emitir unsonido, vibrar o encender el LED de estado presente en muchos terminales. Esto loconseguiríamos añadiendo al atributo defaults de la notificación los valoresDEFAULT_SOUND, DEFAULT_VIBRATE o DEFAULT_LIGHTS.

//AutoCancel: cuando se pulsa la notificación ésta desaparecenotif.flags |= Notification.FLAG_AUTO_CANCEL;

//Añadir sonido, vibración y luces//notif.defaults |= Notification.DEFAULT_SOUND;//notif.defaults |= Notification.DEFAULT_VIBRATE;//notif.defaults |= Notification.DEFAULT_LIGHTS;

En algunos casos tendremos que añadir permisos y configurar el emulador correctamentepara que funcione, como en el caso de la vibración que tendremos que añadir permisos enel manifest (<uses-permission android:name="android.permission.VIBRATE" />)Existen otras muchas opciones que se pueden consultar en la documentación oficial de laclase Notification de Android (atributos flags y defaults).

Por último, una vez tenemos completamente configuradas las opciones de nuestranotificación podemos emitirla llamando al método notify(), que recibe comoparámetros un identificador único definido por nosotros y el objeto Notification

construido.

//Enviar notificaciónnotManager.notify(NOTIF_ALERTA_ID, notif);

El identificador sirve para actualizar la notificación en un futuro (con un nuevo aviso denotificación al usuario). Si se necesita añadir una notificación más, manteniendo laanterior, hay que indicar un nuevo ID.

Ya estamos en disposición de probar nuestra aplicación de ejemplo. Para ello vamos aejecutarla en el emulador de Android y pulsamos el botón que hemos implementado con

Servicios de la plataforma Android

75Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

todo el código anterior. Si todo va bien, debería aparecer en ese mismo momento nuestranotificación en la barra de estado, con el icono y texto definidos.

Notificación

Si desplegamos la bandeja del sistema podremos verificar el resto de información de lanotificación tal como muestra la siguiente imagen:

Notificación

Por último, si pulsamos sobre la notificación se debería abrir automáticamente laaplicación de ejemplo. Además, la notificación también debería desaparecer de la bandejadel sistema, tal y como lo habíamos configurado en el código con la opciónFLAG_AUTO_CANCEL. Este cierre de la notificación también lo podríamos haberimplementado en el método onResume() de la actividad, de la forma:

Servicios de la plataforma Android

76Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

@Overrideprotected void onResume() {

super.onResume();notManager.cancel(NOTIF_ALERTA_ID);

}

A continuación se muestra un ejemplo completo de notificaciones usadas por una tareatipo AsyncTask. Este ejemplo se podría extender fácilmente a un Service: Sólo haríafalta crear una nueva MiTarea en Service.onCreate(), arrancarla conmiTarea.execute() desde Service.onStartCommand(...) y detenerla conmiTarea.cancel() desde Service.onDestroy().

private class MiTareaextends AsyncTask<String, String, String>{

public static final int NOTIF1_ID = 1;Notification notification;NotificationManager notificationManager;

@Overrideprotected void onPreExecute() {

super.onPreExecute();notificationManager =

(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);

notification = new Notification(R.drawable.icon,"Mensaje evento", System.currentTimeMillis());

}

@Overrideprotected String doInBackground(String... params) {

while(condicionSeguirEjecutando){if(condicionEvento){

publishProgress("Información del evento");}

}return null;

}

@Overrideprotected void onProgressUpdate(String... values) {

Intent notificationIntent = new Intent(getApplicationContext(),

MiActividadPrincipal.class);PendingIntent contentIntent = PendingIntent.getActivity(

getApplicationContext(), 0, notificationIntent,0);

notification.setLatestEventInfo(getApplicationContext(),values[0], contentIntent);

notificationManager.notify(NOTIF_ID, notification);}

}

9.3. Cuadros de Diálogo

Otra posible forma de notificación son los cuadros de diálogo, los cuales los podremosutilizar con distintos fines:

• Mostrar un mensaje o aviso.• Pedir una confirmación rápida.

Servicios de la plataforma Android

77Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

• Solicitar al usuario una elección (simple o múltiple) entre varias alternativas.

A parte de estas opciones también podremos personalizar completamente un diálogo paraadaptarlo a cualquier otra necesidad.

La forma de construir cada diálogo dependerá de la información y funcionalidad quenecesitemos. A continuación se muestran algunas de las formas más habituales.

9.3.1. Diálogo de Alerta

Este tipo de diálogo se limita a mostrar un mensaje sencillo al usuario, y un único botónpara confirma su lectura. Este tipo de diálogos los construiremos mediante la claseAlertDialog, y más concretamente su subclase AlertDialog.Builder. Su utilización esmuy sencilla, bastará con crear un objeto de tipo AlertDialog.Builder y establecer laspropiedades del diálogo mediante sus métodos correspondientes: título [setTitle()],mensaje [setMessage()], icono [setIcon()], y el texto y comportamiento del botón[setPositiveButton()]. Veamos un ejemplo:

private void mostrarDialogoAlerta(){

AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("Información");builder.setMessage("Esto es un mensaje de alerta.");builder.setIcon(R.drawable.dialog_icon);builder.setPositiveButton("Aceptar", new

DialogInterface.OnClickListener() {public void onClick(DialogInterface dialog, int which) {

Log.i("Dialogos", "Botón aceptar pulsado");}

});builder.show();

}

El método setPositiveButton() recibe como argumentos el texto a mostrar en el botóny la implementación del evento onClick en forma de objetoDialogInterface.OnClickListener. El diálogo se cerrará automáticamente al pulsar elbotón, por lo que no es necesario realizar ninguna acción en esta función. Si no queremosasignarle ninguna función podemos pasarle el valor null como segundo parámetro. Si nollamamos a esta función (setPositiveButton) el diálogo aparecerá sin ningún botón y elusuario tendrá que cerrarlo pulsando el botón atrás.

El aspecto de nuestro diálogo de alerta sería el siguiente:

Servicios de la plataforma Android

78Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Diálogo de alerta

9.3.2. Diálogo de Confirmación

Un diálogo de confirmación es muy similar al anterior, con la diferencia de que loutilizaremos para solicitar al usuario que nos confirme una determinada acción. Laimplementación de estos diálogos es prácticamente igual, salvo que en esta ocasióntendremos dos botones, uno de ellos para la respuesta afirmativa(setPositiveButton()), y el segundo para la respuesta negativa(setNegativeButton()). Veamos un ejemplo:

private void mostrarDialogoConfirmacion(){

AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("Confirmacion");builder.setMessage("¿Confirma la accion seleccionada?");

builder.setPositiveButton("Aceptar", newDialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int which) {Log.i("Dialogos", "Confirmacion Aceptada.");

}});

builder.setNegativeButton("Cancelar", newDialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int which) {Log.i("Dialogos", "Confirmacion Cancelada.");

}});builder.show();

}

En este caso, generamos a modo de ejemplo dos mensajes de log para poder verificar quebotón pulsamos en el diálogo. El aspecto visual de nuestro diálogo de confirmación seríael siguiente:

Servicios de la plataforma Android

79Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Diálogo de confirmación

9.3.3. Diálogo de Selección

Cuando las opciones a seleccionar por el usuario no son sólo dos, sino que el conjunto esmayor podemos utilizar los diálogos de selección para mostrar una lista de opciones entrelas que el usuario pueda elegir.

Para ello también utilizaremos la clase AlertDialog, pero indicando directamente la listade opciones a mostrar (mediante el método setItems()) y proporcionando laimplementación del evento onClick() sobre dicha lista (mediante un listener de tipoDialogInterface.OnClickListener), evento en el que realizaremos las accionesoportunas según la opción elegida. La lista de opciones la definiremos como un arraytradicional, veamos cómo:

private void mostrarDialogoSeleccion(){

final String[] items = {"Español", "Inglés", "Francés"};AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("Selecciona tu idioma");

builder.setItems(items, new DialogInterface.OnClickListener() {public void onClick(DialogInterface dialog, int item) {

Log.i("Dialogos", "Opción elegida: " +items[item]);

}});

builder.setNegativeButton("Cancelar", newDialogInterface.OnClickListener() {

public void onClick(DialogInterface dialog, int which) {Log.i("Dialogos", "Selección Cancelada.");

}});

builder.show();}

En este caso el diálogo tendrá un aspecto similar a la interfaz mostrada para los controlesSpinner. Además hemos añadido un botón "Cancelar" para poder cerrar el diálogo sinpulsar ninguna opción, aunque esto también se podría conseguir simplemente pulsando elbotón atrás.

Servicios de la plataforma Android

80Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Diálogo de selección

Para más információn sobre diálogos podéis consultar la direcciónhttp://developer.android.com/guide/topics/ui/dialogs.html.

Servicios de la plataforma Android

81Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

10. Ejercicios - Notificaciones

10.1. Notificaciones con Toast (1 punto)

En este primer ejercicio vamos a crear una aplicación de ejemplo que muestre un toastpersonalizado cada vez que se pulse un botón. Empezamos creando un proyecto Androidvacío y añadiendo un campo de edición y un botón en la actividad principal.

Cada vez que se pulse el botón se mostrará un toast por pantalla con el texto que se hayaescrito en el campo de edición. En caso de que el campo esté vacío se mostrará el aviso:"Escribe un texto". Después de cada pulsación del botón se borrará el contenido delcampo de edición.

Por último personalizaremos el aspecto y características del toast de la forma:

• El aviso deberá de aparecer en el centro de la pantalla, tanto vertical comohorizontalmente.

• Tendrá una duración corta de 2 segundos.• Modificaremos su aspecto visual asignándole un layout personalizado con las

siguientes características:• Creamos un linear layout horizontal que ocupe todo el ancho y alto, y tenga un

padding de 5dip.• El layout contendrá una imagen (androide.png) en la parte izquierda y un texto a

continuación (con un padding izquierdo y derecho de 10dip, color de texto de#FFFFFF y un layout_gravity con valor center_vertical).

• Por último asignaremos como background del layout que acabamos de crear unelemento drawable XML que también crearemos nosotros (repasar la sesión de"Drawables, estilos y temas"). Este drawable será de tipo shape, en concreto unrectángulo, con un color de fondo "#f0CCCCCC", un borde de 3dp con color"#FF0000" y esquinas redondeadas con un radio de "10dp".

10.2. Servicio con notificaciones: Números primos (1 punto)

El proyecto android-av-notificaciones de la plantilla tiene un servicio con una tareaque va calculando números primos a un ritmo lento. Vamos a mostrar una Notification

en la barra de tareas cada vez que se descubra un primo y la iremos actualizando con lallegada de cada nuevo número. Si salimos de la aplicación sin parar el servicio, seguiránapareciendo notificaciones, y si pulsamos sobre la notificación, volverá a lanzarse laactividad, cerrándose la notificación que hemos pulsado.

• Dentro del servicio MiNumerosPrimosServicio se encuentra declarada la AsyncTask

llamada MiTarea. En ella tenemos como campos de la clase una Notification y unNotificationManager. Hay que darles valores en el método onPreExecute().

Servicios de la plataforma Android

82Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

• El método doInBackground(...) ejecutará un bucle que irá incrementando i

mientras su valor sea menor de MAXCOUNT. En cada iteración, si el número es primo(función incluida en la plantilla), pedirá que se muestre el progreso, pasándole comoparámetro el nuevo primo encontrado.

• Implementar el método onProgressUpdate(...) para que muestre la notificación.Para ello habrá que actualizar la notificación con el método setLatestEventInfo, alcuál le pasaremos en un String la información del último primo descubierto y lepasaremos un PendingIntent para que al pulsar sobre la notificación, nos devuelva ala actividad de la aplicación, por si la hemos cerrado. Para crear el PendingIntentutilizaremos el método PendingIntent.getActivity(...) al cuál le tenemos quepasar un new Intent(getApplicationContext(),Main.class).

• La aplicación debería funcionar en este punto, mostrando las notificaciones yrelanzando la aplicación si son pulsadas, pero no cerrándolas al pulsarlas. Para ellosimplemente tenemos que llamar al método cancel(id) del notificationManagery pasarle la constante NOTIF_ID para que la notificación no se muestre como unanueva, sino como actualización de la que ya habíamos puesto. Una manera de hacerloes en un método estático del MiNumerosPrimosServicio, que ya está creado en lasplantillas del ejercicio y se llama cerrarMiNotificacion(NotificationManager

nm). Debes invocar este método desde el Main.onResume().

Notificación del servicio de números primos

10.3. Notificaciones mediante diálogos (1 punto)

Para practicar con las notificaciones vamos a crear una aplicación que muestre un texto ynos permita aumentar y reducir el tamaño de la letra, y modificar los colores en los que semuestra el texto.

Servicios de la plataforma Android

83Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Empezamos creando una aplicación y diseñando el layout, el cual tendrá una aparienciasimilar a la imagen inferior:

Toast

Para poder realizar scroll cuando el texto no quepa en la pantalla vamos a incluir unScrollView al layout. Sustituimos el layout principal por uno tipo scroll, el cual a su vezcontendrá un LinearLayout vertical. Es importante que el ScrollView contenga un únicoelemento, sino nos aparecerá un error.

En la parte superior colocaremos un botón del tipo ZoomControls para aumentar ydisminuir el tamaño del texto, y otro botón para cambiar los colores. Justo debajoañadiremos un campo TextView con un trozo del lorem ipsum: "Lorem ipsum dolor sitamet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et doloremagna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisiut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptatevelit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat nonproident, sunt in culpa qui officia deserunt mollit anim id est laborum."

Para disminuir y aumentar el tamaño del texto tenemos que implementar los listenerssetOnZoomOutClickListener y setOnZoomInClickListener del botón de Zoomrespectivamente. En ambos casos tendremos que obtener el tamaño actual del texto enpíxeles (getTextSize()), sumar o restar 0.5 píxels, y volver a asignarlo como tamaño:setTextSize(TypedValue.COMPLEX_UNIT_PX, newTam). En caso de que el tamaño aasignar sea inferior a 10 píxeles o superior a 40 píxeles mostraremos un cuadro de diálogoindicando que se ha superado el tamaño mínimo/máximo.

Para cambiar los colores mostraremos un cuadro de diálogo tipo selección al pulsar elbotón "Color", las posibles opciones serán tres: "Blanco/Negro, Negro/Blanco,Negro/Verde", siendo el primer color el color de fondo a asignar (que asignaremos a todoel ScrollView), y el segundo color el que asignaremos como color de texto.

Servicios de la plataforma Android

84Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Servicios de la plataforma Android

85Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

11. Depuración y pruebas

11.1. Depuración con Eclipse

11.1.1. Log y LogCat

Sin duda alguna el sistema más utilizado para depuraciones sencillas es la salida estándary los logs. En Android podemos imprimir mensajes al log tras importarandroid.util.Log. Los mensajes de Log van clasificados por una etiqueta que lesasignamos, aparte de otra información implícita, como la aplicación que los lanza, el PID,el tiempo y el nivel de debug. Se recomienda crear una constante con la etiqueta:

private static final String TAG = "MiActivity";

Para añadir al log un mensaje de nivel de información utilizaríamos Log.i():

Log.i(TAG, "indice=" + i);

Según el nivel de depuración utilizaremos una llamada de las siguientes:

• Log.v(): Verbose• Log.d(): Debug• Log.i(): Info• Log.w(): Warning• Log.e(): Error

Con esta información el Log podrá ser mostrado filtrando los mensajes menosimportantes, por ejemplo si establecemos la vista del Log a nivel de Info, los de Debug ylos Verbose no se mostrarían, pero sí los de Warning y Error que son más graves queInfo.

En Eclipse contamos con la vista LogCat (si no se muestra por defecto se puede añadir)donde podemos realizar dicho filtrado. También podemos realizar filtrado por etiquetaspara ver sólo los mensajes que nos interesan. Los mensajes van apareciendo en tiemporeal, tanto si estamos con un emulador como si estamos con un dispositivo móvilconectado por USB.

Se recomienda eliminar todas las llamadas a Log cuando se publica un programa en elAndroid Market, a pesar de que en los dispositivos de los usuarios no se vería ningún logni salida estándar.

11.1.2. Dalvik Debug Monitor Server (DDMS)

Android viene con el servidor de depuración DDMS que puede ser ejecutado desde la

Servicios de la plataforma Android

86Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

terminal o desde Eclipse. Desde la terminal del sistema se tendría que ejecutar ./ddms

que se encuentra en la carpeta tools. No es necesario si utilizamos Eclipse, que cuentacon una vista DDMS para interactuar y visualizar resultados de depuración.

En Android cada aplicación se ejecuta en su propia máquina virtual (VM) y cada VMtiene un puerto al que el debugger se conecta. Cuando se inicia DDMS, éste se conecta aadb. Cuando conectamos un dispositivo se crea un servicio de monitorización entre adb yDDMS, que notifica a DDMS cuando una VM del dispositivo arranca o termina. Cuandouna VM arranca, DDMS recoge su PID a través de adb y abre una conexión con eldebugger de la VM. Aunque cada depurador se puede conectar a un único puerto, DDMSmaneja múltiples depuradores conectados.

Vista de DDMS en Eclipse

Como se puede observar en la figura, DDMS nos permite ver los hilos que están enejecución en dada máquina virtual. También se permiten todas las características dedepuración que proporciona Eclipse para Java, como puntos de ruptura, visualización devariables y evaluación de expresiones.

DDMS cuenta con las siguientes funcionalidades y controles:

• Visualización del uso de memoria heap• Seguimiento de reservas de memoria para objetos

Servicios de la plataforma Android

87Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

• Trabajar con el sistema de ficheros del emulador o del dispositivo• Examinar la información de hilos• Profiling de métodos: seguimiento de medidas tales como número de llamadas,

tiempo de ejecución, etc.• LogCat• Emulación de operaciones de telefonía y localización• Cambiar el estado de red y simular red lenta

11.2. Pruebas unitarias con JUnit para Android

Las pruebas unitarias consisten en probar métodos y partes concretas del código,intentando independizarlas del resto del código lo máximo posible. Los proyectos de Testde Android se construyen sobre JUnit y nos proveen de herramientas que permitenrealizar no sólo pruebas unitarias sino también pruebas más amplias.

Cuando creamos un proyecto de Android con el asistente de Eclipse tenemos la opción deincluir casos de prueba en el propio proyecto. Otra manera muy común es la de separarlas pruebas en un proyecto aparte. Para ello creamos un nuevo proyecto Android Test

Project y seleccionamos el proyecto de nuestro workspace que queremos probar.

A continuación creamos casos de prueba en el mismo paquete donde se encuentra elproyecto original, o en subpaquetes de éste. Se crean con New / JUnit Test Case peroen lugar de que la clase padre sea la de JUnit, utilizaremosActivityInstrumentationTestCase2, que es la versión actualizada deActivityInstrumentationTestCase cuyo uso ya está desaconsejado.ActivityInstrumentationTestCase2 se parametriza con la clase de la actividad quevamos a probar. Por ejemplo, si vamos a probar MainActivity, tendremos:

package es.ua.jtech.av.suma.test;

import android.test.ActivityInstrumentationTestCase2;import es.ua.jtech.av.suma.MainActivity;

public class MainActivityTest extendsActivityInstrumentationTestCase2<MainActivity> {

public MainActivityTest() {super("es.ua.jtech.av.suma", MainActivity.class);

}

protected void setUp() throws Exception {super.setUp();

}

public void test1(){// asserts

}

public void test2(){// asserts

}

Servicios de la plataforma Android

88Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

// ...

protected void tearDown() throws Exception {super.tearDown();

}}

En el código anterior se llama al constructor de la clase padre con el paquete"es.ua.jtech.av.suma" y la clase MainActivity de la actividad a probar.

Con esta clase ya se pueden escribir métodos con pruebas. Sus nombres deben comenzarpor "test", por ejemplo testMiPrueba1(). Dentro de estos métodos de pruebas podemosutilizar los métodos assert de JUnit que provocan el fallo si la aserción resulta falsa. Porejemplo, assertEquals(a,b) compara los dos parámetros y si son distintos la pruebafalla. Otro ejemplo es assertTrue(c) que comprueba si es verdadero el parámetro que sele pasa.

Con JUnit se pueden probar métodos de las clases del código pero en Android podemosnecesitar probar partes de la interfaz gráfica. Para acceder a los views de la actividad lonormal sería declarar referencias a éstos como campos de la clase e inicializarlos en elmétodo setUp(). Para obtener las referencias hay que acceder a ellos congetActivity().findViewById(id)):

private Button bt;

protected void setUp() throws Exception {super.setUp();

MainActivity activity = getActivity();bt =

(Button)activity.findViewById(es.ua.jtech.av.suma.R.id.button1);}

Una vez obtenidas las referencias se pueden comprobar los valores que contienen.

assertEquals("32.3", miTextView.getText().toString());

Otra necesidad es la de simular eventos de introducción de texto, de selección, depulsación. Para ello se utiliza la clase TouchUtils

TouchUtils.tapView(this, miEditText);sendKeys("S");sendKeys("i");sendKeys( KeyEvent.KEYCODE_PERIOD );TouchUtils.clickView(this, bt);

Podéis encontrar más información enhttp://developer.android.com/reference/android/test/TouchUtils.html

Aparte de ActivityInstrumentationTestCase2 hay otras alternativas que tambiénheredan de las clases de JUnit:

• AndroidTestCase que sólo ofrece el contexto local y no el de la aplicación.

Servicios de la plataforma Android

89Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

• ServiceTestCase que se usa para probar servicios.• ActivityUnitTestCase que crea la actividad pero no la conecta al entorno, de

manera que se puede utilizar un contexto o aplicación mock.• ApplicationTestCase para probar subclases propias de Application.

Se pueden utilizar todas las características de JUnit, tales como crear una Test Suite paraagrupar distintos casos de pruebas.

11.3. Pruebas de regresión con Robotium

"Robotium es como Selenium pero para Android" es el eslogan de este software depruebas que no está afiliado ni con Android ni con Selenium. Selenium es un softwarepara pruebas de aplicaciones web que permite grabar una secuencia de acciones sobre unapágina web y después reproducirla. Una batería de pruebas de este tipo permitecomprobar que tras realizar cambios en el software, éste sigue comportándose de maneraidéntica en su interacción con el usuario.

Robotium no permite grabar las acciones del usuario sino que la secuencia de accionesdebe ser programada a través de sencillas llamadas a los métodos de Robotium. Dasoporte completo para los componentes Activity, Dialog, Toast, Menu y ContextMenu.

Usar una herramienta como Robotium nos proporciona las siguientes ventajas:

• Desarrollar casos de prueba sin necesidad de conocer el funcionamiento interno de laaplicación probada.

• Automatizar el manejo múltiples actividades de Android.• Pruebas realistas, al realizarse sobre los componentes GUI en tiempo de ejecución• Integración con Maven y Ant para ejecutar pruebas como parte de una integración

continua.

Para crear un conjunto de pruebas con Robotium debemos crear un nuevo proyecto de testde Android desde Eclipse: New / Android Test Project. Es importante crear los casosde prueba dentro del mismo paquete que el proyecto original, así que desde el asistente decreación del proyecto podemos introducir dicho paquete. También puede ser unsubpaquete del original. Otra cuestión importante para evitar errores es indicar unaversión actual del SDK de Android que sea compatible con la versión de Robotium queutilicemos.

Añadiremos el JAR de Robotium en el "build path" del proyecto. Éste puede serdescargado de www.robotium.org y tendrá un nombre similar arobotium-solo-3.1.jar (según la versión). Lo añadiremos como "jar" tras incluirlodentro del proyecto, en una carpeta lib/, por ejemplo. También será necesario marcarloen "Order and Export" en la misma pantalla de configuración del "Java Build Path".

Servicios de la plataforma Android

90Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Incluir Robotium en el Buildpath del proyecto de pruebas.

Una vez creado el proyecto de pruebas debemos crear un nuevo caso de prueba en elpaquete correcto: New / JUnit Test Case en cuyo asistente pondremos el nombre deltest que deseemos y cambiaremos la superclase del test porActivityInstrumentationTestCase2<Activity> en lugar dejunit.framework.TestCase. La Activity que pasamos como tipo puede serdirectamente la clase de la actividad que vayamos a probar, por ejemplo MainActivity.Aunque pertenezca a otro proyecto Eclipse nos permite incluirla configurandoautomáticamente el path para referenciar al otro proyecto.

En el caso de prueba creado declaramos Solo como campo privado de la clase y, si no loestán ya, sobrecargamos los métodos setUp(), tearDown y el constructor.

package es.ua.jtech.av.miproyecto.test;

import android.test.ActivityInstrumentationTestCase2;import es.ua.jtech.av.miproyecto.MainActivity;import com.jayway.android.robotium.solo.Solo;

public class TestMainActivity extendsActivityInstrumentationTestCase2<MainActivity> {

private Solo solo;

public TestMainActivity(){super("es.ua.jtech.av.miproyecto", MainActivity.class);

}

@Overrideprotected void setUp() throws Exception {

super.setUp();

Servicios de la plataforma Android

91Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

solo = new Solo(getInstrumentation(), getActivity());}

public void test1(){//...//assertTrue(comprobacion);

}

//...

@Overrideprotected void tearDown() throws Exception {

solo.finishOpenedActivities();}

}

Para ejecutar las pruebas programadas hay que hacer click sobre TestMain y en el menúcontextual seleccionar Run as / Android JUnit Test. El emulador arrancará si no estáarrancado, la aplicación a probar será instalada y ejecutada, y las acciones programadasen las pruebas serán realizadas sobre la actividad. Los resultados serán reconocidos porlas pruebas y mostradas en el resumen de resultados de JUnit.

Un ejemplo de test podría ser el de una sencilla calculadora que cuenta con dos EditTexty con un botón de sumar. El código de la prueba sería el siguiente:

public void test1(){solo.enterText(0,"10");solo.enterText(1,"22.4");solo.clickOnButton("+");assertTrue(solo.searchText("32.4"));

}

Los EditText de la actividad se corresponen (por orden) con los parámetros 0 y 1. Elbotón se corresponde con el que contiene la cadena indicada como etiqueta. Finalmente sebusca el resultado en toda la actividad.

Existen diferentes métodos con los que Solo da soporte a los componentes gráficos.Algunos ejemplos son:

• getView(id)

• getCurrentTextViews(textView)

• setActivityOrientation(Solo.LANDSCAPE)

• sendKey(Solo.MENU)

• clickOnButton(text)

• clickOnText(text)

• clickOnEditText(text)

• clearText(text)

• enterText(text)

• goBack()

• sleep(millis)

La información sobre todos los métodos está en www.robotium.org.

Servicios de la plataforma Android

92Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

11.4. Pruebas de estrés con Monkey

Monkey es un programa para pruebas que simula input aleatorio del usuario. Su propósitoes realizar una prueba de estrés de la aplicación para confirmar que, haga lo que haga elusuario con la GUI, la aplicación no tendrá un comportamiento inesperado. El input querealiza Monkey no tiene por qué tener sentido alguno.

A continuación solicitamos 1000 eventos simulados cada 100 milisegundos obteniendo lalista de ellos (opción -v) y afectará a las aplicaciones del paquete es.ua.jtech.av.

adb shell monkey -p es.ua.jtech.av -v --throttle 100 1000

Monkey simulará eventos de teclado, tanto qwerty como teclas hardware especializadas,movimientos de trackball, apertura y cierre del teclado, rotaciones de la pantalla.

Si deseamos poder reproducir una serie de acciones aleatorias, podemos fijar la semilla deinicialización aleatoria con una propia, por medio de la opción -s. Así cada vez queejecutemos con esta semilla, la secuencia de eventos aleatorios será la misma y podremosreproducir un problema tantas veces como haga falta hasta dar con la solución.

Servicios de la plataforma Android

93Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

12. Ejercicios - Depuración y pruebas

12.1. Caso de prueba con JUnit para Android (3 puntos)

En las plantillas contamos con el proyecto android-av-suma que consiste en unaactividad con dos campos de texto para introducir dos números y a continuación pulsar elbotón para visualizar el resultado en un TextView. A continuación se explica cómo crearun proyecto de pruebas JUnit.

Crea un nuevo proyecto de tipo Android Test Project y deja marcada la opción deCreate new project in workspace, llamándolo android-av-suma-test.

Crear nuevo proyecto de test de Android

Crea un nuevo caso de prueba con New / JUnit Test Case y cambia la clase de la quehereda a ActivityInstrumentationTestCase2. El nombre de la clase seráMainActivityTest y el paquete el mismo que el del proyecto que estamos probando

Servicios de la plataforma Android

94Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

pero terminado en .test. También podemos sustituir el tipo <T> por el de la clase de laactividad a probar, <MainActivity>:

Asistente para la creación de un caso de prueba

Dará error por la falta del import de MainActivity, pero se puede importar porque el otroproyecto está en el build path del proyecto de pruebas. También debemos cambiar elconstructor para constuir el caso de prueba con el paquete y la actividad a probar:

public MainActivityTest() {super("es.ua.jtech.av.suma", MainActivity.class);

}

Vamos a añadir como campos de la clase objetos que referencien los views de nuestraactividad:

private EditText et1, et2;private TextView tv;private Button bt;

Los inicializaremos en el método setUp(), a partir de la actividad que se obtiene congetActivity():

protected void setUp() throws Exception {

Servicios de la plataforma Android

95Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

super.setUp();MainActivity activity = getActivity();et1 =

(EditText)activity.findViewById(es.ua.jtech.av.suma.R.id.editText1);et2 =

(EditText)activity.findViewById(es.ua.jtech.av.suma.R.id.editText2);tv =

(TextView)activity.findViewById(es.ua.jtech.av.suma.R.id.textView3);bt = (Button)

activity.findViewById(es.ua.jtech.av.suma.R.id.button1);}

Vamos a añadir dos métodos de test, uno de ellos va a comprobar que los componentesgráficos estén bien inicializados y el otro va a introducir unos datos en la interfaz y va acomprobar que el resultado es el correcto:

@SmallTestpublic void testViewsCreados(){

assertNotNull(et1);assertNotNull(et2);assertNotNull(tv);assertNotNull(bt);assertEquals("", et1.getText().toString());assertEquals("", et2.getText().toString());assertEquals(" ...", tv.getText().toString());

}

@SmallTestpublic void testSuma(){

TouchUtils.tapView(this, et1);sendKeys("1");sendKeys("0");TouchUtils.tapView(this, et2);sendKeys("2");sendKeys("2");sendKeys( KeyEvent.KEYCODE_PERIOD );sendKeys("3");TouchUtils.clickView(this, bt);assertEquals("32.3", tv.getText().toString());

}

Para ejecutar la prueba hay que hacer click en Run as/ Android JUnit Test. Elresultado debe salir en verde:

Resultado de JUnit en verde

Servicios de la plataforma Android

96Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.

Servicios de la plataforma Android

97Copyright © 2012-13 Dept. Ciencia de la Computación e IA All rights reserved.