Está en la página 1de 28

CURSO DE DESARROLLO DE APLICACIONES ANDROID

Tema 10

Proveedores de Contenido y Servicios


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Proveedores de Contenido

Los proveedores de contenido gestionan el acceso a conjuntos de datos estructurados,


encapsulando el acceso a los mismos de forma segura. Son las interfaces estándar para
conectar datos de un proceso con código que se ejecuta en otro proceso.

Cuando se quiere acceder a datos de un proveedor de contenido, se utiliza un objeto de tipo


ContentResolver que actuará como cliente del proveedor de contenido, en el contexto de la
aplicación (Aplicación A). El proveedor de contenido, clase que extiende de ContentProvider,
recibirá las peticiones de datos del cliente, realizará las operaciones necesarias para
obtenerlos, y devolverá dichos resultados.

En general, no será necesario desarrollar proveedores de contenido a no ser que se desee


compartir datos de la aplicación con otras aplicaciones. Por otro lado, sí que será necesario
implementar un proveedor de contenido propio en el caso de que se desee proporcionar
resultados de búsqueda personalizados dentro de la propia aplicación o en el caso de querer
extender la funcionalidad estándar de copiar y pegar datos o archivos entre aplicaciones.

Algunos de los proveedores de contenido que ya implementa Android 1 son los siguientes:

• AlarmClock: permite establecer alarmas.


• Browser: permite acceder a los favoritos y al historial del navegador web.
• Calendar: permite acceder a todos los elementos de un evento, recordatorios,
asistentes, zonas horarias, etc.
• CallLog: permite acceder al registro de llamadas entrantes y salientes.
• Contacts: permite acceder a la información de los contactos.

1
Los proveedores de contenido que proporciona Android pueden consultarse en:
http://developer.android.com/reference/android/provider/package-summary.html

CURSO DE DESARROLLO DE APLICACIONES ANDROID 2


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

• MediaStore: permite acceder a todo el contenido multimedia público almacenado en


la memoria interna y en la memoria externa de los dispositivos.
• SearchRecentSuggestions: clase que permite, al ser extendida, crear sugerencias de
búsqueda según se escribe, basadas en consultas recientes y vistas recientes.
• Settings: permite acceder a la configuración global del dispositivo.
• UserDictionary: permite acceder a las palabras definidas por el usuario, para usarlas
en dispositivos de entrada de texto predictivos.
• Voicemail: permite el acceso al buzón de voz.

En general, los proveedores de contenido presentarán los datos a otras aplicaciones en forma
de una o varias tablas, de forma similar a las tablas de una base de datos relacional. Cada fila
de la tabla representará los datos de una instancia del tipo de objeto que el proveedor de
contenido recopila, y cada columna representará cada uno de los tipos de datos que el tipo de
objeto contiene.

Por ejemplo, en el caso de la agenda de teléfonos, los datos contenidos en el proveedor de


contenido tendrán un aspecto parecido a la siguiente tabla.

_ID TYPE NUMBER DISPLAY_NAME …


1 TYPE_HOME 901234567 Mi casa
2 TYPE_MOBILE 601234567 Mi móvil
3 TYPE_OTHER 801234567 Mi taller
4 TYPE_FAX_WORK 701234567 Mi fax
5 TYPE_WORK 501234567 Mi trabajo

En la tabla anterior, cada fila representa una instancia de un contacto, mientras que cada
columna representa cada uno de los datos almacenados en el contacto. Los nombres de la
cabecera de la tabla son los nombres que almacena el proveedor de contenido y que servirán
para localizar y acceder a cada uno de los datos.

La columna _ID representa la clave primaria para acceder a cada una de las instancias del tipo
de objeto que recopila el proveedor y será utilizada automáticamente por ListView en caso
de enlazar los resultados con este tipo de vista 2.

Para acceder a los datos de un proveedor de contenido 3 se utilizará un objeto de tipo


ContentResolver, que contendrá los mismos métodos que su proveedor de contenido. Estos
métodos darán acceso a las típicas operaciones CRUD (create, retrieve, update, delete –crear,
recuperar, actualizar, borrar) sobre los objetos del contenedor o almacén persistente.

2
La clave primaria no suele faltar en las tablas de una base de datos relacional. No obstante, un proveedor de
contenido podría no tener clave primaria (es decir, no tendría la columna _ID).
3
Todos los proveedores de contenido extienden de ContentProvider.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 3


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

El objeto ContentResolver actúa, por lo tanto, como una capa de abstracción entre los datos
de un repositorio y la representación externa de los mismos.

Para acceder a los datos de un proveedor de contenido, se invocará al método query() del
ContentProvider, que recibirá los siguientes métodos:

• URI (Uniform Resource Identifier), dirección de la tabla a consultar en el proveedor de


contenido.
• Proyección, o lista de columnas que devolverá el proveedor de contenido.
• Selección, o filtro equivalente a la cláusula WHERE en SQL (a continuación de
“WHERE”, y sin incluir el nombre de la tabla).
• Argumentos de la selección, valores de tipo String que serán utilizados por el filtro.
• Ordenación, de los resultados obtenidos.

Por ejemplo, en el caso de querer acceder desde una actividad a la agenda para obtener una
lista de contactos, se implementaría una consulta similar a la siguiente:

Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;

String[] proyeccion = { ContactsContract.Contacts.DISPLAY_NAME,


ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.Contacts._ID };
String select = "";
String[] selectArgs = null;
String ordenacion = ContactsContract.Contacts.DISPLAY_NAME + " DESC";

Cursor contactos = getContentResolver().query(uri, proyeccion,


select, selectArgs, ordenacion);

URIs de contenido

Una URI de contenido es una URI 4 que identifica datos de un proveedor de contenido. Incluye
el nombre del proveedor de contenido (la autoridad) así como el nombre de la tabla (el path).

La constante CONTENT_URI del ejemplo anterior contiene la URI de la tabla de Contactos del
dispositivo, la cual es pasada como parámetro al ContentResolver, quien analizará la
autoridad contenida en la URI contrastándola con las tablas del sistema, para averiguar a qué
proveedor de contenido pasarle el resto de parámetros que recibe. El ContentProvider
analizará el path contenido en la URI para determinar las tablas que consultará, y utilizará en la
consulta el resto de parámetros que le ha pasado el ContentResolver. En general, cada
proveedor de contenido tiene un path por cada tabla que expone.

4
Uniform Resource Identifier, cadena corta de caracteres que identifica inequívocamente un recurso

CURSO DE DESARROLLO DE APLICACIONES ANDROID 4


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

En el ejemplo anterior, la URI es content://com.android.contacts/contacts. Se llama


