miércoles, 17 de septiembre de 2014

Proveedores de contenido, conceptos básicos

Los proveedores de contenido son objetos encargados de gestionar el acceso a un conjunto de datos estructurados. Encapsulan los datos y nos proveen de mecanismos para definir su seguridad. Los proveedores de contenido definen una interfaz estándar que permite a un proceso compartir sus datos con otros procesos.

Cuando queremos acceder a los datos de un proveedor de contenido, utilizaremos el objeto ContentResolver asociado al contexto de nuestra aplicación para comunicarnos como cliente del proveedor. Es decir, utilizaremos el objeto ContentResolver para comunicarnos con un objeto ContentProvider. El objeto proveedor recibe las peticiones de datos desde uno o varios clientes, realiza las acciones solicitadas y devuelve los resultados.

Si los datos de nuestra aplicación no se van a compartir con otras aplicaciones, no tendremos que implementar nuestro propio proveedor. Sin embargo, necesitaremos desarrollar un proveedor para nuestra aplicación si queremos implementar mecanismos de búsqueda con predicción de texto. También necesitaremos de proveedores si queremos que se puedan copiar y pegar datos estructurados o archivos desde nuestra aplicación a otras aplicaciones.

Android incorpora una serie de proveedores de contenido para audio, vídeo, imágenes y contactos. Con algunas restricciones, estos proveedores serán accesibles desde cualquier aplicación.

Toma de contacto.

Un proveedor de contenido presenta los datos a una aplicación externa como un conjunto de tablas similar a como lo haría una base de datos relacional. Un registro representa una instancia de algún tipo de dato que el proveedor almacena y cada columna del registro representa una parte de los datos almacenados para esa instancia.

Por ejemplo, uno de los proveedores ya incluido en la plataforma de Android, es el usado para gestionar el acceso al diccionario del usuario. En él se almacenarán palabras que no se encuentren en el diccionario estándar y que el usuario desee conservar. La tabla 1 muestra algunos de los datos almacenados en la tabla de palabras:

word app id frequency locale _ID
mapreduce user1 100 en_US 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const user1 255 pt_BR 4
int user5 100 en_UK 5
Tabla 1. Ejemplo de diccionario del usuario.

En la tabla 1, cada fila representa palabras que no se encuentran en el diccionario estándar. Las columnas son atributos asociados a esas palabras. Los nombres en las cabeceras de columna se almacenan en el proveedor para poder hacer referencia a los atributos. Por ejemplo, para referirnos a la clave principal lo haremos a través de la columna _ID.

Nota. No estamos obligados a utilizar una columna como clave principal en un proveedor y tampoco tendremos que utilizar el nombre _ID para la clave principal si ésta existiese. Sin embargo, si queremos enlazar los datos de un proveedor automáticamente con una vista ListView, una de las columnas tendrá que llamarse _ID.

Accediendo a un proveedor.

Para acceder a los datos de un proveedor utilizaremos una instancia de la clase ContentResolver. Este objeto dispone de métodos que tendrán sus iguales declarados en el proveedor (instancia de alguna de las subclases de ContentProvider). Los métodos disponibles nos proporcionan operaciones básicas para crear, leer, actualizar y borrar datos persistentes (CRUD).

La instancia de tipo ContentResolver en el proceso cliente y el objeto de tipo ContentProvider en la aplicación que provee los datos, gestionan la comunicación entre procesos. ContentProvider también hace de capa de abstracción entre el repositorio de datos y su apariencia externa a modo de tablas.

Nota. Para acceder a los datos de un proveedor, nuestras aplicaciones tendrán que declarar los permisos necesarios en el archivo manifest.

Por ejemplo, para obtener un listado de las palabras contenidas en el diccionario del usuario, llamaremos al método ContentResolver.query() especificando el URI a la tabla de palabras. Este método query() a su vez llamará al método ContentProvider.query() definido por el proveedor del diccionario del usuario.
// Consulta al diccionario del usuario
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // El URI a la tabla de palabras
    mProjection,                        // Cláusula de proyección
    mSelectionClause                    // Cláusula de selección
    mSelectionArgs,                     // Argumentos para la cláusula de selección
    mSortOrder);                        // Cláusula de ordenación (ORDER BY)

