Inicio Android Content Providers en Android (I): Construcción

Content Providers en Android (I): Construcción

por sgoliver

En este nuevo artículo del Curso de Programación en Android que estamos publicando vamos a tratar el temido [o a veces incomprendido] tema de los Content Providers.

Un Content Provider no es más que el mecanismo proporcionado por la plataforma Android para permitir compartir información entre aplicaciones. Una aplicación que desee que todo o parte de la información que almacena esté disponible de una forma controlada para el resto de aplicaciones del sistema deberá proporcionar un content provider a través del cuál se pueda realizar el acceso a dicha información. Este mecanismo es utilizado por muchas de las aplicaciones estandard de un dispositivo Android, como por ejemplo la lista de contactos, la aplicación de SMS, o el calendario/agenda. Esto quiere decir que podríamos acceder a los datos gestionados por estas aplicaciones desde nuestras propias aplicaciones Android haciendo uso de los content providers correspondientes.

Son por tanto dos temas los que debemos tratar en este apartado, por un lado a construir nuevos content providers personalizados para nuestras aplicaciones, y por otro utilizar un content provider ya existente para acceder a los datos publicados por otras aplicaciones.

En gran parte de la bibliografía sobre programación en Android se suele tratar primero el tema del acceso a content providers ya existentes (como por ejemplo el acceso a la lista de contactos de Android) para después pasar a la construcción de nuevos content providers personalizados. Yo sin embargo voy a tratar de hacerlo en orden inverso, ya que me parece importante conocer un poco el funcionamiento interno de un content provider antes de pasar a utilizarlo sin más dentro de nuestras aplicaciones. Así, en este primer artículo sobre el tema veremos cómo crear nuestro propio content provider para compartir datos con otras aplicaciones, y en el próximo artículo veremos como utilizar este mecanismo para acceder directamente a datos de terceros.

Empecemos a entrar en materia. Para añadir un content provider a nuestra aplicación tendremos que:

  1. Crear una nueva clase que extienda a la clase android ContentProvider.
  2. Declarar el nuevo content provider en nuestro fichero AndroidManifest.xml

Por supuesto nuestra aplicación tendrá que contar previamente con algún método de almacenamiento interno para la información que queremos compartir. Lo más común será disponer de una base de datos SQLite, por lo que será esto lo que utilizaré para todos los ejemplos de este artículo, pero internamente podríamos tener los datos almacenados de cualquier otra forma, por ejemplo en ficheros de texto, ficheros XML, etc. El content provider sera el mecanismo que nos permita publicar esos datos a terceros de una forma homogenea y a través de una interfaz estandarizada.

Un primer detalle a tener en cuenta es que los registros de datos proporcionados por un content provider deben contar siempre con un campo llamado _ID que los identifique de forma unívoca del resto de registros. Como ejemplo, los registros devueltos por un content provider de clientes podría tener este aspecto:

_ID Cliente Telefono Email
3 Antonio 900123456 email1@correo.com
7 Jose 900123123 email2@correo.com
9 Luis 900123987 email3@correo.com

Sabiendo esto, es interesante que nuestros datos también cuenten internamente con este campo _ID (no tiene por qué llamarse igual) de forma que nos sea más sencillo después generar los resultados del content provider.

Con todo esto, y para tener algo desde lo que partir, vamos a construir en primer lugar una aplicación de ejemplo muy sencilla con una base de datos SQLite que almacene los datos de una serie de clientes con una estructura similar a la tabla anterior. Para ello seguiremos los mismos pasos que ya comentamos en los artículos dedicados al tratamiento de bases de datos SQLite en Android (consultar índice del curso).

Por volver a recordarlo muy brevemente, lo que haremos será crear una nueva clase que extienda a SQLiteOpenHelper, definiremos las sentencias SQL para crear nuestra tabla de clientes, e implementaremos finalmente los métodos onCreate() y onUpgrade(). El código de esta nueva clase, que yo he llamado ClientesSqliteHelper, quedaría como sigue:

public class ClientesSqliteHelper extends SQLiteOpenHelper {