esquema (scheme) al principio de la URI: “content:”. Todos los proveedores de contenido en
Android tendrán el mismo esquema de URI. El esquema siempre se separa por una doble barra
inclinada (“//”) de la autoridad (“com.android.contacts”) y, por último, esta se separa del
path (“contacts”) por una barra simple inclinada (“/”). El path, a su vez, puede ser
compuesto.

En la mayoría de los casos, los proveedores de contenido permiten acceder a una fila concreta
de sus tablas añadiendo, al final de la URI, el valor del identificador de la columna _ID. Esta
característica permitirá actualizar o borrar filas de una tabla de un proveedor de contenido
desde las aplicaciones. Para ayudar a construir URIs bien formadas desde cadenas de texto,
Android provee las clases Uri, Uri.Builder y ContentUris.

Por ejemplo, para concatenar un _ID y extraer la URI de un contacto concreto, se utilizaría un
código similar al siguiente 5:

Uri contactoSimple = ContentUris.withAppendedId(


ContactsContract.Contacts.CONTENT_URI, 2);

Obtención de datos de un proveedor de contenidos

Como todas las operaciones que pueden llegar a ser largas, las consultas a proveedores de
contenidos deben realizarse siempre en un segundo hilo que no bloquee el hilo principal de la
aplicación, para que la interfaz de usuario no se quede bloqueada. Por lo tanto, en general, las
consultas a proveedores de contenido se asociarán a CursorLoader 6.

Para poder acceder a los proveedores de contenido, primero es necesario solicitar permiso a
través del manifiesto de la aplicación 7, ya que no se puede solicitar el mismo en tiempo de
ejecución. Para cada proveedor se deberá solicitar el permiso adecuado, en función de las
operaciones que vaya a realizar la aplicación sobre los datos que obtenga del mismo. Por
ejemplo, en el caso de querer visualizar los contactos del teléfono, una aplicación deberá
declarar el siguiente permiso en el manifiesto:

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

Si se quisiera además añadir, modificar o borrar contactos de la agenda, se deberá solicitar el


permiso android.permission.WRITE_CONTACTS.

5
Lo cual dará como resultado una Uri con el formato content://authority/path/id
6
Es necesario recordar que un CursorLoader es un Loader que pasa la consulta al ContentResolver y que
devuelve un Cursor.
7
La confirmación de estos permisos será responsabilidad del usuario propietario del dispositivo que deberá
aceptarlos al instalar la aplicación. Si no acepta las solicitudes de permisos que se mostrarán al instalar la aplicación,
la instalación no se realizará.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 5


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

El segundo paso para poder obtener datos de un proveedor de contenidos es la definición de


la query que será pasada al proveedor de contenido vía ContentResolver. Esta query
devolverá un Cursor con los datos obtenidos en la consulta. Como se ha mencionado
previamente, a partir del nivel de API 11 (Android 3.0), se puede utilizar CursorLoader para
encapsular la consulta al proveedor de contenido en un loader de forma que no se bloquee la
interfaz de usuario de la aplicación al realizar la consulta. En este caso, la inicialización de la
carga de datos del proveedor se realizará en el método onCreateLoader().

Filtros

Como en toda consulta a una tabla, se pueden establecer filtros que se pasarán al método
query(), a través de sus parámetros Selección (String) y Argumentos de la selección
(String[]), previamente explicados.

Por ejemplo, se podría implementar una vista de tipo EditText en la cual poder introducir un
texto para filtrar la lista de contactos cargada. Teniendo en cuenta que el parámetro Selección
corresponde a la cláusula WHERE, para la tabla de la consulta, en este caso sería:

String select = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? ";


String[] selectArgs = {"%" +
findViewById(R.id.filtroNombre).getText().toString() + "%"};

Al pasar esos argumentos a la consulta, se estará realizando un filtro sobre la columna


DISPLAY_NAME del tipo “WHERE DISPLAY_NAME LIKE %texto%”. El caracter comodín “%”
permite buscar cadenas que coincidan por la izquierda y/o por la derecha con el texto del filtro
(sin tener en cuenta las mayúsculas o minúsculas, gracias a “LIKE”), por lo que, por ejemplo,
se darán los resultados mostrados en la siguiente tabla:

filtroNombre DISPLAY_NAME ¿Obtenido en la consulta?


“%cAs%” Mi casa Sí
“i mó%” Mi móvil No (no coincide por la izquierda)
“Mi %” Mi taller Sí (sí coincide por la izquierda)
Mi fax Sí
“%a%”
Mi trabajo Sí

Es necesario comentar que el lenguaje SQL permite también crear cláusulas WHERE
directamente a través del parámetro Selección como, por ejemplo:

select = "DISPLAY_NAME = ’Mi casa’";

CURSO DE DESARROLLO DE APLICACIONES ANDROID 6


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Sin embargo, este tipo de consultas son peligrosas debido a que permiten la inyección de
código malicioso 8 en la propia consulta SQL, por lo que nunca deberán ser utilizadas.

Mostrar resultados de una consulta

Como ya se ha mencionado, el método ContentResolver.query() devuelve un objeto de


tipo Cursor que contendrá las columnas especificadas en el parámetro proyection para todas
aquellas filas de la tabla que coincidan con los criterios de selección y proveerá acceso
aleatorio a todas las filas devueltas. A través de sus métodos, se podrán recorrer las mismas y
obtener los tipos de datos contenidos en las columnas, así como otras propiedades. Existen
algunas implementaciones de Cursor que actualizarán automáticamente los datos obtenidos
en la consulta en el caso de que cambien en el proveedor de contenido y/o que notifiquen
dichos cambios a través de un objeto observador 9.

Si la consulta realizada no da ningún resultado, en función del proveedor de contenidos, se


obtendrá o bien una Exception o bien un Cursor vacío (Cursor.getCount() = 0).

La forma más óptima de mostrar los resultados de una consulta será a través de un
SimpleCursorAdapter que mostrará los resultados en una ListView. Por ejemplo, en el caso
de la consulta realizada a la agenda de contactos, se implementaría un código similar al
siguiente:

Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;


String[] proyeccion = {
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER,
ContactsContract.Contacts._ID};
String select = null;
String[] selectArgs = null;

if (null != filtro && !filtro.equals("")) {


select = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? ";
selectArgs = new String[1];
selectArgs[0] = "%" + filtro + "%";
}

String ordenacion = ContactsContract.Contacts.DISPLAY_NAME + " ASC";

8
Por ejemplo, si en el filtro se escribe “nothing; drop table PHONE”, la query que se obtendría sería “SELECT *
FROM TABLE PHONE WHERE DISPLAY_NAME = nothing; drop table PHONE”, inyectándose la sentencia “drop
table PHONE” que borraría todos los datos de la tabla PHONE.
9
Que implementará el patrón Observer el cual define una dependencia del tipo 1-N entre objetos, de manera que
cuando uno de los objetos cambia su estado, el observador notifica este cambio a todos los objetos dependientes.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 7


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

// Obtención del cursor


mCursorContactos = getContentResolver().query(uri, proyeccion, select,
selectArgs, ordenacion);

// Definición de lista de columnas para obtener del Cursor y cargar en


// la lista
String[] columnasListaContactos = {
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.Contacts.HAS_PHONE_NUMBER,
ContactsContract.Contacts._ID
};

// Lista de IDs que contendrán las columnas del Cursor para cada fila
int[] itemsListaContactos = {
R.id.nombreContacto,
R.id.tipoTelefono,
R.id.numeroTelefono,
R.id.idContacto};

// Creación de un SimpleCursorAdapter
mAdaptadorContactos = new SimpleCursorAdapter(
getApplicationContext(), // Contexto de la aplicación
R.layout.lista_contactos, // Layout en XML para una
// fila en el ListView
mCursorContactos, // Resultado de la consulta al
//proveedor de contenidos
columnasListaContactos, // Array de strings de los
// nombres de las columnas en el
// cursor
itemsListaContactos);

// Se establece el adaptador para la ListView


setListAdapter(mAdaptadorContactos);

Inserción, modificación y borrado de datos de un proveedor de contenidos

Inserción

Para insertar nuevas filas en un proveedor de contenido, se invocará al método


ContentProvider.insert(). Este método devolverá la URI de la nueva fila 10 y recibirá como
parámetros la URI del proveedor de contenido así como un objeto de tipo ContentValues que
contendrá los valores de la nueva fila. Para poblar este objeto, se utilizará su método put()
cuyos parámetros serán el nombre de la columna y el valor que contendrá. Por ejemplo:

10
Es decir, una URI con el formato content://authority/path/id. Se podrá obtener el _ID con
ContenUris.parseID()

CURSO DE DESARROLLO DE APLICACIONES ANDROID 8


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

ContentValues valoresNuevoContacto = new ContentValues();

valoresNuevoContacto.put(
ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, "Name Primary");
valoresNuevoContacto.put(
ContactsContract.Contacts.DISPLAY_NAME, "Name ");
valoresNuevoContacto.put(
ContactsContract.Contacts.HAS_PHONE_NUMBER, "No");

Uri uriNuevoContacto = getContentResolver().insert(


ContactsContract.Data.CONTENT_URI, valoresNuevoContacto);

Si se quiere insertar algún valor nulo, se podrá utilizar ContenValues.putNull(columna).

Como se podrá observar, no se ha de añadir la columna _ID a ContentValues, puesto que se


genera automáticamente al ser usualmente la clave primaria de la tabla.

Actualización

Para actualizar filas, se invocará al método ContentProvider.update(), que devolverá el


número de filas actualizadas y recibirá como parámetros la URI del proveedor de contenido,
los valores a actualizar y los criterios de selección de las filas a actualizar. Por ejemplo, con el
siguiente código se actualizaría a valor nulo el nombre de todos los contactos que tuvieran
“AB” dentro del nombre:

String select = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? ";


String[] selectArgs = {"%AB%"};

ContentValues valoresUpdateContactos = new ContentValues();

valoresUpdateContactos.putNull(ContactsContract.Contacts.DISPLAY_NAME);

int numeroContactosActualizados = getContentResolver().update(


ContactsContract.Data.CONTENT_URI,
valoresUpdateContactos,
select,
selectArgs);

Borrado

El borrado de datos es similar. Se utilizará el método ContentProvider.delete(), el cual


devolverá el número de filas borradas y recibirá como parámetros la URI del proveedor de
contenido y los criterios de selección de las filas a borrar. Por ejemplo, con el siguiente código
se borrarían todos los contactos que tuvieran el nombre igual a “AB”:

CURSO DE DESARROLLO DE APLICACIONES ANDROID 9


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

String select = ContactsContract.Contacts.DISPLAY_NAME + " LIKE ? ";


String[] selectArgs = {"AB"};

int numeroContactosBorrados = getContentResolver().delete(


ContactsContract.Data.CONTENT_URI,
select,
selectArgs);

Accesos delegados y alternativos a proveedores de contenido

Existen diversas formas de obtener los datos de un proveedor de contenido, incluso sin la
necesidad de solicitar los permisos necesarios a través de la publicación de los mismos en el
manifiesto de la aplicación. En el tema anterior ya se estudió el acceso a proveedores en hilos
separados, a través de loaders, para no bloquear la interfaz de usuario. Además, se puede
acceder por lotes (acceso batch) así como a través de Intents que delegarán en la propia
aplicación propietaria del proveedor de contenido quien, en general, proveerá una interfaz
mucho más adecuada para manejar los datos necesarios de dicho proveedor.

Los accesos por lotes son útiles cuando se pretende insertar, modificar o incluso borrar gran
cantidad de datos en un proveedor de contenido, en una o más tablas, en el contexto de una
transacción. Para realizar este tipo de operaciones, se invocará a
ContentResolver.applyBatch(), método que recibirá como parámetro un String que
determine la autoridad del proveedor (y no una URI), así como un ArrayList de
ContentProviderOperation, objetos que podrán operar sobre diferentes tablas del
proveedor.

Acceso a proveedores vía Intents

Una aplicación puede ofrecer acceso a los datos de un proveedor sin necesidad de requerir
permisos especiales por parte del usuario a la hora de instalarla. Para ello, bastará con lanzar
un Intent contra el proveedor de contenido para mostrar su interfaz o para obtener los
resultados del Intent de vuelta en la aplicación.

Como resultado obtenido de vuelta en la aplicación, al lanzar el Intent, se recibirá una URI
con permisos temporales 11 (de lectura o escritura), de los datos solicitados, al recibir uno de
estos flags en el resultado: FLAG_GRANT_READ_URI_PERMISSION, o
FLAG_GRANT_WRITE_URI_PERMISSION.

Por ejemplo, para obtener contactos del proveedor de contactos sin necesidad de solicitar el
permiso READ_CONTACTS en el manifiesto de una aplicación, se podrá enviar un Intent con la
acción ACTION_PICK y el tipo MIME de los contactos
(ContactsContract.RawContacts.CONTACTS_ITEM_TYPE) a través del método de la

11
Estos permisos tendrán validez mientras la actividad no sea destruida.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 10


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

actividad startActivityForResult(Intent intent, int requestCode). Este Intent


invocará a la aplicación de Contactos, que aparecerá en primer plano, permitiendo seleccionar
un contacto. Cuando se realice la selección, la aplicación de Contactos invocará al método
setResult(int resultCode, Intent intent) que establecerá el Intent que recibirá la
aplicación inicial, cuyo contenido constará de la URI del contacto seleccionado, así como el flag
de lectura temporal. A continuación, la aplicación de Contactos se cerrará, mostrándose la
aplicación inicial, que recibirá, a través del método onActivityResult(), el Intent generado
en la aplicación de Contactos. Con la URI que contendrá dicho Intent se podrá acceder al
contacto seleccionado mientras la actividad no sea destruida, gracias al flag de lectura
temporal.

Incluso si la aplicación posee permisos de acceso a la información de un proveedor, será útil


utilizar un Intent para mostrar la información de dicho proveedor en la interfaz propia del
proveedor, de forma que no sea necesario desarrollar otra interfaz para mostrar dichos datos
(muchos proveedores de contenido aceptan la acción ACTION_VIEW para mostrar un ítem de
su almacén).

Mediante procedimientos similares se podrá incluso modificar información de proveedores de


contenido, sin necesidad de que la aplicación obtenga permisos de escritura. Se podrán enviar
Intents a proveedores de contenido con la acción ACTION_INSERT y con datos adicionales que
utilizará la aplicación de destino para rellenar los campos de su propia interfaz de usuario.

Tipos de datos de los proveedores de contenido

Los proveedores de contenido pueden contener datos de tipo String, enteros (Integer, int),
enteros largos (long), decimales (float) y decimales largos (double). Además, también
pueden almacenar objetos de tipo BLOB 12 de hasta 64 KB, short y null. La clase Cursor
ofrece métodos accesores (tipo get()) para cada uno de estos tipos de datos.

Pese a que el tipo de dato de cada una de las columnas debe quedar especificado en la
documentación de cada proveedor de contenido, se podrá averiguar el mismo a través del
método Cursor.getType(int columnIndex) que devolverá las siguientes constantes:

FIELD_TYPE_NULL (= 0)
FIELD_TYPE_INTEGER (= 1)
FIELD_TYPE_FLOAT (= 2)
FIELD_TYPE_STRING (= 3)
FIELD_TYPE_BLOB (= 4)

Además de estos tipos básicos de datos, los proveedores pueden ofrecer tipos complejos de
datos, o tipos de datos MIME13. Para cada URI que defina un proveedor, existirá un tipo de

12
BLOB: Binary Large OBject
13
definidos originalmente para el intercambio a través de internet de todo tipo de archivos (audio, vídeo, texto
enriquecido, etc.)

CURSO DE DESARROLLO DE APLICACIONES ANDROID 11


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

dato MIME, que se podrá obtener a través de ContentResolver.getType(). En general, una


aplicación que acceda a un proveedor de contenido que mantenga estructuras de datos
complejas deberá averiguar el tipo MIME que ofrece para cada URI. Por ejemplo, en el caso del
proveedor de Contactos, existe un tipo MIME que etiqueta cada uno de los tipos de datos de
contacto almacenados en cada fila.

Existen tipos MIME estándar (para contenidos multimedia) y tipos MIME propietarios. Un
proveedor de contenido podrá devolver cualquiera de ellos, o ambos.

El formato de los tipos MIME es tipo/subtipo, como por ejemplo, text/html. Las
peticiones de contenido a un proveedor que devuelva este tipo MIME en la URI accedida
devolverán texto con etiquetas HTML.

Los tipos MIME propietarios, o específicos del proveedor (“vendor-specific”), son más
complejos, aunque el tipo siempre será:

vnd.android.cursor.dir, para filas múltiples (o tablas)


vnd.android.cursor.item, para una única fila.

El subtipo será específico del proveedor y podrán estar basados en la autoridad (authority) del
proveedor, así como en los nombres de las tablas a los cuales se accede.

En el caso de los Contactos de Android, cuando se crea una fila para insertar un teléfono en su
correspondiente tabla, se establece el siguiente tipo MIME para dicha fila:

vnd.android.cursor.item/phone_v2.

En el caso de un proveedor de contenidos con otro tipo de datos más complejos, como por
ejemplo, un proveedor que contenga inventarios de elementos de instalaciones deportivas,
que tendría tablas para almacenar datos de elementos dentro de los gimnasios, de canchas
multipropósito, o de vestuarios, podría definir la autoridad com.instalacdeps.inventario y
las tablas Gimnasio, Cancha, Vestuario. Entonces, el tipo MIME de la URI

content://com.instalacdeps.inventario/Gimnasio

podría ser

vnd.android.cursor.dir/vnd.instalacdeps.gimnasio,

mientras que para la URI

content://com.instalacdeps.inventario/Cancha/3

sería

vnd.android.cursor.item/vnd.instalacdeps.cancha.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 12


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Servicios

Los servicios, que se ejecutan en el hilo principal de la aplicación, son componentes diseñados
para realizar tareas en segundo plano que no requieran interfaz de usuario. Estas tareas
podrán ser, por ejemplo, lecturas y escrituras de archivos, interacciones con proveedores de
contenido, reproducción de música en segundo plano, etc.

Los servicios pueden ser iniciados por componentes de la aplicación o, incluso, por
componentes de otras aplicaciones a través del lanzamiento de Intents y, además, pueden
permitir que dichos componentes se les unan con el objetivo de permitir una comunicación
entre el servicio y el componente unido.

Existen dos posibles formas para los servicios:

1. Servicios iniciados, invocados por un componente a través de startService().


Este tipo de inicialización hará que el servicio se ejecute indefinidamente aun
cuando el componente que lo inició haya sido destruido. Generalmente, un
servicio diseñado de esta forma realizará una única tarea en segundo plano (por
ejemplo, grabar un archivo en la tarjeta SD) y, a continuación, deberá pararse por
sí mismo para liberar recursos.
2. Servicios enlazados, invocados por componentes que se les unen (bind) a través de
bindService(). Este tipo de servicio ofrece una interfaz cliente-servidor que
permite a los componentes clientes interactuar con el servicio, enviando
peticiones y recibiendo respuestas del mismo. Permite también realizar estas
acciones entre diferentes procesos a través de comunicación inter-proceso (IPC).
Un servicio enlazado se ejecuta mientra tengan algún cliente unido a sí mismo.
Cuando el último cliente se haya desconectado, el servicio será destruido por el
sistema, a no ser que haya sido iniciado previamente vía startService().

Un servicio podrá adoptar cualquiera de estas dos formas, o ambas. Bastará para ello que el
servicio implemente los métodos callback onStartCommand() y/o onBind() que serán
invocados por el sistema cuando se invoquen los métodos startService() y/o
bindService(), respectivamente, métodos que reciben de parámetro un Intent, entre otros.

Al igual que las actividades, los servicios han de ser declarados en el manifiesto de la aplicación
propietaria pudiendo ser definidos como privados de forma que solo sean accesibles desde la
propia aplicación.

Debido a que los servicios podrán realizar tareas de larga duración, en general deberán
implementar un segundo hilo de trabajo dentro de sí mismos para no bloquear el hilo principal
de la aplicación. Aun así, se deberá analizar adecuadamente la necesidad de crear un servicio
para realizar ciertas tareas. Si la tarea o proceso se ejecutará indefinidamente aunque la
actividad que la haya lanzado se cierre, será necesario crear un servicio. En cambio, si dicha

CURSO DE DESARROLLO DE APLICACIONES ANDROID 13


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

tarea o proceso únicamente ha de ejecutarse mientras la actividad esté viva, únicamente será
necesario crear un hilo en segundo plano 14.

Todos los servicios extenderán de la clase Service o de una subclase de esta. Será necesario
sobrescribir los métodos callback que el sistema invocará en distintos momentos del ciclo de
vida del servicio, y que además proveerán el mecanismo de unión al servicio, en caso de que
sea necesario.

Los servicios deberán ser declarados en el manifiesto de la aplicación, del mismo modo que las
actividades:

<application …>
<service android:name=".DownloaderService"></service>
</application>

Existen más atributos tales como los permisos requeridos para iniciar el servicio o el proceso
sobre el cuál se ejecutará, aunque el atributo android:name es el único atributo obligatorio (y
nunca deberá ser cambiado, una vez la aplicación sea publicada en Google Play).

Al igual que las actividades, y tal y como se estudiará detenidamente en un próximo tema, los
servicios también podrán definir filtros de Intents que permitan a otros componentes usar el
servicio utilizando Intents implícitos que serán pasados a cualquiera de los métodos del
contexto de la aplicación, startService() o bindService().

Si se desea hacer el servicio privado, no se declarará ningún filtro de Intent, de forma que
sólo se podrá invocar al servicio por medio de un Intent explícito (pasando el nombre de la
clase del servicio) y, además, se declarará el atributo android:exported="false" que, por
otro lado, permitirá la declaración y uso de filtros de Intents, accesibles solo dentro de la
propia aplicación.

Por último, los servicios podrán enviar notificaciones de eventos al usuario a través de Toast o
notificaciones en la barra de estado.

Ciclo de vida de un servicio

El ciclo de vida de un servicio es bastante más sencillo que el de una actividad, aunque deberá
prestarse mayor atención a cómo y cuándo se crea y destruye puesto que el servicio puede
ejecutarse en segundo plano sin que el usuario lo perciba.

Un servicio podrá seguir, básicamente, dos líneas en su ciclo de vida, según haya sido iniciado
vía startService() o se hayan unido componentes a él vía bindService(). Cualquiera de

14
Los hilos y procesos se tratarán en el próximo tema.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 14


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

estos métodos creará el servicio (en el caso de que no haya sido ya creado 15). En el primer
caso, el servicio finalizará cuando él mismo invoque a su método stopSelf() o bien se
invoque desde el contexto a stopService(). En el segundo caso, el servicio finalizará cuando
todos los clientes que estén unidos se desvinculen del mismo.

No obstante, a un servicio iniciado se le podrán unir posteriormente componentes por lo que


dicho servicio no será finalizado (destruido) cuando se invoque a stopService() o
stopSelf() hasta que el último cliente se desvincule del mismo.

El siguiente diagrama muestra los elementos principales del ciclo de vida de un servicio:

15
Es decir, la primera vez que se invoque cualquiera de esos métodos, se invocará al método callback onCreate()
del servicio. En sucesivas llamadas a dichos métodos, no se invocará a dicho método callback.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 15


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Se describen, a continuación, los métodos callback básicos del ciclo de vida de un servicio:

• onCreate(): método invocado únicamente cuando se crea el servicio y que, al igual


que en el caso de las actividades, estará dedicado a inicializar recursos y configurar el
servicio. Si el servicio ya se está ejecutando, este método no será invocado. No recibe
ningún parámetro.
• onStartCommand(): método invocado por el sistema cuando un componente invoca a
startService(). Cuando la ejecución de este método concluya, el servicio estará
iniciado y estará ejecutándose en segundo plano de forma indefinida. Si este método
es implementado, será necesario parar el servicio manualmente cuando finalice su
tarea, bien a través del método stopSelf() (del servicio), o bien a través del método
stopService() (del Ccntexto de la aplicación). Recibe, entre otros parámetros, un
Intent.
• onBind(): método invocado por el sistema cuando un componente quiere unirse al
servicio, lo cual podrá solicitar invocando al método del contexto bindService(). En
caso de permitir la unión al servicio, este método devolverá una interfaz IBinder que
será usada por los componentes unidos para poder comunicarse con el servicio. En
caso de no permitirse la unión al servicio, este método devolverá null. Recibe un
Intent como parámetro.
• onUnbind(): método invocado por el sistema cuando todos los clientes se han
desvinculado del servicio. Recibe un Intent como parámetro y devolverá un valor
booleano que indicará si se permite o no invocar posteriormente el método
onRebind().
• onDestroy(): último método que será invocado por el sistema antes de destruir el
servicio. En él, deberán liberarse recursos tales como conexiones, receptores de
broadcasts, hilos, etc.

A diferencia de los métodos callback de una actividad, no es necesario invocar a la


implementación de los métodos de la superclase.

Existe otro importante método callback, onRebind(), que será invocado cuando nuevos
clientes se conecten al servicio, después de haberse notificado que todos los anteriores
clientes ya se habían desconectado, siempre y cuando onUnbind() devuelva true.

Creación de un servicio

Tal y como ya se ha mencionado, un componente podrá iniciar un servicio al invocar al método


del contexto startService() pasando como parámetro un Intent que especificará el servicio
a iniciar (bien por medio de un filtro, bien directamente invocando al nombre del servicio) así
como los datos que el servicio necesite manejar. El servicio recibirá este Intent en el método

CURSO DE DESARROLLO DE APLICACIONES ANDROID 16


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

onStartCommand() 16. Al tener su propio ciclo de vida, el servicio seguirá existiendo pese a que
el componente que lo haya invocado haya sido destruido, y finalizará cuando se invoque al
método del contexto stopService() o él mismo invoque a stopSelf().

Es necesario recordar que los servicios se ejecutan en el mismo proceso que el hilo principal de
la aplicación, por lo que si van a realizar tareas de larga duración deberán utilizar otro hilo
distinto que será instanciado dentro del servicio.

En general, para implementar un servicio, se extenderá cualquiera de las siguientes clases:

1. IntentService. Subclase de Service que ya implementa un worker o hilo de trabajo


separado del hilo principal de la aplicación y que podrá gestionar instancias múltiples del
servicio ya que el worker las encolará y procesará una por una. Es útil si no se necesita
gestionar peticiones múltiples simultáneas. Se deberá implementar el método
onHandleIntent() que recibirá cada uno de los Intents.
Los servicios que extiendan esta clase:
• Utilizarán un worker por defecto separado del hilo principal de la aplicación, que
ejecutará todos los Intents recibidos en onStartCommand() de forma secuencial.
• Utilizarán una cola que enviará un único Intent cada vez a onHandleIntent(). No se
enviará otro Intent hasta que dicho método termine la ejecución de la tarea
desencadenada por el Intent anterior.
• Invocarán automáticamente a stopSelf() cuando finalice la cola de Intents.
• Incluyen la implementación por defecto del método onBind() que devuelve null por
lo que, a no ser que sea sobrescrito, no permitirá la unión de componentes.
• Incluyen una implementación del método onStartCommand() que añadirá los Intents
a la cola de trabajo e invocará a onHandleIntent().
Por lo tanto, un servicio que extienda esta clase bastará con que implemente el método
onHandleIntent(), además del constructor del servicio. Es posible implementar el resto
de métodos del ciclo de vida de un servicio, pero será necesario invocar a la
implementación de los mismos métodos de IntentService (por medio de super) para
que se gestione correctamente el ciclo de vida y el worker.

2. Service. Clase padre de todos los servicios. Necesita la implementación de al menos un


hilo para la realización de tareas largas y, por otro lado, evita la restricción que impone
IntentService, que encola los Intent que recibe para gestionarlos de uno en uno. Al
extender Service se podrá realizar una gestión (manual) multihilo al recibir Intents sin
necesidad de encolarlos uno detrás de otro (como, por ejemplo, en el caso de implementar
un servicio que gestione varias descargas simultáneas, mostrando el progreso de cada una
de ellas en diferentes barras de progreso).

16
O bien en el método onStart(), en versiones menores o iguales a 1.6; este método está deprecado a partir de
2.0

CURSO DE DESARROLLO DE APLICACIONES ANDROID 17


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Se puede controlar qué acción se realizará en caso de que el sistema elimine el servicio
abruptamente, a través del valor entero que devuelve el método onStartCommand(), gracias a
una de las siguientes constantes:

• START_NOT_STICKY. Si el sistema elimina el servicio después de que finalice el método


onStartCommand(), no lo recreará de nuevo, a no ser que existan Intents pendientes
de envío (encolados). Se trata de la opción más segura para evitar que el servicio esté
ejecutándose más allá de lo necesario o cuando la aplicación es capaz de reiniciar las
tareas que se hayan dejado sin finalizar.
• START_STICKY. Si el sistema elimina el servicio después de que finalice el método
onStartCommand(), sí recreará el servicio e invocará de nuevo a onStartCommand(),
aunque sin pasarle de nuevo el último Intent. Si existiesen Intents pendientes para
iniciar el servicio, sí que serían enviados. Esta opción es útil para servicios que se
ejecutan indefinidamente esperando tareas que gestionar.
• START_REDELIVER_INTENT. En este caso, sí que se enviará el último Intent y a
continuación el resto de Intents encolados. Es útil para servicios que realizan tareas
que no deben ser paradas como, por ejemplo, descargar un archivo.

Inicio y parada de un servicio

Como ya se ha explicado anteriormente, para iniciar un servicio desde una actividad, bastará
invocar al método startService() pasando de parámetro un Intent, y el propio sistema
invocará al método onStartCommand() pasando el Intent como parámetro. El método
startService() devuelve inmediatamente el control al hilo principal de la aplicación, sin
bloquearlo, mientras que el sistema invoca al método onStartCommand(), en caso de que el
servicio ya estuviese iniciado, o bien a onCreate(), si el servicio aún no está iniciado (y, a
continuación, a onStartCommand()).

Si no se habilita el modo “enlace”, implementando el método onBind(), el único medio de


comunicación con el servicio será a través de la información anexada en el Intent que se
pasa al método startService(). La única posibilidad entonces para recibir información de
vuelta en la actividad que inicia el servicio será a través de un PendingIntent generado con
getBroadcast().

Cualquier servicio puede ser invocado múltiples veces de forma concurrente a través de
startService() (y se deberán gestionar los Intents tal y como ya se ha explicado, bien
encolándolos al extender IntentService, o bien de forma manual lanzando múltiples hilos),
pero bastará una única llamada a stopService() o stopSelf() para detener el servicio.

A diferencia de las actividades, el sistema no gestiona automáticamente el ciclo de vida de un


servicio, si no que es él mismo quien debe gestionar su propio ciclo de vida. Únicamente en
casos de necesidad de memoria el sistema detendrá unos u otros servicios que estén activos
en función de ciertos flags de prioridad asociados a los mismos.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 18


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Por lo tanto, un servicio permanecerá indefinidamente activo después de la llamada a


onStartCommand() hasta que se invoque a stopService() o stopSelf(), momento en el
cual el sistema destruirá el servicio tan pronto como sea posible.

No obstante, es necesario tener en cuenta que, en el caso de gestionar múltiples hilos en un


servicio, no se deberá parar el mismo cuando se haya acabado de procesar una petición de
inicio, ya que pueden existir nuevas peticiones de inicio de servicio que ya hayan sido enviadas.
Para evitar el problema de interrumpir estas sucesivas peticiones 17, existe el método
stopSelfResult(int) cuyo parámetro es el identificador de la petición de inicio más
reciente, recibida en el tercer parámetro de onStartCommand(), y que devolverá un booleano
que será true en caso de que el startId coincida con el identificador recibido en la última
petición de inicio de servicio, parando el mismo, y devolverá false en caso de que no lo sea
(por lo que existirán más peticiones de inicio de servicio procesándose todavía), evitando la
parada del servicio.

Servicios en primer plano

Los servicios en primer plano son aquellos que el sistema no deberá detener en casos de
necesidades de memoria, ya que el usuario es consciente de su ejecución. Estos servicios
deben proveer una notificación en la barra de estado que estará ubicada en la zona de
notificaciones “ejecutándose” de modo que no pueda ser borrada hasta que el servicio haya
finalizado su tarea o haya sido eliminado del primer plano.

Uno de los ejemplos más típicos de un servicio en primer plano es la de una aplicación que
reproduzca música a través de un servicio, ya que el usuario estará escuchando lo que se esté
reproduciendo. La notificación en la barra de estado indicará qué canción se está
reproduciendo y no podrá ser borrada a no ser que el usuario pulse sobre la misma, active la
interfaz del reproductor, y detenga la canción. En ese momento, el servicio podrá ser enviado
a background o ser detenido, eliminándose así la notificación.

Para iniciar un servicio en primer plano, será necesario invocar al método startForeground()
pasando como parámetros un entero que identifique a la notificación, así como el objeto
Notification que será mostrado en la barra de estado. Para eliminar el servicio del primer
plano, bastará con invocar a stopForeground(), método que recibirá un booleano que
determinará si se elimina (true) o no (false) la notificación de la barra de estado. En
cualquier caso, al parar el servicio con stopService(), la notificación será eliminada.

17
Es importante adoptar estas buenas prácticas al gestionar el ciclo de vida de los servicios y solo parar los servicios
cuando hayan acabado de realizar su trabajo ya que, en otro caso, se estarán usando indebidamente los recursos
del sistema gastando, además, más batería. Siempre será necesario invocar a stopSelf() o stopSelfResult()
desde el momento en que se haya realizado una llamada a onStartCommand().

CURSO DE DESARROLLO DE APLICACIONES ANDROID 19


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Servicios enlazados

Los servicios enlazados permiten que otros componentes se les unan de forma que puedan
interactuar entre ellos. Representan la parte “servidor” en un interfaz de tipo cliente-servidor.
Los clientes pueden enviarles peticiones, recibir repuestas e incluso realizar comunicación
entre procesos (IPC). En general, únicamente vivirán mientras tengan tareas que realizar
proveyendo información a ciertos componentes, a diferencia de los servicios comunes que, en
general viven indefinidamente.

Como todo servicio, los servicios enlazados extienden la clase Service y es necesario que
provean una implementación del método callback onBind() que devuelve una interfaz de tipo
IBinder la cual define los métodos que pueden usar los clientes para interactuar con el
servicio.

Para unirse a un servicio, el cliente deberá invocar al método del contexto de la aplicación,
bindService(), pasando como parámetro un Intent al servicio invocado, así como un
objeto ServiceConnection que se encargará de monitorizar la conexión con el servicio, y un
flag. El método, asíncrono, devolverá true si la unión al servicio ha sido satisfactoria y false si
la conexión no se puede realizar.

Una vez se haya creado la conexión al servicio, el sistema invocará a onServiceConnected()


en el objeto ServiceConnection y le entregará la interfaz IBinder que el cliente deberá usar
para comunicarse con el servicio. Los sucesivos clientes que se conecten recibirán la misma
interfaz IBinder ya creada, de forma que no se vuelve a invocar a onBind().

Tal y como ya se mencionó en una sección anterior, cuando el último cliente se desconecte del
servicio, el sistema lo destruirá, a no ser que dicho servicio hubiese sido iniciado previamente
con el método startService().

Creación de un servicio enlazado

Cuando se crea un servicio enlazado, lo más importante es definir la interfaz de tipo IBinder
con la cual interactuarán los clientes. Para ello, se pueden utilizar dos técnicas distintas 18:
extender la clase Binder o bien utilizar un objeto de tipo Messenger.

Extensión de Binder

Se creará una interfaz tipo IBinder que extienda de Binder y que sea devuelta en el método
onBind(), en aquellos casos en los que el servicio sea privado (usado únicamente dentro de la

18
Existe una tercera técnica que no será estudiada en el curso, basada en la implementación de objetos que se
reducen a primitivas utilizando el Lenguaje de Definición de Interfaces de Android (AIDL)

CURSO DE DESARROLLO DE APLICACIONES ANDROID 20


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

aplicación) y se ejecute en el mismo proceso que el cliente y actúe como una tarea en segundo
plano (worker), lo cual es bastante común. Esta técnica no deberá ser usada si el servicio ha de
ejecutarse en otro hilo o si puede ser usado por otras aplicaciones.

Para configurar un servicio de este modo, se deberá crear una instancia de Binder que, o bien
contenga los métodos públicos que el cliente puede invocar, o bien devuelva una instancia del
servicio o de otra clase que albergue el servicio, el cual contendrá los métodos públicos que el
cliente puede invocar. A continuación, dicha instancia del Binder será devuelta al cliente a
través del método callback onBind(). La interfaz será recibida en el cliente a través del
método callback onServiceConnected().

A continuación se muestra un ejemplo de un servicio que provee ciertos métodos a través de


su interfaz IBinder.

public class AdivinaNumeroService extends Service {

// Este es el Binder que recibirán los clientes


private final IBinder mMiBinder = new AdivinaNumeroBinder();
private final Random mNumeroAleatorio = new Random();
private int mNumeroIntentos = 0;

public class AdivinaNumeroBinder extends Binder {

// En este caso, solo se expondrán estos métodos a los


// clientes.
public int getNumero() {
return getNumeroAleatorio();
}

public int getIntentos() {


return getNumeroPeticiones();
}
}

@Override
public IBinder onBind(Intent arg0) {
return mMiBinder;
}

public int getNumeroAleatorio() {


mNumeroIntentos++;
return mNumeroAleatorio.nextInt(10);
}

public int getNumeroPeticiones() {


return mNumeroIntentos;
}
}

El servicio anterior, dedicado a generar números aleatorios del 0 al 9 y a contar el número de


veces que se solicita un número aleatorio, implementa una clase interna tipo Binder,

CURSO DE DESARROLLO DE APLICACIONES ANDROID 21


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

AdivinaNumeroBinder, que implementa los métodos a través de los cuales los clientes podrán
extraer información del servicio. En el método onBind() se devolverá la interfaz de tipo
IBinder de AdivinaNumeroBinder.

Para unirse a dicho servicio, una actividad cliente deberá crear un Intent al servicio e invocar
a bindService(), pasando como parámetro dicho Intent, un flag, y un objeto tipo
ServiceConnection. En este objeto se deberán implementar los métodos
onServiceConnected() y onServiceDisconnected(), para monitorizar la conexión con el
servicio. Además, el primero de los métodos recibe la interfaz IBinder a través de la cual el
cliente se comunicará con el servicio.

public class ServicioEnlazadoActivity extends Activity {

AdivinaNumeroBinder mBinder;
boolean mServicioUnido = false;

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

@Override
protected void onStart() {
super.onStart();
// Se invoca a AdivinaNumeroService vía Intent para unirse a
// él.
Intent intent = new Intent(this, AdivinaNumeroService.class);
bindService(intent, mConexion, Context.BIND_AUTO_CREATE);
}

@Override
protected void onStop() {
super.onStop();
// Desconexión del servicio
if (mServicioUnido) {
unbindService(mConexion);
mServicioUnido = false;
}
}

public void comprobarNumero() {…}

private ServiceConnection mConexion = new ServiceConnection() {

@Override
public void onServiceConnected(ComponentName className,
IBinder service) {
// Se obtiene el interfaz del servicio, una vez se ha
// unido a él.
mBinder = (AdivinaNumeroBinder) service;
mServicioUnido = true;
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 22


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

@Override
public void onServiceDisconnected(ComponentName arg0) {
mServicioUnido = false;
}
};
}

En el ejemplo anterior, se puede ver cómo se implementa el objeto ServiceConnection y


cómo se obtiene la interfaz IBinder del Binder, AdivinaNumeroBinder. Además, se utiliza
una variable miembro booleana para verificar si el servicio está unido o no, la cual deberá
utilizarse para controlar la desconexión del servicio, así como el acceso a los métodos del
mBinder, antes de que el servicio esté disponible.

Para utilizar los métodos que hace disponibles la interfaz IBinder de este servicio, se puede
implementar un método que obtenga números aleatorios generados en el servicio:

public void comprobarNumero(View view) {

if (mServicioUnido) {

int numeroAAdivinar = mBinder.getNumero();


((EditText) findViewById(R.id.edit_numero_pensado)).setText(""+
numeroAAdivinar);
}
}

Uso de Messenger

Cuando el servicio se ejecuta en otro proceso distinto al proceso del cliente que lo invoca, y se
quiere comunicar con él, se podrá utilizar un Messenger que provea la interfaz con el servicio y
que permite comunicación entre procesos (IPC). Para utilizar esta técnica, que permite que el
cliente pueda enviar mensajes al servicio, se deberán realizar los siguientes pasos:

1. El servicio implementará un Handler que recibirá las peticiones del cliente a través de
sus métodos callback.
2. El Handler se pasará como referencia al constructor que creará un objeto de tipo
Messenger (en el servicio).
3. El Messenger creará la interfaz tipo IBinder que devolverá el método onBind().
4. Los clientes utilizarán la interfaz IBinder para instanciar el Messenger 19 (que a su vez
contiene una referencia al Handler), que será utilizado por el cliente para enviar
objetos de tipo Message al servicio.
5. El servicio recibirá los objetos Message en el método handleMessage() del Handler.

19
Existe por lo tanto dos objetos de tipo Messenger (dos mensajeros): uno en el servicio y otro en el cliente.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 23


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

De esta forma, el cliente no accede a métodos de la interfaz IBinder si no que envía mensajes
que serán gestionados por el servicio en el objeto Handler.

En caso de querer recibir respuestas en el cliente desde el servicio, será necesario crear un
Messenger en el cliente. Cuando el cliente reciba la llamada a su servicio callback
onServiceConnected(), deberá enviar un mensaje al servicio que incluya el objeto
Messenger del cliente como parámetro replyTo del método send(). De esta forma, el
servicio puede almacenar una referencia del Messenger del cliente, que utilizará para enviarle
mensajes de respuesta, ante sucesivas peticiones del cliente.

A continuación se muestra un ejemplo de un cliente y un servicio que implementan la misma


funcionalidad que el ejemplo anterior, pero mediante el uso de Messenger.

El cliente ha de crear un Messenger basado en el IBinder que devuelve el servicio en el


método onServiceConnected() y lo utilizará para enviar mensajes a través del método
send().

Tanto el cliente como el servicio gestionarán los mensajes recibidos vía Messenger en el
método handleMessage() de la clase interna tipo Handler.

Código fuente de ServicioEnlazadoMessengerActivity.java

public class ServicioEnlazadoMessengerActivity extends Activity {

// Mensajero con el que se comunicará con el servicio


Messenger mServicio = null;
boolean mServicioUnido = false;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btnAdivinar = (Button) findViewById(R.id.btnAdivinar);
btnAdivinar.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
adivinar(v);
}
});
// Se conecta automáticamente el servicio según se inicia la
// actividad
conectarServicio(null);
}