La tabla 2 muestra la correspondencia entre los argumentos del método query(Uri, projection, selection, selectionArgs, sortOrder) y SQL.

query() SQL Nota
Uri FROM table_nameEl Uri enlaza a la tabla del proveedor table_name en este caso.
projection col, col, col, ... Cláusula de proyección. Un array con los nombres de las columnas para las que queremos obtener sus valores.
selection WHERE col = value Cláusula de selección. Una cadena con el criterio de selección de registros.
selectionArgs No existe equivalencia con SQL. Es un array de valores que pasarán a sustituir cada uno de los caracteres ? que hayamos especificado en la cláusula de selección para completarla.
sortOrder ORDER BY col, col, col, ... Cláusula de ordenación.
Tabla 2. Correspondencia entre los argumentos del método query() y SQL.

URIs al contenido.

Una URI identifica datos concretos en un proveedor. Las URIs incluyen un nombre que representa al proveedor (autoridad) seguido del nombre de la tabla (ruta). Cuando accedemos a la tabla de un proveedor desde un cliente, el URI será uno de los argumentos necesarios.

En el código anterior, la constante CONTENT_URI contenía el URI que hace referencia a la tabla que contienen las palabras del diccionario del usuario. El objeto ContentResolver extrae el proveedor del URI (su autoridad) y lo intenta localizar en la tabla de proveedores registrados del sistema. Si lo encuentra, le entregará la consulta.

ContentProvider usará la ruta del URI para acceder a una tabla indicada. Un proveedor generalmente tiene una ruta definida para cada una de sus tablas.

En el código anterior, el URI completo a la tabla de palabras es:
content://user_dictionary/words
Donde user_dictionary es el proveedor (autoridad) y words es la ruta a la tabla. content:// es el esquema que identifica un URI de contenido.

Algunos proveedores nos permitirán acceder a un registro determinado de una tabla añadiendo su identificador al final del URI. Por ejemplo, para obtener el registro cuya columna _ID vale 4 en la tabla del diccionario del usuario, podemos construir el URI relacionado de la siguiente manera:
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
Nota. Las clases Uri y Uri.Builder contienen los métodos adecuados para la construcción de URIs bien formadas. La clase ContentUris contiene los métodos necesarios para añadir un identificador a un URI. En el ejemplo anterior se usa el método withAppendedId() para añadir el identificador al final del URI que enlaza a la tabla del diccionario del usuario (UserDictionary.Words.CONTENT_URI).

Obteniendo datos del proveedor.

Para hacer que el código sea lo más claro posible, realizaremos las consultas desde la hebra principal asociada a la interfaz de usuario (UI thread). Sin embargo, estas consultas debería hacerse de manera asíncrona desde una hebra diferente, es decir, deberíamos permitir que la interfaz del usuario se pintase independientemente de la carga de los datos. Una manera de hacer esto sería usando la clase CursorLoader.

Para obtener datos de un proveedor, seguiremos dos pasos básicos:
  1. Solicitar permiso de lectura para ese proveedor.
  2. Escribir el código para realizar la consulta.

Solicitando permiso de lectura.

Para tener acceso a los datos de un proveedor, nuestra aplicación necesita tener permiso de lectura sobre el proveedor. No podremos solicitar este permiso en tiempo de ejecución; en su lugar, tendremos que hacerlo a través del archivo manifest, usando un elemento <uses-permission> en el que especificaremos el nombre exacto del permiso que vendrá dado por el propio proveedor. Durante el proceso de instalación de nuestra aplicación, el sistema de paquetes solicitará dicho permiso al usuario, siendo éste el responsable final de su aprobación.

El proveedor para el diccionario del usuario define la constante android.permission.READ_USER_DICTIONARY para conceder permisos de lectura sobre sus datos. Esta constante es la que utilizaremos en el archivo manifest:

