El API de contactos
Android nos ofrece todo lo necesario para poder trabajar con los datos relativos a los contactos. En Android Eclair 2.0 (API 5), se incorporó una nueva API, más completa y estructurada para poder manejar e integrar contactos desde múltiples cuentas y orígenes de datos.
La nueva API de contactos la encontramos en android.provider.ContactsContract. Se utilizará el mecanismo provider para poder gestionar los datos.
En la nueva API, los datos están establecidos en tres tablas:
- contactos (contacts).
- contactos en bruto (raw contacts)
- datos (data).
Con esta nueva estructura, el sistema puede almacenar y manejar fácilmente información para un contacto específico desde múltiples orígenes de contactos.
La tabla Data, es una tabla genérica que almacena todos los datos asociados con el contacto en bruto. Cada fila almacena datos de una clase específica, por ejemplo, nombre, foto, email, dirección, números de teléfono y grupo al que pertenece. Cada fila, es etiquetada con un tipo MIME, que es el que identifica el tipo de dato que puede contener. Las columnas son genéricas, siendo el tipo de dato determinado por el tipo MIME que especifiquemos. Por ejemplo, si la clase de datos de una fila es Phone.CONTENT_ITEM_TYPE, la primera columna almacenará el número de teléfono, pero si la clase de datos es Email.CONTENT_ITEM_TYPE entonces la columna almacenará direcciones de mail.
En la clase ContactsContract.CommonDataKinds podemos encontrar definiciones para los tipos de datos más habituales que vamos a poder almacenar en la tabla ContactsContract.Data.
Una fila en la tabla RawContacs representa el conjunto de datos (Data) y otra información que describe a una persona y todo asociado con un único origen de contactos. Por ejemplo, una fila contendrá los datos asociados con una cuenta personal de Google, otra con una cuenta de Exchange o Facebook friend, etc.
Ya sólo nos queda el tercer elemento, el Contacto (Contacts). Una fila en la tabla Contacts representa un conjunto de uno o más contactos en bruto que describen a la misma persona (o entidad).
Cuando añadimos un Raw Contacts, el content provider, se encarga de agregar este dentro de un contacto único. Si no existe ningún contacto que tenga un contacto en bruto con datos similares al que estamos agregando creará un contacto nuevo, en caso contrario, agregará nuestro contacto bruto al contacto que tiene ya datos similares.
Android, tiene una serie de reglas, para establecer si un contacto puede referirse a una misma persona o entidad, teniendo datos ligeramente diferentes. Por ejemplo, podemos tener el contacto "Bob Parr" en dos cuentas, una como compañero de trabajo y la otra como amigo personal. Android, identifica que estamos tratando datos de una misma persona, y lo integra dentro de un mismo contacto.
El sistema automáticamente agrega los contactos por defecto, pero, si es necesario, puedes controlar en tu aplicación como debe manejarse la agregación o incluso desactivarla.
Reglas para la agregación automática.
Como ya hemos comentado, si añadimos o modificamos un contacto en bruto, el sistema buscará otros contactos en bruto con los que exista correspondencia. Si no encuentra ninguno, se creará un contacto nuevo. Si encuentra una correspondencia única, creará un nuevo contacto, que contenga los dos contactos en bruto. En el caso de encontrar múltiples contactos en bruto similares, elegirá la correspondencia más cercana.
Se considera que existe correspondencia entre contactos en bruto si se cumple al menos una de estas condiciones:
- Sus nombres son iguales.
- Sus nombres consisten en las mismas palabras pero en diferente orden (por ejemplo, "Bob Parr", y "Parr, Bob").
- uno de ellos tiene una parte del nombre en común con el otro (por ejemplo, "Bob Parr" y "Robert Parr").
- si existe coindicencia en el nombre o en el apellido. Como esta regla no tiene mucha fortaleza, también se comprueban otros datos, como el número de teléfono, una dirección mail o un nick.
- uno de los contactos no contiene el nombre pero si comparte el número de teléfono, el mail o el nick. Por ejemplo, "Bob Parr[incredible@android.com]=incredible@android.com.
A la hora de comparar nombres, Android ignora las diferencias por mayúsculas y minúsculas (Bob=BOB_bob) y los símbolos diacríticos (Hélène=Helene). Si el sistema está comparando dos números de teléfonos, ignora los caracteres especiales como "*", "#","(",")", y espacios. En el caso de que uno de los números lleve los dígitos del país y el otro no, sí se establece la correspondencia (excepto para los números de Japón).
Hay que tener en cuenta que la agregación automática no es permanente, y cualquier cambio que sufra un contacto en bruto puede crear un nuevo agregado o romper el existente.
Agregación explícita.
Si deseamos controlar nosotros el sistema de agregación, para poder añadir nuevas características, existen modos de agregación que nos permitirán controlar el comportamiento o mediante una excepción para sobrescribir enteramente la agregación automática.
Modos de agregación.
Hay que añadir una constante de tipo modo en la fila del RawContact:
- AGGREGATION_MODE_DEFAULT — modo normal, la agregación automática es permitida.
- AGGREGATION_MODE_DISABLED — la agregación automática no es permitida. El contacto bruto no será agregado.
- AGGREGATION_MODE_SUSPENDED — la agregación automática está desactivada. Si el contacto bruto es parte de un contacto agregado cuando el modo cambia, los cambios serán suspendidos.
Excepciones de agregación.
Para almacenar dos contactos brutos incondicionalmente juntos o incondicionalmente separados, hay que añadir el contacto en bruto a tratar a la tabla ContactsContract.AggregationExceptions. Las excepciones definidas en la tabla sobrescriben todas las reglas de agregación automática.
Búsquedas URI.
En la nueva API podemos realizar búsquedas a través de claves para un contacto. Si la aplicación necesita mantener referencia a los contactos, deberías usar claves de búsquedas dentro de los ids de las filas.
Uri lookupUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey)
y se usa como un content URI tradicional, por ejemplo:
Cursor c = getContentResolver().query(lookupUri, new String[]{Contacts.DISPLAY_NAME}, ...); try { c.moveToFirst(); String displayName = c.getString(0); } finally { c.close();
Puede parecer algo complejo, pero es necesario, ya que los ids de las filas de contactos son inestables. Es decir, nuestra aplicación puede almacenar un id de un contacto. Si el usuario manualmente une el contacto con otro contacto, ahora el long del contacto almacenado no apunta a ninguna parte.
La clave de búsqueda ayuda a resolver el contacto en este caso. La clave es un string que concatena la identidad del contacto en bruto. La aplicación puede usar esta clave para localizar el contacto, independientemente de si se ha agregado a otros o no.
Si deseamos realizar búsquedas rápidas y eficaces debemos utilizar los dos:
Uri lookupUri = getLookupUri(contactId, lookupKey)
Cuando ambos IDs están presentes en el URI, el sistema intentará usar el id primero. Es una query muy rápida. Si el contacto no es encontrado, o si el que ha encontrado tiene una clave de búsqueda errónea, el content provider analizará la clave de búsqueda y localizará a los constituyentes raw contacts.
Gestionando los contactos.
Para gestionar los contactos debemos utilizar un content provider. Un Content provider es el mecanismo que proporciona Android para manejar el acceso de datos que no son propios de nuestra aplicación. Es decir, para poder compartir la información que una aplicación distinta a la nuestra almacena. Para ello, un content provider debe mostrar los métodos query, update, delete, insert y getType.
Vamos a ver cómo podemos recuperar todos los contactos del teléfono siempre que tengan grabado el número de teléfono.
Uri URI = ContactsContract.Data.CONTENT_URI; String where = Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'"; ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE + "'"; Cursor cursor = mContext.getContentResolver().query(URI, null, where, null, null);
Para ello, necesitamos obtener un ContentResolver. Ahora ya podemos llamar al método query.
final Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
Los parámetros que podemos pasarle son:
- URI: la URI. Una URI no es más que una cadena de texto que va a definir de forma inequívoca un recurso, como pueden ser las direcciones web. Podemos encontrar en la clase Data su CONTENT_URI. Su contenido es "content://com.android.contacts/data". Igualmente, cada tabla contiene una constante con su URI.
- projection: es un array de string en la que le indicamos las columnas que deseamos que se devuelvan. Si le pasamos null obtendremos todos. Podemos visualizar todos los campos de las clases Data y CommonDataKinds.
String[] projection = new String[]{ ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.CommonDataKinds.Phone.NUMBER, };
Le estamos diciendo que queremos que nos devuelva el _id del contacto, su nombre y su número de teléfono.
Tenemos un gran número de campos que vamos a poder obtener, como los relacionados con el email, con el grupo, el nick, notas, organización, foto, etc.
- selection: nos permite realizar un where SQL, sin incluir la palabra WHERE.
- selectionArgs: los argumentos que tuviésemos que pasar a la where.
- sortOrder: los campos por los que deseamos que se ordenen los resultados.
Vamos a ver cómo podemos añadir un contacto. Como ya sabemos, no añadimos contactos directamente, sino el contacto en bruto, y es el sistema el que decide si crear un contacto nuevo o agregarle. La columna CONTACT_ID del raw contact contiene el _ID del contacto agregado.
La forma que recomienda Android de añadir un contacto es a través de un proceso por lotes.
ContentProviderResult[] resultado = null; ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Felipe Hernandez") .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "687451225") .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, Phone.TYPE_MOBILE) .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Email.DATA, "felipe.hernandez@gmail.com") .withValue(ContactsContract.CommonDataKinds.Email.TYPE, Email.TYPE_WORK) .build()); try { resultado = getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); } catch (Exception e) { Context ctx = getApplicationContext(); CharSequence txt = "Fallo la creación del contacto"; int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(ctx, txt, duration); toast.show(); Log.e(Constantes.TAG, "Exceptoin encoutered while inserting contact: " + e); }
Veamos el código detalladamente. Tenemos que llenar un array de objetos ContentProviderOperation con toda la información del contacto.
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build());
Un ContentProviderOperation va a estar compuesto de una URI junto con los valores que queremos asignar a esa URI. En la URI le vamos a indicar en qué tabla queremos insertar esos valores. Por ejemplo, ContactsContract.RawContacts.CONTENT_URI, le estamos diciendo que vamos a pasarle valores para las columnas de la tabla content://com.android.contacts/raw_contacts. Para pasar los valores llamamos al método withValue al que le pasamos un objeto ContentValues con un formato de clave/valor. En un contacto en bruto, tenemos que indicarle el tipo de cuenta y el nombre de cuenta (Gmail, Exchange, Facebook, Skype, etc). Estas cuentas se gestionan a través de la clase AccountManager y nos permiten sincronizar nuestros contactos con el tipo de cuenta que le hemos indicado. Si no deseamos que nuestro contacto se sincronice con ninguna cuenta le pasaremos el valor null. Finalmente creamos el ContentProviderOperation con el método build().
Ahora debemos seguir creando objetos ContentProviderOperation con los datos del contacto, los que se van a almacenar en la tabla Data. Vamos a ver como podemos asignarle el nombre.
ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Felipe Hernandez") .build());
Le indicamos la URI, en este caso ContactsContract.Data.CONTENT_URI. Necesitamos indicar que todos los valores Data que vamos a pasar tienen que pertenecer al contacto en bruto. Por ello, hacemos referencia al id del contacto en bruto que se va a crear con el método withValueBackReference(String key, int previousResult).
Esta tabla, se compone de varios campos, que determinan su contenido en función del tipo MIME que le indiquemos. El primer valor que vamos a añadir a nuestro ContentValue es el tipo de MIME. Para el nombre vamos a utilizar la clase StructuredName: ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE. Vemos que el valor de la constante es vnd.android.cursor.item/name". De esta forma, Android sabe que el valor que le vamos a pasar es cualquier columna del tipo nombre. La segunda fila que añadimos es el nombre. En la clave le tenemos que decir en qué campo vamos a almacenarlo (DISPLAY_NAME).
Con esta misma estructura añadiremos el resto de los campos que necesitemos, número de teléfono, email, notas, etc.
Para ejecutar el proceso en lotes tenemos el método applyBatch de nuestro content provider. Este método nos devuelve un array de objetos ContentProviderResult[] donde nos indica la URI con el identificador creado para cada uno de los datos insertados.
resultado = getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Vamos a comprobar el contact_id y el raw_contact_id que se ha creado.
public ArrayList<String> obtenerIdContactoEIdContactoEnBruto(String nombre){ ArrayList<String> idsRawContacts = new ArrayList<String>(); Cursor cursor = null; Uri URI = ContactsContract.Data.CONTENT_URI; try{ cursor = mContext.getContentResolver().query( URI, new String[]{Data.RAW_CONTACT_ID, Data.CONTACT_ID, Data.DISPLAY_NAME}, Data.DISPLAY_NAME + " LIKE '%" + nombre + "%'", null, null); }catch (Exception e){ Log.i(Constantes.TAG, e.getMessage()); } if (cursor.moveToFirst()){ while (cursor.moveToNext()){ idsRawContacts.add(cursor.getString(0) + " " + cursor.getString(1) + " " + cursor.getString(2)); } } return idsRawContacts;
En este caso se ha creado para ambos ids el número 16.
Si ahora añadimos un contacto con el mismo nombre, pero otro número de teléfono, debería de crearse un nuevo rawcontact_id y con el id de contacto 16, ya que el sistema debería de agregar este contacto en bruto al contacto que ya existe y tiene correspondencia.
ContentProviderResult[] resultado = null; ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI) .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null) .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null) .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Felipe Hernandez") .build()); ops.add(ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI) .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0) .withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, "979852142") .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, Phone.TYPE_HOME) .build()); try { resultado = getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops); } catch (Exception e) { Context ctx = getApplicationContext(); CharSequence txt = "Fallo la creación del contacto"; int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(ctx, txt, duration); toast.show(); Log.e(Constantes.TAG, "Exceptoin encoutered while inserting contact: " + e); }
Si ejecutáis el método que nos devuelve los ids, veréis que en efecto, tenemos el id_contact 16 con dos raw_contacs el 16 y el 17.
La actualización y la eliminación son procesos muy similares. En ambos, como en la insercción, seguimos utilizando el proceso por lotes.
A la hora de actualizar utilizaremos el método newUpdate de la clase ContentProviderOperation, donde le indicamos la URI que deseamos modificar. Después, debemos indicarle el where con el que filtraremos los valores a actualizar. En este caso, vamos a modificar el id de la tabla Data que contiene el número de teléfono que deseamos modificar. Finalmente, con el método withValue le decimos el nombre del campo a modificar y el valor nuevo.
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newUpdate(Data.CONTENT_URI) .withSelection(Data._ID + "=?", new String[]{String.valueOf(dataId)}) .withValue(Phone.NUMBER, "888777888") .build()); resultado = mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
Podemos eliminar tanto datos como contactos en bruto como el contacto. Si eliminamos un contacto en bruto también se elimina en cascada todos los datos, excepciones de agregación, claves de búsqueda, etc. Si eliminamos el contacto, eliminamos todos los contactos en bruto y sus datos.
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); ops.add(ContentProviderOperation.newDelete(RawContacts.CONTENT_URI) .withSelection(RawContacts._ID + "=?", new String[]{String.valueOf(dataId)}) .build()); mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
En este caso, vamos e eliminar el segundo contacto en bruto que añadimos. Utilizamos el método newDelete para indicar la acción que deseamos llevar a cabo y le pasamos el where.
Reader Comments (2)
Muy buena la explicación. Te felicito
Muy buena tu explicacion gracias....