// Clase interna con la que interactuará con la interfaz IBinder


// del servicio
private ServiceConnection mConexion = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className,
IBinder service) {

CURSO DE DESARROLLO DE APLICACIONES ANDROID 24


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

// Se utiliza un Messenger con referencia al IBinder


// recibido
mServicio = new Messenger(service);
mServicioUnido = true;

// Se envía la referencia a mMessenger del cliente al


// servicio para poder recibir respuestas desde el
// servicio, a través de dicho Messenger.
try {
Message msg = Message.obtain(null,
AdivinaNumeroServiceMessenger.MSG_CONECTAR_CLIENTE);
msg.replyTo = mMessenger;
mServicio.send(msg);
} catch (RemoteException e) {
// El servicio ha fallado antes de que se pueda hacer
// nada.
}
}

@Override
public void onServiceDisconnected(ComponentName arg0) {
mServicio = null;
mServicioUnido = false;
}
};

public void conectarServicio(View v) {


// Se invoca a AdivinaNumeroServiceMessenger a través de
// Intent para unirse a él.
Intent intent = new Intent(this,
AdivinaNumeroServiceMessenger.class);
bindService(intent, mConexion, Context.BIND_AUTO_CREATE);
mServicioUnido = true;
}

public void desconectarServicio(View v) {


// Desconexión del servicio
if (mServicioUnido) {
unbindService(mConexion);
mServicioUnido = false;
((TextView) findViewById(R.id.txtResultado)).setText(
R.string.servicio_desconectado);
}
}
// Implementación de la respuesta del servicio a través de otro
// Messenger en el cliente, en una clase interna
/**
* Handler (“manejador”) de los mensajes provenientes del servicio.
*/