/app/manifests/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication" >

    <application>
    ...
    </application>
    <uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
</manifest>

Construyendo la consulta.

El próximo paso sería obtener datos del proveedor realizando una consulta:
// Definimos la proyección
String[] mProjection =
{
    UserDictionary.Words._ID,    // Columna _ID. Clave principal.
    UserDictionary.Words.WORD,   // Columna WORD. La palabra.
    UserDictionary.Words.LOCALE  // Columna LOCALE. Localización.
};

// Definimos la cláusula de selección (null seleccionará todos los registros)
String mSelectionClause = null;

// Definimos los argumentos para la cláusula de selección
String[] mSelectionArgs = {""};
En la siguiente secuencia de código usaremos el método ContentResolver.query() sobre el proveedor del diccionario del usuario. Es similar a realizar una consulta SQL: contiene una cláusula SELECT, una cláusula WHERE y una cláusula ORDER BY.

Al conjunto de columnas cuyos valores queremos sean devueltos por la consulta, le llamamos proyección (variable mProjection).

La cláusula de selección estaría compuesta por la unión de la cláusula de selección con sus argumentos (variables mSelectionClause y mSelectionArgs). Para cada ocurrencia del carácter ? en la variable mSelectionClause, será sustituido por un valor del array mSelectionArgs.

En la siguiente secuencia de código, si el usuario no introduce una palabra, la cláusula de selección será null y la consulta devolverá todas las palabras de la tabla. Si el usuario introduce una palabra, el resultado se verá restringido a los registros que cumplan la condición UserDictionary.Words.WORD + " = ?":
/*
 * Definimos los argumentos de la cláusula de selección por defecto.
 */
String[] mSelectionArgs = {""};

// Obtenemos la palabra desde la interfaz del usuario.
mSearchString = mSearchWord.getText().toString();

// Aquí deberíamos validar la palabra introducida.

// Si el usuario no ha introducido una palabra
if (TextUtils.isEmpty(mSearchString)) {
    // Obtenemos todas las palabras (cláusula de selección null).
    mSelectionClause = null;
    mSelectionArgs[0] = "";
} else {
    // Construimos la cláusula de selección teniendo en cuenta la palabra introducida.
    mSelectionClause = UserDictionary.Words.WORD + " = ?";

    // La cláusula de selección sustituirá su carácter ? por el valor especificado
    // en su array de argumentos: la palabra introducida por el usuario.
    mSelectionArgs[0] = mSearchString;
}

// Realizamos la consulta que nos devolverá un cursor con los resultados.
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // Tabla del diccionario del usuario.
    mProjection,                       // Cláusula de proyección.
    mSelectionClause                   // Cláusula de selección.
    mSelectionArgs,                    // Argumentos de la cláusula de selección.
    mSortOrder);                       // Cláusula de ordenación.

// Si se produjo un error, el cursor será nulo.
// Algunos proveedores lanzarán una excepción.
if (null == mCursor) {
    /*
     * Tratamos el error. No deberíamos usar el cursor aquí.
     * Podríamos registrar el error con android.util.Log.e().
     */
// Si el cursor está vacío.
} else if (mCursor.getCount() < 1) {

    /*
     * Notificamos al usuario que no se encontraron resultados.
     * No tiene por qué tratarse de un error.
     * Podríamos ofrecer al usuario la posibilidad de volver a introducir una nueva
     * palabra y repetir la búsqueda.
     */

} else {
    // Procesamos el resultado.
}
En SQL tendríamos la sentencia análoga siguiente:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

Validando los datos introducidos.

Si el proveedor de contenidos almacena sus datos en una base de datos SQL, añadirle los datos introducidos por el usuario sin ser validados previamente, nos podría ocasionar problemas.