    //Sentencia SQL para crear la tabla de Clientes
    String sqlCreate = "CREATE TABLE Clientes " +
                       "(_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
    		           " nombre TEXT, " +
    		           " telefono TEXT, " +
    		           " email TEXT )";

    public ClientesSqliteHelper(Context contexto, String nombre,
                               CursorFactory factory, int version) {

        super(contexto, nombre, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        //Se ejecuta la sentencia SQL de creación de la tabla
        db.execSQL(sqlCreate);

        //Insertamos 15 clientes de ejemplo
        for(int i=1; i<=15; i++)
        {
            //Generamos los datos de muestra
            String nombre = "Cliente" + i;
            String telefono = "900-123-00" + i;
            String email = "email" + i + "@mail.com";

            //Insertamos los datos en la tabla Clientes
            db.execSQL("INSERT INTO Clientes (nombre, telefono, email) " +
                       "VALUES ('" + nombre + "', '" + telefono +"', '" + email + "')");
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int versionAnterior, int versionNueva) {
        //NOTA: Por simplicidad del ejemplo aquí utilizamos directamente la opción de
        //      eliminar la tabla anterior y crearla de nuevo vacía con el nuevo formato.
        //      Sin embargo lo normal será que haya que migrar datos de la tabla antigua
        //      a la nueva, por lo que este método debería ser más elaborado.

        //Se elimina la versión anterior de la tabla
        db.execSQL("DROP TABLE IF EXISTS Clientes");

        //Se crea la nueva versión de la tabla
        db.execSQL(sqlCreate);
    }
}

Como notas relevantes del código anterior:

  • Nótese el campo «_id» que hemos incluido en la base de datos de clientes por lo motivos indicados un poco más arriba. Este campo lo declaramos como INTEGER PRIMARY KEY AUTOINCREMENT, de forma que se incremente automáticamente cada vez que insertamos un nuevo registro en la base de datos.
  • En el método onCreate(), además de ejecutar la sentencia SQL  para crear la tabla Clientes, también inserta varios registros de ejemplo.
  • Para simplificar el ejemplo, el método onUpgrade() se limita a eliminar la tabla actual y crear una nueva con la nueva estructura. En una aplicación real habría que hacer probáblemente la migración de los datos a la nueva base de datos.

Dado que la clase anterior ya se ocupa de todo, incluso de insertar algunos registro de ejemplo con los que podamos hacer pruebas, la aplicación principal de ejemplo no mostrará en principio nada en pantalla ni hará nada con la información. Esto lo he decidido así para no complicar el código de la aplicación innecesariamente, ya que no nos va a interesar el tratamiento directo de los datos por parte de la aplicación principal, sino su utilización a través del content provider que vamos a construir.

Una vez que ya contamos con nuestra aplicación de ejemplo y su base de datos, es hora de empezar a construir el nuevo content provider que nos permitirá compartir sus datos con otras aplicaciones.

Lo primero que vamos a comentar es la forma con que se hace referencia en Android a los content providers. El acceso a un content provider se realiza siempre mediante una URI. Una URI no es más que una cadena de texto similar a cualquiera de las direcciones web que utilizamos en nuestro navegador. Al igual que para acceder a mi blog lo hacemos mediante la dirección «http://www.sgoliver.net«, para acceder a un content provider utilizaremos una dirección similar a «content://net.sgoliver.android.contentproviders/clientes«.

Las direcciones URI de los content providers están formadas por 3 partes. En primer lugar el prefijo «content://» que indica que dicho recurso deberá ser tratado por un content provider. En segundo lugar se indica el identificador en sí del content provider, también llamado authority. Dado que este dato debe ser único es una buena práctica utilizar un authority de tipo «nombre de clase java invertido», por ejemplo en mi caso «net.sgoliver.android.contentproviders«. Por último, se indica la entidad concreta a la que queremos acceder dentro de los datos que proporciona el content provider. En nuestro caso será simplemente la tabla de «clientes«, ya que será la única existente, pero dado que un content provider puede contener los datos de varias entidades distintas en este último tramo de la URI habrá que especificarlo. Indicar por último que en una URI se puede hacer referencia directamente a un registro concreto de la entidad seleccionada. Esto se haría indicando al final de la URI el ID de dicho registro. Por ejemplo la uri «content://net.sgoliver.android.contentproviders/clientes/23» haría referencia directa al cliente con _ID = 23.

Todo esto es importante ya que será nuestro content provider el encargado de interpretar/parsear la URI completa para determinar los datos que se le están solicitando. Esto lo veremos un poco más adelante.

Sigamos. El siguiente paso será extender a la clase ContentProvider. Si echamos un vistazo a los métodos abstractos que tendremos que implementar veremos que tenemos los siguientes:

  • onCreate()
  • query()
  • insert()
  • update()
  • delete()
  • getType()

El primero de ellos nos servirá para inicializar todos los recursos necesarios para el funcionamiento del nuevo content provider. Los cuatro siguientes serán los métodos que permitirán acceder a los datos (consulta, inserción, modificación y eliminación, respectivamente) y por último, el método getType() permitirá conocer el tipo de datos devueltos por el content provider (más tade intentaremos explicar algo mejor esto último).

Además de implementar estos métodos, también definiremos una serie de constantes dentro de nuestra nueva clase provider, que ayudarán posteriormente a su utilización. Veamos esto paso a paso. Vamos a crear una nueva clase ClientesProvider que extienda de ContentProvider.

Lo primero que vamos a definir es la URI con la que se accederá a nuestro content provider. En mi caso he elegido la siguiente: «content://net.sgoliver.android.contentproviders/clientes». Además, para seguir la práctica habitual de todos los content providers de Android,  encapsularemos además esta dirección en un objeto estático de tipo Uri llamado CONTENT_URI.

//Definición del CONTENT_URI
private static final String uri =
	"content://net.sgoliver.android.contentproviders/clientes";

public static final Uri CONTENT_URI = Uri.parse(uri);

A continuación vamos a definir varias constantes con los nombres de las columnas de los datos proporcionados por nuestro content provider. Como ya dijimos antes existen columnas predefinidas que deben tener todos los content providers, por ejemplo la columna _ID. Estas columnas estandar están definidas en la clase BaseColumns, por lo que para añadir la nuevas columnas de nuestro content provider definiremos una clase interna pública tomando como base la clase BaseColumns y añadiremos nuestras nuevas columnas.

//Clase interna para declarar las constantes de columna
public static final class Clientes implements BaseColumns
{
	private Clientes() {}

	//Nombres de columnas
	public static final String COL_NOMBRE = "nombre";
	public static final String COL_TELEFONO = "telefono";
	public static final String COL_EMAIL = "email";
}

Por último, vamos a definir varios atributos privados auxiliares para almacenar el nombre de la base de datos, la versión, y la tabla a la que accederá nuestro content provider.

//Base de datos
private ClientesSqliteHelper clidbh;
private static final String BD_NOMBRE = "DBClientes";
private static final int BD_VERSION = 1;
private static final String TABLA_CLIENTES = "Clientes";

Como se indicó anteriormente, la primera tarea que nuestro content provider deberá hacer cuando se acceda a él será interpretar la URI utilizada. Para facilitar esta tarea Android proporciona una clase llamada UriMatcher, capaz de interpretar determinados patrones en una URI. Esto nos será útil para determinar por ejemplo si una URI hace referencia a una tabla genérica o a un registro concreto a través de su ID. Por ejemplo:

  • «content://net.sgoliver.android.contentproviders/clientes» –> Acceso genérico a tabla de clientes
  • «content://net.sgoliver.android.contentproviders/clientes/17» –> Acceso directo al cliente con ID = 17

Para conseguir esto definiremos también como miembro de la clase un objeto UriMatcher y dos nuevas constantes que representen los dos tipos de URI que hemos indicado: acceso genérico a tabla (lo llamaré CLIENTES) o acceso a cliente por ID (lo llamaré CLIENTES_ID). A continuación inicializaremos el objeto UriMatcher indicándole el formato de ambos tipos de URI, de forma que pueda diferenciarlos y devolvernos su tipo (una de las dos constantes definidas, CLIENTES o CLIENTES_ID).

//UriMatcher
private static final int CLIENTES = 1;
private static final int CLIENTES_ID = 2;
private static final UriMatcher uriMatcher;

//Inicializamos el UriMatcher
static {
	uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
	uriMatcher.addURI("net.sgoliver.android.contentproviders", "clientes", CLIENTES);
	uriMatcher.addURI("net.sgoliver.android.contentproviders", "clientes/#", CLIENTES_ID);
}

En el código anterior vemos como mediante el método addUri() indicamos el authority de nuestra URI, el formato de la entidad que estamos solicitando, y el tipo con el que queremos identificar dicho formato. Más tarde veremos cómo utilizar esto de forma práctica.

Bien, pues ya tenemos definidos todos los miembros necesarios para nuestro nuevo content provider. Ahora toca implementar los métodos comentados anteriormente.

El primero de ellos es onCreate(). En este método nos limitaremos simplemente a inicializar nuestra base de datos, a través de su nombre y versión, y utilizando para ello la clase ClientesSqliteHelper que creamos al principio del artículo.

@Override
public boolean onCreate() {

	clidbh = new ClientesSqliteHelper(
			getContext(), BD_NOMBRE, null, BD_VERSION);

	return true;
}

La parte interesante llega con el método query(). Este método recibe como parámetros una URI, una lista de nombres de columna, un criterio de selección, una lista de valores para las variables utilizadas en el criterio anterior, y un criterio de ordenación. Todos estos datos son análogos a los que comentamos cuando tratamos la consulta de datos en SQLite para Android, artículo que recomiendo releer si no tenéis muy frescos estos conocimientos. El método query deberá devolver los datos solicitados según la URI indicada y los criterios de selección y ordenación pasados como parámetro. Así, si la URI hace referencia a un cliente concreto por su ID ése deberá ser el único registro devuelto. Si por el contrario es un acceso genérico a la tabla de clientes habrá que realizar la consulta SQL correspondiente a la base de datos respetanto los criterios pasados como parámetro.

Para disitinguir entre los dos tipos de URI posibles utilizaremos como ya hemos indicado el objeto uriMatcher, utilizando su método match(). Si el tipo devuelto es CLIENTES_ID, es decir, que se trata de un acceso a un cliente concreto, sustituiremos el criterio de selección por uno que acceda a la tabla de clientes sólo por el ID indicado en la URI. Para obtener este ID utilizaremos el método getLastPathSegment() del objeto uri que extrae el último elemento de la URI, en este caso el ID del cliente.

Hecho esto, ya tan sólo queda realizar la consulta a la base de datos mediante el método query() de SQLiteDatabase. Esto es sencillo ya que los parámetros son análogos a los recibidos en el método query() del content provider.

@Override
public Cursor query(Uri uri, String[] projection,
	String selection, String[] selectionArgs, String sortOrder) {

	//Si es una consulta a un ID concreto construimos el WHERE
	String where = selection;
	if(uriMatcher.match(uri) == CLIENTES_ID){
                where = "_id=" + uri.getLastPathSegment();
        }

	SQLiteDatabase db = clidbh.getWritableDatabase();

	Cursor c = db.query(TABLA_CLIENTES, projection, where,
		            selectionArgs, null, null, sortOrder);

	return c;
}

Como vemos, los resultados se devuelven en forma de Cursor, una vez más exactamente igual a como lo hace el método query() de SQLiteDatabase.

Por su parte, los métodos update() y delete() son completamente análogos a éste, con la única diferencia de que devuelven el número de registros afectados en vez de un cursor a los resultados. Vemos directamente el código:

@Override
public int update(Uri uri, ContentValues values,
	          String selection, String[] selectionArgs) {

	int cont;

	//Si es una consulta a un ID concreto construimos el WHERE
	String where = selection;
	if(uriMatcher.match(uri) == CLIENTES_ID){
            where = "_id=" + uri.getLastPathSegment();
        }

	SQLiteDatabase db = clidbh.getWritableDatabase();

	cont = db.update(TABLA_CLIENTES, values, where, selectionArgs);

	return cont;
}

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {

	int cont;

	//Si es una consulta a un ID concreto construimos el WHERE
	String where = selection;
	if(uriMatcher.match(uri) == CLIENTES_ID){
            where = "_id=" + uri.getLastPathSegment();
        }

	SQLiteDatabase db = clidbh.getWritableDatabase();

	cont = db.delete(TABLA_CLIENTES, where, selectionArgs);

	return cont;
}

El método insert() sí es algo diferente, aunque igual de sencillo. La diferencia en este caso radica en que debe devolver la URI que hace referencia al nuevo registro insertado. Para ello, obtendremos el nuevo ID del elemento insertado como resultado del método insert() de SQLiteDatabase, y posteriormente construiremos la nueva URI mediante el método auxiliar ContentUris.withAppendedId() que recibe como parámetro la URI de nuestro content provider y el ID del nuevo elemento.

@Override
public Uri insert(Uri uri, ContentValues values) {

	long regId = 1;

	SQLiteDatabase db = clidbh.getWritableDatabase();

	regId = db.insert(TABLA_CLIENTES, null, values);

	Uri newUri = ContentUris.withAppendedId(CONTENT_URI, regId);

	return newUri;
}

Por último, tan sólo nos queda implementar el método getType(). Este método se utiliza para identificar el tipo de datos que devuelve el content provider. Este tipo de datos se expresará como un MIME Type, al igual que hacen los navegadores web para determinar el tipo de datos que están recibiendo tras una petición a un servidor. Identificar el tipo de datos que devuelve un content provider ayudará por ejemplo a Android a determinar qué aplicaciones son capaces de procesar dichos datos.

Una vez más existirán dos tipos MIME distintos para cada entidad del content provider, uno de ellos destinado a cuando se devuelve una lista de registros como resultado, y otro para cuando se devuelve un registro único concreto. De esta forma, seguiremos los siguientes patrones para definir uno y otro tipo de datos:

  • «vnd.android.cursor.item/vnd.xxxxxx» –> Registro único
  • «vnd.android.cursor.dir/vnd.xxxxxx» –> Lista de registros

En mi caso de ejemplo, he definido los siguientes tipos:

  • «vnd.android.cursor.item/vnd.sgoliver.cliente»
  • «vnd.android.cursor.dir/vnd.sgoliver.cliente»

Con esto en cuenta, la implementación del método getType() quedaría como sigue:

@Override
public String getType(Uri uri) {

	int match = uriMatcher.match(uri);

	switch (match)
	{
		case CLIENTES:
			return "vnd.android.cursor.dir/vnd.sgoliver.cliente";
		case CLIENTES_ID:
			return "vnd.android.cursor.item/vnd.sgoliver.cliente";
		default:
			return null;
	}
}

Como se puede observar, utilizamos una vez más el objeto UriMatcher para determinar el tipo de URI que se está solicitando y en función de ésta devolvemos un tipo MIME u otro.

Pues bien, con esto ya hemos completado la implementación del nuevo content provider. Pero aún nos queda un paso más, como indicamos al principio del artículo. Debemos declarar el content provider en nuestro fichero AndroidManifest.xml de forma que una vez instalada la aplicación en el dispositivo Android conozca la existencia de dicho recurso.

Para ello, bastará insertar un nuevo elemento <provider> dentro de <application> indicando el nombre del content provider y su authority.

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

        ...

        <provider android:name="ClientesProvider"
		android:authorities="net.sgoliver.android.contentproviders"/>

</application>

Ahora sí hemos completado totalmente la construcción de nuestro nuevo content provider mediante el cual otras aplicaciones del sistema podrán acceder a los datos almacenados por nuestra aplicación.

En el siguiente artículo veremos cómo utilizar este nuevo content provider para acceder a los datos de nuestra aplicación de ejemplo, y también veremos cómo podemos utilizar alguno de los content provider predefinidos por Android para consultar datos del sistema, como por ejemplo la lista de contactos o la lista de llamadas realizadas.

Puedes consultar y/o descargar el código completo de los ejemplos desarrollados en este artículo accediendo a la página del curso en GitHub.

Curso de Programación Android en PDF

Este curso también está disponible en PDF. Descubre cómo conseguirlo…

¿Te ha sido de utilidad el Curso de Programación Android? ¿Quieres colaborar de forma económica con el proyecto? Puedes contribuir con cualquier cantidad, unos céntimos, unos euros, cualquier aportación será bienvenida. Además, si tu aportación es superior a una pequeña cantidad simbólica recibirás como agradecimiento un documento con la última versión del curso disponible en formato PDF. Sea como sea, muchas gracias por colaborar!

Más información:

También te puede interesar

13 comentarios

Desarrollo en Android | sgoliver.net blog 28/08/2011 - 11:59

[…] Content Providers en Android (I): Construcción [Nuevo!] […]

Responder
Content Providers en Android (II): Utilización | sgoliver.net blog 31/08/2011 - 13:39

[…] Providers en Android (II): Utilización sgoliver Móviles, Programación 2011-08-31 En el artículo anterior del Curso de Programación en Android vimos como construir un content provider personalizado para […]

Responder
Juan Ardissone (micayael) 08/05/2012 - 22:02

Excelente artículo. Estuve leyendo también tus artículos sobre acceso a los servicios REST y me surge la siguiente idea:

Se puede accecer a un servicio REST a través de un ContentProvider? Según veo el método query() retorna un android.database.Cursor y es ahí donde no sabría si se puede hacer la conversión por ejemplo desde un JSON.

Que opinas?

Responder
Dasanro 19/08/2012 - 0:20

Estaba leyendo, y en el caso del Cursor query… pone linea 11 que SQLiteDatabase db = clidbh.getWritableDatabase();

Es decir, si estas abriendo la base de datos para leerla, no deberia de ser getReadableDatabase(); ??

Gracias

Saludos

Responder
Hitsbam 19/06/2013 - 21:34

Recomiendo que si no se quiere que los datos sean accedidos desde fuera de la aplicación declarar el atributo android:exported = «false» en el manifest.

Un saludo

Responder
ditox26 27/08/2013 - 14:13

Hola, excelente información!
Mi duda es la siguiente, me gustaria enviar algunos datos ingresados en la tabla(por ej. Antonio tel 0993425, datos que el usuario registra), mediante SMS a otra persona.
Pensaba que tal vez se pueda, utilizando un content provider para unir dos aplicaciones, una de enviar sms y la otra de registro de datos.

Es posible?

muchas gracias

Responder
Emanuell Paredes 04/09/2013 - 7:20

Excelente articulo, gracias

Responder
Luis 23/02/2014 - 15:22

Hola he seguido el manual, y he adaptado el código de GitHub a mis necesidades, pero al iniciar la aplicación en el logcat me salta el siguiente error: E/AndroidRuntime(863): at android.app.ActivityThread.installProvider(ActivityThread.java:4186)

Podrias ayudarme a resolverlo

Muchas gracias y como siempre excelentes tus manuales

Responder
migarmi 31/05/2014 - 11:40

no se si respondes por correo o no lees esto, pero si, realmente el articulo esta muy bien, y tal vez mi duda sea simplona, pero … para que creas la clase Clientes que implementa basecolumns … si no la usas??

Responder
maria 02/09/2015 - 19:08

hola tengo un lg y a cada momento me sale eso de provider no hay manera de eliminarlo ???

Responder
Joro 31/01/2016 - 17:30

Un inciso, si vuestra idea es hacer uso del provider desde otro proyecto es necesario que en el provider del manifest añadáis: android:exported=»true». Ya que desde la versión 4 punto algo pone el valor por defecto a false y si intentais hacer uso de ella desde otro proyecto os va a dar un error de seguridad. Un saludo, muy buen post (Tan bueno que mi profesor de Android hizo un copia y pega de él en sus apuntes sin si quiera ejecutarlo).

Responder
anonimo 09/12/2016 - 12:47

Donde pones «vnd.android.cursor.dir/vnd.xxxxxx»
¿Que son exactamente las xxxxx?

Responder

Responder a Desarrollo en Android | sgoliver.net blog

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies