Skip to content
Andrés Matte edited this page Feb 25, 2016 · 6 revisions

Un content provider es exactamente lo que su nombre dice, un proveedor de contenido o de datos. Ofrece un esquema estructurado para acceder, crear, borrar o actualizar los datos que se pongan a disposición a través de él. En definitiva es la forma en que tu aplicación ofrece sus datos para que puedan ser consumidos o editados por procesos externos a ella o por otras aplicaciones sin la necesidad de que esta esté abierta. Se pueden compartir los datos desde bases de datos o archivos, pero en este caso utilizaremos la primera.

Se pueden usar incluso como una forma de abstraer la implementación de un sistema de datos complejo frente al resto de los desarrolladores de una misma aplicación. De esta forma el resto de los integrantes del equipo solo deben estar al tanto de los estándares de comunicación con el content provider y no necesitan preocuparse de detalles de la implementación. De todas formas su principal objetivo es compartir los datos con otros procesos.

Qué haremos

En esta sección cubriremos los siguientes temas:

  1. Funcionamiento general

  2. Content URIs

  3. Clase Contrato

  4. Clase Content Provider

  5. Cómo utilizar el Content Provider

  6. Permisos

Funcionamiento general

Como se aprecia en la figura el flujo parte desde tu aplicación obteniendo una instancia de ContentResolver[^2]. Luego, a través de esta se hace una consulta pasando como primer parámetro una URI que hace referencia a un conjunto de datos que ofrece un content provider específico. Es importante saber que pueden haber muchos content providers disponibles, cada uno con su conjunto de URIs (corresponde un URI para cada conjunto de datos ofrecido en el provider). Por ejemplo, android provee Contacts Provider[^3] y Calendar Provider[^4]. En la próxima sección se entrará más en detalles en el diseño de las URIs.

Nuestra instancia de ContentResolver decide a qué provider hacer la consulta basado en la URI. Luego el content provider determina a qué conjunto de datos debe afectar (también basado en la URI) e interactúa con la base de datos para realizar la operación que fue invocada.

Flujo general de content provider<span
data-label="fig:flujo_content_provider"></span>

Content URIs

Un content URI es un identificador de un conjunto de datos en un provider. Está formado por el identificador del provider (authority) y por un nombre que indica a la tabla o al contenido que hace referencia. Cuando se llama a un método para acceder a algún conjunto de datos se debe incluir en los parámetros una URI determinada. Esto para que el ContentResolver pueda determinar a qué provider le corresponde manejar el llamado, y para que el provider pueda determinar sobre que datos realizar la operación.

Estructura content URI<span
data-label="fig:estructura_content_URI"></span>

Authority

La authority es el nombre de tu provider. Por lo general el formato de este viene dado por el package de la aplicación más ’provider’. Por ejemplo, en el caso de nuestra aplicación es com.example.andres.myapplication.provider. En el package (y en este caso, en la authority) se suele utilizar como primeras palabras un dominio de internet, al revés, al cual pertenece la aplicación. En este caso asumimos que hay un dominio con la ruta andres.example.com.

Estructura de rutas URI

Para el content provider se puede diseñar una estructura nueva de los datos de acuerdo a lo que se quiera ofrecer. En la clase contrato definiremos nuevas tablas (que pueden ser iguales a las de tu base de datos o no) que conformarán la estructura de rutas. En nuestro caso solo definiremos una tabla Students igual a la de nuestra base de datos. Claramente se pueden hacer estructuras más complejas con tablas anidadas y más esoterismos, pero no se cubrirán en este tutorial[^5].

Para nuestra aplicación definiremos dos URIs, una para obtener la lista completa de estudiantes y otra para obtener a un estudiante en particular basándonos en su ID. La primera es exactamente como la que está en la Figura [fig:estructura_content_URI]. La segunda debe poder soportar llamados del tipo

content://com.example.andres.myapplication.provider/students/3,

donde 3 es el id del estudiante a seleccionar. Para poder soportar esto debemos definir la URI de la siguiente manera:

content://com.example.andres.myapplication.provider/students/#

Clase Contrato

La clase contrato es la que contiene todos los URIs, MIME Types y constantes necesarias para poder tener un protocolo de comunicación fijo entre los distintos procesos que utilizan el provider. Esta clase es la que debes compartir con otros desarrolladores si quieres que usen tu provider, ya que con ella les proveerás todos las valores necesarios para que se puedan comunicar con él de manera correcta. Básicamente, en la aplicación que utilice el provider se debe copiar y pegar la clase contrato.

A continuación se presenta el código de la clase contrato de nuestra aplicación de ejemplo. Los conceptos involucrados se explicarán luego.

    import android.content.ContentResolver;
    import android.net.Uri;
    import android.provider.BaseColumns;

    /**
     *  Esta clase provee las constantes y URIs necesarias para trabajar con el StudentsProvider
     */
    public final class StudentsContract {

        public static final String AUTHORITY = "com.example.andres.myapplication.provider";
        public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
        public static final Uri STUDENTS_URI = Uri.withAppendedPath(StudentsContract.BASE_URI, "/students");

        /*
            MIME Types
            Para listas se necesita  'vnd.android.cursor.dir/vnd.com.example.andres.provider.students
            Para items se necesita 'vnd.android.cursor.item/vnd.com.example.andres.provider.students'
            La primera parte viene esta definida en constantes de ContentResolver
         */
        public static final String URI_TYPE_STUDENT_DIR = ContentResolver.CURSOR_DIR_BASE_TYPE +
                "/vnd.com.example.andres.provider.students";

        public static final String URI_TYPE_STUDENT_ITEM = ContentResolver.CURSOR_ITEM_BASE_TYPE +
                "/vnd.com.example.andres.provider.students";

        /*
            Tabla definida en provider. Aca podria ser una distinta a la de la base de datos,
            pero consideramos la misma.
         */
        public static final class StudentsColumns implements BaseColumns{

            private StudentsColumns(){}

            public static final String NAMES = "names";
            public static final String FIRST_LASTNAME = "firstlastname";
            public static final String SECOND_LASTNAME = "secondlastname";
            public static final String ID_CLOUD = "idcloud";

            public static final String DEFAULT_SORT_ORDER = FIRST_LASTNAME + " ASC";

        }
    }

MIME Types

De lo que está presente en la clase contrato y no se ha hablado es de los MIME Types. Estos conforman una manera estandar de clasificar los tipos de archivos o datos. Estos tipos son los mismos siempre, independiente del sistema operativo o de cualquier otra variante que se presente. Un MIME Type tiene dos partes: un tipo y un sub-tipo que están separados por un slash (/). Por ejemplo, las imágenes de formato JPEG tienen el MIME Type image/jpeg.

En el caso del provider se especifican dos MIME Types principales. El primero es para items individuales, donde el tipo viene dado por vnd.android.cursor.item y el subtipo (para nuestro provider) por /vnd.com.example.andres.provider.students. El otro es para listas, donde el tipo viene dado por vnd.android.cursor.dir y el subtipo (para nuestro provider) por /vnd.com.example.andres.provider.students (igual que para el de items individuales). De esta forma los MIME Types completos quedan como se especifica en el código.

Los MIME Types son importantes debido a que con ellas el desarrollador puede determinar el tipo de dato que se le retornará si utiliza una URI determinada en una consulta. No hay que olvidar que esta clase es principalmente un apoyo para que otros puedan utilizar los datos que les provees, por lo tanto se debe ser consistente y claro en la implementación.

Clase Content Provider

A continuación se creará una clase StudentProvider que herede de la clase abstracta ContentProvider. Esta obliga a implementar una serie de métodos especificados en el código que se mostrará a continuación. Por ejemplo, se pide implementar el método query. Este método tiene el mismo nombre que el que se usa desde ContentResolver. Es decir, si llamamos a ContentResolver.query(), el content resolver determinará a qué provider llamar basado en la URI y llamará al método query() de ese provider.

    import android.content.ContentProvider;
    import android.content.ContentValues;
    import android.content.UriMatcher;
    import android.database.Cursor;
    import android.database.sqlite.SQLiteDatabase;
    import android.net.Uri;
    import android.text.TextUtils;

    /*
       Clase que extiende ContentProvider y que interactua con la base de datos
     */
    public class StudentsProvider extends ContentProvider {
        public static final int STUDENT_LIST = 1;
        public static final int STUDENT_ID = 2;
        private static final UriMatcher sUriMatcher;
        static{
            sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
            /*
                URI para todos los estudiantes.
                Se setea que cuando se pregunta a UriMatcher por la URI:
                content://com.example.andres.myapplication.provider/students
                se devuelva un entero con el valor de 1.
             */
            sUriMatcher.addURI(StudentsContract.AUTHORITY, "students", STUDENT_LIST);
            /*
                URI para un estudiante.
                Se setea que cuando se pregunta a UriMatcher por la URI:
                content://com.example.andres.myapplication.provider/students/#
                se devuelva un entero con el valor de 2.
             */
            sUriMatcher.addURI(StudentsContract.AUTHORITY, "students/#", STUDENT_ID);
        }
        /*
            Instancia de StudentsDbHelper para interactuar con la base de datos
         */
        private StudentsDbHelper mDbHelper;

        public StudentsProvider() { }

        @Override
        public boolean onCreate() {
            mDbHelper = StudentsDbHelper.getInstance(getContext());
            return true;
        }

        /*
            Llamado para borrar una o mas filas de una tabla
         */
        @Override
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int rows = 0;
            switch (sUriMatcher.match(uri)) {
                case STUDENT_LIST:
                    // Se borran todas las filas
                    rows = db.delete(DatabaseContract.Students.TABLE_NAME, null, null);
                    break;
                case STUDENT_ID:
                    // Se borra la fila del ID seleccionado
                    rows = db.delete(DatabaseContract.Students.TABLE_NAME, selection, selectionArgs);
            }
            // Se retorna el numero de filas eliminadas
            return rows;
        }

        /*
            Se determina el MIME Type del dato o conjunto de datos al que apunta la URI
         */
        @Override
        public String getType(Uri uri) {
            switch (sUriMatcher.match(uri)){
                case STUDENT_LIST:
                    return StudentsContract.URI_TYPE_STUDENT_DIR;
                case STUDENT_ID:
                    return StudentsContract.URI_TYPE_STUDENT_ITEM;
                default:
                    return null;
            }
        }

        /*
            Inserta nuevo estudiante
         */
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            db.insert(DatabaseContract.Students.TABLE_NAME, null, values);

            // Le avisa a los observadores
            getContext().getContentResolver().notifyChange(uri, null);

            return null;
        }

        /*
            Retorna el o los datos que se le pida de acuerdo a la URI
         */
        @Override
        public Cursor query(Uri uri, String[] projection, String selection,
                            String[] selectionArgs, String sortOrder) {

            SQLiteDatabase db = mDbHelper.getReadableDatabase();
            switch (sUriMatcher.match(uri)){

                // Se pide la lista completa de estudiantes
                case STUDENT_LIST:
                    // Si no hay un orden especificado,
                    // lo ordenamos de manera ascendente de acuerdo a lo que diga el contrato
                    if (sortOrder == null || TextUtils.isEmpty(sortOrder))
                            sortOrder = StudentsContract.StudentsColumns.DEFAULT_SORT_ORDER;
                    break;

                // Se pide un estudiante en particular
                case STUDENT_ID:
                    // Se adjunta la ID del estudiante selecciondo en el filtro de la seleccion
                    if (selection == null)
                            selection = "";
                    selection = selection + "_ID = " + uri.getLastPathSegment();
                    break;

                // La URI que se recibe no esta definida
                default:
                    throw new IllegalArgumentException(
                            "Unsupported URI: " + uri);
            }
            Cursor cursor = db.query(DatabaseContract.Students.TABLE_NAME,
                                     projection,
                                     selection,
                                     selectionArgs,
                                     null,
                                     null,
                                     sortOrder);
            // Se retorna un cursor sobre el cual se debe iterar para obtener los datos
            return cursor;
        }

        @Override
        public int update(Uri uri, ContentValues values, String selection,
                          String[] selectionArgs) {
            // No se implemento un update
            throw new UnsupportedOperationException("Not yet implemented");
        }
    }

URI Matcher

Una clase importante que se utiliza en StudentProvider es UriMatcher. Esta clase se encarga de ayudarte a determinar que acción seguir para cada URI definida. Esto lo logra asociando cada URI a un entero que idealmente debes definir como constante. Además permite definir URIs genéricas, como se ve en el código para la elección de estudiantes por su ID.

Cómo utilizar el Content Provider

Una vez que se tiene todo lo anterior definido correctamente podemos hacer uso de nuestro ContentProvider. Para probar si funciona correctamente hay dos opciones: probarlo simplemente en tu aplicación o crear otra aplicación de prueba que lo utilice. Ahora el uso será muy sencillo, por ejemplo para seleccionar todos los estudiantes se debe hacer lo siguiente:

    Cursor c = mContentResolver.query(StudentsContract.STUDENTS_URI, null, null, null, null);
    c.moveToFirst();
    while (c.moveToNext()){
        String names = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.NAMES));
        String firstLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.FIRST_LASTNAME));
        String secondLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.SECOND_LASTNAME));
        // Hacer lo que se necesite con los datos
    }
    c.close();

Para seleccionar un estudiante cuyo ID = 1 se podría hacer lo siguiente:

    Cursor c = mContentResolver.query(Uri.withAppendedPath(StudentsContract.STUDENTS_URI, "1"), null, null, null, null);
    c.moveToFirst();
    String names = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.NAMES));
    String firstLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.FIRST_LASTNAME));
    String secondLast = c.getString(c.getColumnIndexOrThrow(StudentsContract.StudentsColumns.SECOND_LASTNAME));
    // Hacer lo que se necesite con los datos
    c.close();

Permisos

Se deben setear ciertos permisos en el archivo AndroidManifest.xml presente en toda aplicación android. Para nuestra aplicación agregaremos solo lo fundamental, que es lo siguiente:´

    ...
    <provider
       android:name=".Provider.StudentsProvider"
       android:authorities="com.example.andres.myapplication.provider"
       android:enabled="true"       // Permite al sistema iniciar el provider
       android:exported="true"      // Permite que otras apps usen el provider
       android:syncable="true">     // Indica que los datos del provider son sincronizados con la nube
    </provider>
    ...

Siguiente Authenticator.

Para más información sobre tipos de permisos más complejos se recomienda visitar http://developer.android.com/guide/topics/providers/content-provider-creating.html#Permissions.