Supongamos la siguiente cláusula de selección:
// mUserInput contiene un valor introducido por el usuario
String mSelectionClause =  "var = " + mUserInput;
Si hacemos esto, estaremos permitiendo al usuario a que pueda inyectar código SQL en la variable mSeletionClause. Por ejemplo, el usuario podría haber introducido el siguiente código: nothing; DROP TABLE *;. En este caso, nuestra variable mSelectionClause contendría el valor var = nothing; DROP TABLE *;. Si nuestro proveedor llegase a ejecutar esta consulta, acabaríamos borrando todas las tablas de nuestra base de datos (a menos que el proveedor fuera capaz de detectar este tipo de código antes de ejecutar la consulta).

Para evitar ataques por inyección de SQL, en vez de usar los valores introducidos por el usuario directamente en la cláusula de selección, usaremos el carácter ? en su lugar y los valores de usuario, los añadiremos al array de argumentos de selección. Haciendo esto, los valores del array serán tratados como simples datos sin llegar ser interpretados como sentencias SQL:
// Definimos el array de argumentos de selección
String[] selectionArgs = {""};

// Creamos la cláusula de selección.
// Utilizamos una ? por cada valor introducido por el usuario.
String mSelectionClause =  "var = ?";

// Añadimos el valor que sustituirá el carácter ? de la cláusula de selección.
selectionArgs[0] = mUserInput;

// Realizamos la consulta
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  
    mProjection,                       
    mSelectionClause 
    mSelectionArgs,                    
    mSortOrder);                       

Utilizaremos esta técnica incluso si el proveedor de contenidos no se basa en una base de datos SQL.

Mostrando los resultados de la consulta.

Cuando invocamos el método ContentResolver.query() desde un cliente, el resultado será un objeto de tipo Cursor conteniendo los registros que cumplen con la cláusula de selección. Un objeto de tipo Cursor nos permite acceso aleatorio de lectura a los registros y columnas que contiene. Usando los métodos de la clase Cursor podremos iterar sobre los registros resultado, determinar el tipo de cada columna, consultar los valores de cada columna y examinar otras propiedades asociadas al resultado. Algunas implementaciones de la clase Cursor actualizan automáticamente los datos cuando estos cambian en el proveedor, o se invocan métodos de un objeto que controla cambios en el Cursor (patrón observer), o ambos.

Nota. Un proveedor debería restringir el acceso a ciertas columnas basándose en la naturaleza del objeto que hace la consulta. Por ejemplo, el proveedor de contactos restringe el acceso a ciertas columnas que solo son accesibles para un adaptador sincronizado. Sin embargo, esas mismas columnas no serán accesibles desde una actividad o un servicio.

Si no se encuentran registros que cumplan con el criterio de selección, el proveedor devolverá un objeto de tipo Cursor donde el método Cursor.getCount() devuelve 0 (cursor vacío).

Si se produce algún error realizando la consulta, el resultado final dependerá de proveedor concreto. El proveedor podría devolver el valor null o lanzar una excepción.

Una buena manera de visualizar el resultado de un objeto de tipo Cursor es enlazándolo a una vista de tipo ListView a través de una adaptador SimpleCursorAdapter.

En la siguiente secuencia de código se utiliza un objeto SimpleCursorAdapter conteniendo un Cursor obtenido de la consulta y asigna este adaptador a un objeto de tipo ListView:
// Definimos la cláusula de proyección
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Columna que almacena la palabra
    UserDictionary.Words.LOCALE  // Columna que almacena la localización
};

// Identificadores de la vista. Visualizarán los valores de la cláusula de proyección.
int[] mWordListItems = { R.id.dictWord, R.id.locale };

// Creamos el objeto de tipo SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(), // Contexto de la aplicación
    R.layout.wordlistrow,    // Plantilla XML con una vista ListView personalizada
    mCursor,                 // Resultado de la consulta
    mWordListColumns,        // Cláusula de selección
    mWordListItems,          // Identificadores de la vista ListView personalizada
    0);                      // Indicadores (generalmente no hacen falta)

// Asignamos el adaptador al objeto ListView
mWordList.setAdapter(mCursorAdapter);
Nota. Para que los datos del cursor se muestren correctamente en una vista de tipo ListView, éste tendrá que tener una columna _ID. Esta columna no será mostrada en la vista pero es necesaria.

Accediendo a los registros del cursor.

Más allá de querer mostrar los resultados de una consulta, nos podría interesar realizar ciertas operaciones con ellos. Por ejemplo, podríamos obtener ciertas palabras del diccionario del usuario y después buscarlas en otros proveedores. Para hacer esto, tendremos que iterar sobre los registros del Cursor devuelto por la consulta:
// Obtenemos la posición que ocupa la columna WORD en su tabla.
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Solo se ejecuta si el resultado es válido. El proveedor devolverá null si
 * se produce algún error. Otros proveedores lanzarán una excepción.
 */
if (mCursor != null) {
    /*
     * Por defecto, la fila a la que apunta el cursor no existe. Para acceder
     * al primer registro con datos, tendremos que desplazar el cursor una
     * posición. Si no lo hacemos, se lanzará una excepción.
     */
    while (mCursor.moveToNext()) {

        // Obtenemos el valor de la columna WORD.
        newWord = mCursor.getString(index);

        // Procesamos el valor obtenido.

        ...

        // fin del bucle while
    }
} else {

    // Procesamos el error. El proveedor devolvió el valor null.
}
La clase Cursor contiene varios métodos get para obtener tipos diferentes de datos. Por ejemplo, en la secuencia de código anterior se ha utilizado el método getString(). También podemos utilizar el método getType() si queremos saber de qué tipo es el valor de una columna.

Permisos.

Las aplicaciones proveedoras de contenidos deben definir los permisos necesarios para que otras aplicaciones puedan acceder a sus datos. Las aplicaciones que necesiten acceder a los datos de un proveedor tendrán que especificar dichos permisos en el archivo manifest. Será el usuario final el encargado de confirmar los permisos durante el proceso de instalación de las aplicaciones.

Si una aplicación proveedora no define sus permisos entonces el resto de aplicaciones no tendrán acceso a sus datos. Sin embargo, los componentes propios de la aplicación proveedora siempre tendrán acceso total (lectura y escritura), independientemente de si se han definido o no esos permisos.

Como vimos anteriormente, el proveedor del diccionario del usuario requería el permiso android.permission.READ_USER_DICTIONARY para poder consultar sus datos. También define un permiso android.permission.WRITE_USER_DICTIONARY para permitir acceso de escritura a sus datos (insertar, actualizar y borrar).

Para obtener los permisos necesarios para acceder a un proveedor, la aplicación los solicita usando elementos <uses-permission> en el archivo manifest. Cuando el gestor de paquetes de Android instala la aplicación, el usuario deberá aprobar todos los permisos solicitados por ésta. Si el usuario los aprueba todos, el gestor de paquetes continuará con la instalación; si el usuario no los aprueba, el sistema de paquetes aborta la instalación.

En la siguiente secuencia de código, vemos cómo solicitar permiso de lectura sobre el diccionario del usuario:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />

Insertando, actualizando y borrando datos.

De la misma manera que obtenemos información de un proveedor, también podremos modificar sus datos.

Insertando datos.

Para insertar datos en un proveedor, utilizaremos el método ContentResolver.insert(). Este método inserta un nuevo registro en el proveedor y devuelve su URI. En el siguiente ejemplo se muestra cómo insertar una nueva palabra en el proveedor del diccionario del usuario:
// Variable donde almacenaremos el URI del nuevo registro
Uri mNewUri;

...

// Creamos la variable que almacenará los valores del registro.
ContentValues mNewValues = new ContentValues();

/*
 * Asignamos los valores a cada columna del registro.
 */
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,   // El URI a la tabla del diccionario
    mNewValues                         // El registro a insertar (palabra nueva)
);
Los valores que queremos insertar los almacenamos en un objeto de tipo ContentValue, equivalente al registro de un cursor. Las columnas en el objeto se especifican como cadenas y no tienen por qué corresponderse en tipo con el tipo de la columna. Puedes especificar el valor null para una columna usando el método ContentValues.putNull().