class HandlerCliente extends Handler {


@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case AdivinaNumeroServiceMessenger.
MSG_RECIBIR_NUMERO_INTENTO:
comprobarNumero(msg.arg1, msg.arg2);
break;

CURSO DE DESARROLLO DE APLICACIONES ANDROID 25


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

}
}
}

/**
* Mensajero que se publica para el servicio, para enviar mensajes
* al Handler del Cliente.
*/
final Messenger mMessenger = new Messenger(new HandlerCliente());

public void adivinar(View v) {


if (mServicio != null && mServicioUnido) {
try {
Message msg = Message.obtain(null,
AdivinaNumeroServiceMessenger.
MSG_GENERAR_NUMERO_ALEATORIO);
msg.replyTo = mMessenger;
mServicio.send(msg);
} catch (RemoteException e) {
}
} else {
// Servicio no conectado
}
}

// Método invocado por el botón btnAdivinar a través de su


// propiedad android:onClick
public void comprobarNumero(int numeroAAdivinar, int numeroIntentos) {
if (mServicioUnido) {
[…]
}
}
}

Código fuente de AdivinaNumeroServiceMessenger.java

public class AdivinaNumeroServiceMessenger extends Service {

// Comandos para que el servicio realice las acciones correspondientes


public static final int MSG_CONECTAR_CLIENTE = 1;
public static final int MSG_GENERAR_NUMERO_ALEATORIO = 2;
// Messenger del Cliente recibido en el mensaje MSG_CONECTAR_CLIENTE
private Messenger mMessengerCliente;
// Comandos para recepción en Cliente
public static final int MSG_RECIBIR_NUMERO_INTENTO = 5;
public static final int MSG_RECIBIR_INTENTOS = 6;
private final Random mNumeroAleatorio = new Random();
private int mNumeroIntentos = 0;

/**
* Handler que gestionará los mensajes provenientes de los
* clientes.
*/

CURSO DE DESARROLLO DE APLICACIONES ANDROID 26


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

class MiHandler extends Handler {


@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CONECTAR_CLIENTE:
mMessengerCliente = msg.replyTo;
resetNumeroIntentos();
break;
case MSG_GENERAR_NUMERO_ALEATORIO:
enviarNumeroEIntentosACliente();
break;
default:
super.handleMessage(msg);
}
}
}
/**
* Messenger del servico, que se publica para los clientes para
* enviar mensajes a MiHandler.
*/
final Messenger mMessenger = new Messenger(new MiHandler());
/**
* Cuando el cliente se une al servicio, se devuelve la interfaz al
* Messenger para enviar mensajes al servicio.
*/
@Override
public IBinder onBind(Intent arg0) {
resetNumeroIntentos();
return mMessenger.getBinder();
}

private int getNumeroAleatorio() {


mNumeroIntentos++;
return mNumeroAleatorio.nextInt(10);
}

private int getNumeroPeticiones() {


return mNumeroIntentos;
}

private void resetNumeroIntentos() {


mNumeroIntentos = 0;
}

private void enviarNumeroEIntentosACliente() {


try {
mMessengerCliente.send(Message.obtain(null,
AdivinaNumeroServiceMessenger.MSG_RECIBIR_NUMERO_INTENTO,
getNumeroAleatorio(), getNumeroPeticiones()));
} catch (RemoteException e) {
// Si se alcanza este catch, el cliente se habrá desconectado.
}
}

CURSO DE DESARROLLO DE APLICACIONES ANDROID 27


TEMA 10. PROVEEDORES DE CONTENIDO Y SERVICIOS

Comentarios finales

En caso de que se rompa la conexión con el servicio, la única excepción que enviarán los
métodos remotos (en caso de usar un Binder) será DeadObjectException, la cual deberá ser
gestionada.

Cualquier actividad, servicio o proveedor de contenido podrá unirse a un servicio. En cambio,


un receptor de broadcast no podrá unirse nunca a un servicio.

Se deberá optimizar la unión y desconexión del servicio, en función de su utilidad. Si la


actividad sólo necesita mantener el servicio activo mientras está visible, se unirá a él en el
método onStart() y se desconectará de él en el método onStop(). En cambio, si se desea
que el servicio envíe respuestas a la actividad mientras esta está parada en segundo plano, se
deberá unir el servicio en el método onCreate() y se deberá desconectar en el método
onDestroy().

Se ha de tener en cuenta que si se mantiene el servicio activo en otro proceso, mientras la


actividad se encuentra en segundo plano, la aplicación en conjunto consumirá más recursos,
por lo que el sistema podrá llegar a parar la actividad y el servicio en caso de necesidad de
recursos.

No se deben utilizar los métodos onResume() y onPause() para la unión y desconexión del
servicio, puesto que dichos métodos han de permanecer lo más ligeros posible.

CURSO DE DESARROLLO DE APLICACIONES ANDROID 28

También podría gustarte