Observa que en el código en ningún momento se especifica valor para la columna _ID. El valor para esta columna se genera automáticamente. El proveedor se encargará de asignar automáticamente este valor cada vez que se inserte un registro. Este campo suele ser la clave principal de la tabla.

El URI devuelto, identifica al nuevo registro insertado y será de la forma: content://user_dictionary/words/<id_nuevo>.

<id_nuevo> es el _ID generado para el registro.

Podemos obtener el _ID del URI devuelto usando el método ContentUris.parseId().

Actualizando datos.

Para actualizar un registro determinado, usaremos un objeto de tipo ContentValues con los datos que queremos actualizar. También necesitaremos un cláusula de selección para indicar a qué registros aplicar los cambios. Para las operaciones de actualización usaremos el método ContentResolver.update(). El método devuelve el número de registros actualizados. Si queremos eliminar el valor de alguna de las columnas especificaremos su valor a null.

En el siguiente ejemplo asignaremos el valor null a la columna LOCALE si su valor empieza por la cadena en:
// Definimos el objeto que contendrá los valores
ContentValues mUpdateValues = new ContentValues();

// Definimos la cláusula de selección
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// Número de registros afectados por la actualización. Inicialmente vale cero.
int mRowsUpdated = 0;

...

/*
 * Especificamos el valor null para la columna LOCALE.
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,   // URI a la tabla del diccionario del usuario.
    mUpdateValues                       // Las columnas a actualizar.
    mSelectionClause                    // Cláusula de selección.
    mSelectionArgs                      // Argumentos de selección.
);
Deberíamos limpiar la entrada del usuario antes de invocar el método ContentResolver.update().

Borrando datos.

Borrar datos es muy similar a como se obtienen. Tendremos que especificar una cláusula de selección para los datos que queramos eliminar. El método devolverá el número de registros eliminados. En el siguiente ejemplo, eliminamos los registros que tengan el valor user en la columna APP_ID:
// Definimos el criterio de selección.
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

// Definimos la variable que almacenará el número de registros eliminadas.
int mRowsDeleted = 0;

...

// Eliminamos las palabras que cumplan con el criterio de selección.
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   // URI a la tabla del diccionario del usuario.
    mSelectionClause                    // Cláusula de selección.
    mSelectionArgs                      // Argumentos de selección.
);
Deberíamos limpiar la entrada del usuario antes de invocar el método ContentResolver.delete().

Tipo de los datos.

Los proveedores de contenido pueden servir datos de múltiples tipos. El proveedor del diccionario del usuario sólo entrega datos de tipo texto, pero los proveedores pueden entregar los siguiente tipos adicionales:
  • Entero (int).
  • Entero largo (long).
  • Coma flotante.
  • Coma flotante largo (double).

Podrás consultar los tipos de dato de un proveedor desde su documentación. Los tipos de dato para el proveedor del diccionario del usuario puedes consultarlos desde su clase contractual UserDictionary.Words. También podemos obtener el tipo de un dato utilizando el método Cursor.getType().

Los proveedores también mantienen un tipo MIME asociado a cada URI definida. Podríamos usar el tipo MIME para averiguar si nuestra aplicación es capaz de trabajar con los contenidos del proveedor, o realizar una u otra operación en función del tipo MIME de los datos. Generalmente necesitaremos usar el tipo MIME cuando tengamos que trabajar con estructuras complejas de datos o con archivos. Por ejemplo, la tabla ContactsContract.Data en el proveedor de contactos usa un tipo MIME para etiquetar a cada uno de los contactos almacenados en cada una de sus registros. Para obtener el tipo MIME asociado al URI usaremos el método ContentResolver.getType().

Formas alternativas de acceder a un proveedor.

Hay tres maneras de acceder a un proveedor:
  • Acceso por lotes. Podemos crear una secuencia de operaciones con ContentProviderOperation y ejecutarlas posteriormente con el método ContentResolver.applyBatch().
  • Consultas asíncronas. Podemos realizar las consultas desde una hebra independiente. Una manera de hacer esto automáticamente es usando la clase CursorLoader.
  • A través de intenciones. Aunque no podemos enviar intenciones directamente a un proveedor, podemos enviar una intención a la aplicación del proveedor para que ésta sea la encargada de modificar los datos del proveedor.

Acceso por lotes.

El procesamiento por lotes está indicado para cuando necesitemos insertar un número elevado de registros, o para insertar registros en varias tablas con tan solo invocar un método, o en general para realizar un conjunto de operaciones como una transacción (una operación atómica). Para el procesamiento por lotes, necesitaremos definir un array de operaciones con la clase ContentProviderOperation y posteriormente ejecutarlas con el método ContentResolver.applyBatch(). Cada objeto ContentProviderOperation estará relacionado con un URI y éste estará asociado con la tabla sobre la que se realizará la operación. El método ContentResolver.applyBatch() devolverá un array con los resultados.
 ArrayList ops =
          new ArrayList();
 ...
 int rawContactInsertIndex = ops.size();
 ops.add(ContentProviderOperation.newInsert(RawContacts.CONTENT_URI)
          .withValue(RawContacts.ACCOUNT_TYPE, accountType)
          .withValue(RawContacts.ACCOUNT_NAME, accountName)
          .build());

 ops.add(ContentProviderOperation.newInsert(Data.CONTENT_URI)
          .withValueBackReference(Data.RAW_CONTACT_ID, rawContactInsertIndex)
          .withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
          .withValue(StructuredName.DISPLAY_NAME, "Mike Sullivan")
          .build());

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

Acceso mediante intenciones.

Las intenciones pueden dar acceso a un proveedor de manera indirecta. Una intención nos podría dar acceso a los datos de un proveedor incluso si nuestra aplicación no tuviera los permisos necesarios declarados en el archivo manifest.

Obteniendo acceso temporal.

Aún si nuestra aplicación no tuviera los permisos necesarios para usar un proveedor determinado, podríamos enviar una intención a una aplicación que si tuviera esos permisos y recibir el resultado en otra intención con el URI con acceso temporal a los datos. Los permisos se mantendrán durante todo el ciclo de vida de la aplicación. Los indicadores usados en la intención resultado para otorgar este tipo de permiso son:
  • Para otorgar permiso de lectura se utiliza la constante: FLAG_GRANT_READ_URI_PERMISSION.
  • Para otorgar permiso de escritura se utiliza la constante: FLAG_GRANT_WRITE_URI_PERMISSION.

Nota. Estos indicadores no otorgan acceso de lectura y escritura al proveedor asociado al URI (su autoridad). El acceso es solo para ese URI concreto. Un proveedor define los permisos sobre URIs en el archivo manifest a través de su atributo android:grantUriPermission y elementos hijo <grant-uri-permission>.

Por ejemplo, podemos obtener datos para un contacto desde el proveedor de contactos aún sin tener el permiso READ_CONTACTS. Supongamos que necesitamos crear una aplicación que se encargará de felicitar por su cumpleaños a una serie de contactos de nuestra agenda. En lugar de solicitar acceso de lectura a todos los contactos, podremos hacerlo solo con aquellos que nos interesen de la siguiente manera:
  1. Nuestra aplicación enviará una intención con la acción ACTION_PICK y con el tipo MIME CONTENT_ITEM_TYPE usando el método startActivityForResult().
  2. La actividad para seleccionar contactos pasa a un primer plano.
  3. Desde la actividad de contactos, el usuario seleccionará uno. Cuando esto sucede, la actividad ejecuta el método setResult(resultcode, intent) para devolver la intención de vuelta a nuestra aplicación. La intención contendrá el URI al contacto seleccionado y un indicador extra FLAG_GRANT_READ_URI_PERMISSION. Este indicador permite a nuestra aplicación acceso de lectura al contacto seleccionado. La actividad finalizará su ejecución llamando al método finish().
  4. Nuestra aplicación pasa a primer plano, y el sistema invoca su método onActivityResult(). Desde el método podremos acceder a la intención conteniendo el URI al contacto seleccionado.
  5. Con el URI del contacto podremos leer sus datos gracias al indicador GLAG_GRANT_READ_URI_PERMISSION y obtener así su fecha de nacimiento para poder felicitarlo por el día de su cumpleaños.

Usando otra aplicación.

Una manera de poder acceder a datos para los que no tenemos permiso es utilizar actividades de otras aplicaciones que si dispongan de ellos. Por ejemplo, la aplicación Calendario es capaz de aceptar intenciones con acciones de tipo ACTION_INSERT, lo que nos permitiría invocar la actividad relacionada con la creación de un nuevo evento. También podemos especificar datos extra en la intención que la actividad utilizará para rellenar por anticipado ciertos campos de la interfaz del usuario.

Clases contractuales.

Una clase contractual define las constantes que nos ayudaran a trabajar con los URIs de los contenidos, los nombres de columnas, las acciones disponibles para las intenciones y cualquier otra característica que esté relacionada con un proveedor. Las clases contractuales no vienen incluidas con los proveedores por defecto y será el propio desarrollador el encargado de hacer que este tipo de clases se encuentren disponibles para el resto de desarrolladores.

Muchos de los proveedores incluidos en Android disponen de su propia clase contractual. Estas clases las podemos encontrar dentro del paquete android.provider. Por ejemplo, el proveedor del diccionario del usuario tiene una clase contractual llamada UserDictionary conteniendo el URI al contenido y los nombres de las columnas. El URI viene definido por la constante UserDictionary.Words.CONTENT_URI. La clase interna UserDictionary.Words también contiene las constantes que definen los nombres para cada una de las columnas:
String[] mProjection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};
Otra de las clases contractuales es la clase ContactsContract para el proveedor de contactos. Esta clase consta además de varias clases internas como puede ser ContactsContract.Intents.Insert que a su vez es una clase contractual que define las constantes asociadas con intenciones.

Tipos MIME.

Los proveedores de contenido pueden devolver los tipos MIME estándar (multimedia, por ejemplo), tipos MIME propios, o ambos. Los tipos MIME tienen la forma: type/subtype. Por ejemplo, el contenido de un proveedor que trabaje con datos de tipo MIME text/html, será un proveedor cuyos datos son de tipo texto y su contenido está compuesto por etiquetas HTML.

La forma para los tipos MIME propios es algo más compleja:
  • Un tipo MIME propio que haga referencia a un conjunto de registros, será de la forma: vnd.android.cursor.dir.
  • Un tipo MIME propio que haga referencia a un solo registro, será de la forma: vnd.android.cursor.item.

El subtipo de un tipo MIME propio es algo más sencillo. Por ejemplo, cuando la aplicación de contactos crea un registro para un número de teléfono, se le asigna el tipo MIME: vnd.android.cursor.item/phone_v2. En este caso, el subtipo sería phone_v2.

A la hora de escribir un proveedor, tendremos que crear los subtipos a partir de su autoridad y los nombres de tabla. Por ejemplo, supongamos un proveedor que contiene los horarios para tres líneas de tren. La autoridad del proveedor es com.example.trains, y el contenido se organiza por líneas de tren, donde cada línea se corresponde con una tabla: Linea1, Linea2 y Linea3. Los URIs al contenido serían de la forma: content://com.example.trains/Linea1 (para la tabla Linea1). Para este URI, el tipo MIME asociado sería: vnd.android.cursor.dir/vnd.example.linea1. Para el URI content://com.example.trains/Linea2/5 (registro 5 de la tabla Linea2), su tipo MIME sería: vnd.android.cursor.item/vnd.example.linea2.

La mayoría de los proveedores de contenido definen sus clases contractuales con constantes asociadas a sus tipos MIME. Por ejemplo, la clase contractual ContactsContract.RawContacts, define la constante CONTENT_ITEM_TYPE como el tipo MIME para hacer referencia a un solo registro de un contacto tal cual.

1 comentario:

  1. hola tendrás un código ejecutable del proveedor de contenido con el diccionario del usuario??

    ResponderEliminar