Proveedor de contactos

El proveedor de contactos es un componente de Android potente y flexible que administra el repositorio central de datos de personas en dispositivos Android. El proveedor de contactos es la fuente de datos que ves en la aplicación de contactos del dispositivo, y también puedes acceder a sus datos en tu propia aplicación y transferir datos entre el dispositivo y servicios en línea. El proveedor considera una gran variedad de fuentes de datos e intenta administrar la mayor cantidad de datos posible para cada persona. Como resultado, su organización es compleja. Por este motivo, la API del proveedor incluye un amplio conjunto de clases de contacto e interfaces que facilitan la recuperación y la modificación de datos.

Esta guía describe lo siguiente:

  • Estructura de proveedor básica.
  • Cómo recuperar datos del proveedor.
  • Cómo modificar datos en el proveedor.
  • Cómo escribir un adaptador de sincronización para sincronizar datos de tu servidor con el proveedor de contactos.

Esta guía presupone que conoces los aspectos básicos de los proveedores de contenido de Android. Para obtener más información acerca de los proveedores de contenido de Android, lee la guía Conceptos básicos sobre el proveedor de contenido.

Organización del proveedor de contactos

El proveedor de contactos es un componente del proveedor de contenido de Android que mantiene tres tipos de datos acerca de una persona; cada uno de esos tipos de datos se corresponde con una tabla que ofrece el proveedor, como se ilustra en la figura 1:

Figura 1: Estructura de las tablas del proveedor de contactos

Generalmente, las tres tablas se denominan con los nombres de sus clases de contrato. Las clases definen constantes para los URI de contenido, los nombres de las columnas y los valores de las columnas que usan las tablas:

Tabla ContactsContract.Contacts
Las filas representan diferentes personas en función de la adición de filas de contactos sin procesar.
Tabla ContactsContract.RawContacts
Filas que contienen un resumen de los datos de una persona, específicas para un tipo de usuario y una cuenta de usuario.
Tabla ContactsContract.Data
Filas que contienen los detalles de un contacto sin procesar, como direcciones de correo electrónico o números de teléfono.

Las demás tablas representadas por las clases de contrato en ContactsContract son tablas auxiliares que el proveedor de contactos usa para administrar sus operaciones o respaldar funciones específicas en las aplicaciones de telefonía o de contactos del dispositivo.

Contactos sin procesar

Un contacto sin procesar representa los datos de una persona que provienen de un solo tipo de cuenta y nombre de cuenta. Como el proveedor de contactos admite más de un servicio en línea como origen de datos para una persona, el proveedor de contactos permite usar múltiples contactos sin procesar para la misma persona. El uso de múltiples contactos sin procesar también le permite al usuario usar el mismo tipo de cuenta para combinar los datos de una persona provenientes de más de una cuenta.

La mayoría de los datos de un contacto sin procesar no se guardan en la tabla ContactsContract.RawContacts. En su lugar, se guardan en una o más filas de la tabla ContactsContract.Data. Cada fila de datos tiene una columna Data.RAW_CONTACT_ID que contiene el valor RawContacts._ID de su fila principal ContactsContract.RawContacts.

Columnas importantes de contactos sin procesar

Las columnas importantes de la tabla ContactsContract.RawContacts se indican en la tabla 1. Lee las notas que aparecen después de la tabla:

Tabla 1: Columnas importantes de contactos sin procesar

Nombre de la columna Uso Notas
ACCOUNT_NAME Nombre de cuenta del tipo de cuenta que es el origen de ese contacto sin procesar. Por ejemplo, el nombre de una cuenta de Google es una de las direcciones de Gmail del dueño del dispositivo. Consulta la próxima entrada para obtener más información acerca de ACCOUNT_TYPE. El formato de este nombre es específico para su tipo de cuenta. No es necesariamente una dirección de correo electrónico.
ACCOUNT_TYPE El tipo de cuenta que es el origen del contacto sin procesar. Por ejemplo, el tipo de una cuenta de Google es com.google. Siempre debes calificar tu tipo de cuenta con un identificador de dominio para un dominio que tú poseas o controles. Esto asegura que tu tipo de cuenta sea único. Un tipo de cuenta que ofrece datos de contactos, generalmente, tiene un adaptador de sincronización asociado que se sincroniza con el proveedor de contactos.
DELETED El marcador "eliminado" para un contacto sin procesar. Este marcador permite que el proveedor de contactos mantenga la fila internamente hasta que los adaptadores de sincronización puedan borrarla de sus servidores y, finalmente, del repositorio.

Notas

Las siguientes son notas importantes acerca de la tabla ContactsContract.RawContacts:

  • El nombre de un contacto sin procesar no se guarda en su fila en ContactsContract.RawContacts. En su lugar, se guarda en la tabla ContactsContract.Data, en una fila ContactsContract.CommonDataKinds.StructuredName. Un contacto sin procesar tiene una sola fila de este tipo en la tabla ContactsContract.Data.
  • Advertencia: Para usar los datos de tu cuenta en una fila de contacto sin procesar, primero debe estar registrada con el AccountManager. Para poder hacerlo, pídeles a los usuarios que agreguen el tipo y el nombre de cuenta a la lista de cuentas. Si no haces esto, el proveedor de contactos eliminará automáticamente la fila del contacto sin procesar.

    Por ejemplo, si deseas que tu aplicación conserve datos de contactos para tu servicio basado en la Web con el dominio com.example.dataservice, y la cuenta del usuario para tu servicio es becky.sharp@dataservice.example.com, el usuario debe agregar primero el "tipo" (com.example.dataservice) y el "nombre" (becky.smart@dataservice.example.com) de la cuenta para que tu aplicación pueda agregar filas de contactos sin procesar. Puedes explicarle este requisito al usuario a través de la documentación o puedes solicitarle que agregue el tipo y el nombre, o ambas opciones. Los tipos y los nombres de cuentas se describen más detalladamente en la próxima sección.

Orígenes de los datos de contactos sin procesar

Para comprender cómo funcionan los contactos sin procesar, usemos como ejemplo a la usuaria "Emily Dickinson", quien tiene las siguientes tres cuentas de usuario definidas en su dispositivo:

  • emily.dickinson@gmail.com
  • emilyd@gmail.com
  • Cuenta de Twitter: "belle_of_amherst"

Esta usuaria habilitó la Sincronización de contactos para las tres cuentas en la configuración Cuentas.

Supón que Emily Dickinson abre una ventana del explorador, accede a Gmail como emily.dickinson@gmail.com, abre "Contactos" y agrega a "Thomas Higginson". Más adelante, accede a Gmail como emilyd@gmail.com y le envía un correo electrónico a "Thomas Higginson"; esta acción lo agrega automáticamente como contacto. También sigue en Twitter a "colonel_tom" (ID de Thomas Higginson en Twitter).

Como resultado de estas acciones, el proveedor de contactos crea tres contactos sin procesar:

  1. Un contacto sin procesar para "Thomas Higginson" asociado con emily.dickinson@gmail.com. El tipo de cuenta del usuario es Google.
  2. Un segundo contacto sin procesar para "Thomas Higginson" asociado con emilyd@gmail.com. El tipo de cuenta de usuario también es Google. Hay un segundo contacto sin procesar, aunque el nombre sea idéntico al nombre anterior, ya que la persona se agregó en un tipo de cuenta diferente.
  3. Un tercer contacto sin procesar para "Thomas Higginson" asociado con "belle_of_amherst". El tipo de cuenta de usuario es Twitter.

Datos

Como se indicó más arriba, los datos para un contacto sin procesar se guardan en una fila ContactsContract.Data vinculada al valor _ID del contacto sin procesar. Esto permite que un contacto sin procesar tenga múltiples instancias del mismo tipo de datos, como direcciones de correo electrónico o números de teléfono. Por ejemplo, si "Thomas Higginson" para emilyd@gmail.com (la fila de contacto sin procesar de Thomas Higginson asociada con la cuenta de Google emilyd@gmail.com) tiene una dirección de correo electrónico particular thigg@gmail.com y una dirección de correo electrónico de trabajo thomas.higginson@gmail.com, el proveedor de contactos guarda las dos filas de direcciones de correo electrónico y las vincula al contacto sin procesar.

Ten en cuenta que en esta tabla se guardan diferentes tipos de datos. La tabla ContactsContract.Data incluye las filas nombre para mostrar, número de teléfono, correo electrónico, dirección postal, foto y detalles del sitio web. Para ayudar a administrar esto, la tabla ContactsContract.Data tiene algunas columnas con nombres descriptivos y otras con nombres genéricos. Los contenidos de una columna con nombre descriptivo tienen el mismo significado independientemente del tipo de datos de la fila, mientras que los contenidos de una columna con nombre genérico tienen significados diferentes según el tipo de datos.

Columnas con nombre descriptivo

Algunos ejemplos de columnas con nombre descriptivo:

RAW_CONTACT_ID
El valor de la columna _ID del contacto sin procesar para estos datos.
MIMETYPE
El tipo de datos que se guarda en esta fila, expresado como un tipo de MIME personalizado. El proveedor de contactos usa los tipos de MIME definidos en las subclases de ContactsContract.CommonDataKinds. Esos tipos de MIME son de código abierto, y los puede usar cualquier aplicación o adaptador de sincronización que trabaje con el proveedor de contactos.
IS_PRIMARY
Si este tipo de fila de datos puede repetirse para un contacto sin procesar, la columna IS_PRIMARY marca la fila de datos que contiene los datos primarios para ese tipo. Por ejemplo, si el usuario presiona un número de teléfono para un contacto y selecciona Establecer predeterminado, la fila ContactsContract.Data que contiene ese número tiene su propia columna IS_PRIMARY configurada en un valor que no es cero.

Columnas con nombre genérico

Hay 15 columnas genéricas denominadas DATA1 en DATA15 que generalmente están disponibles y cuatro columnas genéricas adicionales, de SYNC1 a SYNC4, que solo los adaptadores de sincronización pueden usar. Las constantes de las columnas con nombre genérico funcionan siempre, independientemente del tipo de datos que contenga la fila.

La columna DATA1 está indexada. El proveedor de contactos siempre usa esta columna para los datos que prevé que serán los más frecuentes en una consulta. Por ejemplo, en una fila de correo electrónico, esta columna contiene la dirección de correo electrónico misma.

Por convención, la columna DATA15 se reserva para guardar datos del objeto binario grande (BLOB), como miniaturas de fotos.

Nombres de columnas específicos para los diferentes tipos

Para facilitar el trabajo con las columnas para un tipo de fila específico, el proveedor de contactos también proporciona constantes para nombre de columna de tipo específico, que se definen en las subclases de ContactsContract.CommonDataKinds. Las constantes simplemente proporcionan un nombre de constante diferente para el mismo nombre de columna, lo cual te ayuda a acceder a los datos de una fila de un tipo determinado.

Por ejemplo, la clase ContactsContract.CommonDataKinds.Email define las constantes del nombre de columna específico para una fila ContactsContract.Data que tiene el tipo de MIME Email.CONTENT_ITEM_TYPE. La clase contiene la constante ADDRESS para la columna de dirección de correo electrónico. El valor real de ADDRESS es "data1", que coincide con el nombre genérico de la columna.

Advertencia: No agregues tus propios datos personalizados a la tabla ContactsContract.Data usando una fila que tenga uno de los tipos de MIME predefinidos por el proveedor. Si lo haces, podrías perder los datos o provocar que el proveedor no funcione correctamente. Por ejemplo, no debes agregar una fila con el tipo de MIME Email.CONTENT_ITEM_TYPE que contiene un nombre de usuario en lugar de una dirección de correo electrónico en la columna DATA1. Si usas tu propio tipo de MIME personalizado para la fila, puedes definir los nombres de columna específicos para tipos de fila que elijas y usar las columnas de la forma que quieras.

La figura 2 describe cómo aparecen las columnas descriptivas y las columnas de datos en una fila ContactsContract.Data y cómo los nombres de las columnas para tipos específicos de fila "superponen" los nombres de las columnas genéricas.

Manera en que los nombres de columnas de tipo específico se asignan a nombres de columnas genéricos

Figura 2: Nombres de columnas de tipo específico y nombres de columnas genéricos

Clases de nombres de columnas de tipo específico

La tabla 2 indica las clases de nombres de columnas de tipo específico que se usan con más frecuencia:

Tabla 2: Clases de nombres de columnas de tipo específico.

Clase de asignación Tipo de dato Notas
ContactsContract.CommonDataKinds.StructuredName Los datos del nombre para el contacto sin procesar asociado con esta fila de datos. Un contacto sin procesar tiene una sola fila de este tipo.
ContactsContract.CommonDataKinds.Photo La foto principal del contacto sin procesar asociado con esta fila de datos. Un contacto sin procesar tiene una sola fila de este tipo.
ContactsContract.CommonDataKinds.Email Una dirección de correo electrónico del contacto sin procesar asociado con esta fila de datos. Un contacto sin procesar puede tener varias direcciones de correo electrónico.
ContactsContract.CommonDataKinds.StructuredPostal Una dirección postal del contacto sin procesar asociado con esta fila de datos. Un contacto sin procesar puede tener varias direcciones postales.
ContactsContract.CommonDataKinds.GroupMembership Un identificador que vincula al contacto sin procesar con uno de los grupos en el proveedor de contactos. Los grupos son una característica opcional para el tipo y el nombre de cuenta. Se describen más detalladamente en la sección Grupos de contactos.

Contactos

El proveedor de contactos combina las filas de contactos sin procesar en todos los tipos y nombres de cuentas para formar un contacto. Esto facilita la visualización y modificación de todos los datos que recopiló un usuario para una persona. El proveedor de contactos administra la creación de nuevas filas de contacto y la adición de contactos sin procesar a una fila de contacto existente. Ni las aplicaciones ni los adaptadores de sincronización pueden agregar contactos, y algunas columnas en una fila de contacto son de solo lectura.

Nota: Si intentas agregar un contacto al proveedor de contactos con un insert(), obtendrás una excepción UnsupportedOperationException. Si intentas actualizar una columna que se indica como de "solo lectura", se ignora la actualización.

El proveedor de contactos crea un contacto nuevo en respuesta a la adición de un nuevo contacto sin procesar que no coincide con los contactos existentes. El proveedor también hace esto si los datos de un contacto sin procesar existente cambian de modo que ya no coincide con el contacto al que se había adjuntado anteriormente. Si una aplicación o un adaptador de sincronización crea un nuevo contacto sin procesar que coincide con un contacto existente, el nuevo contacto sin procesar se agrega al contacto existente.

El proveedor de contactos vincula una fila de contacto para sus filas de contactos sin procesar con la columna _ID de la fila en la tabla Contacts. La columna CONTACT_ID de la tabla de contactos sin procesar ContactsContract.RawContacts contiene valores _ID para la fila de contactos asociada con cada fila de contactos sin procesar.

La tabla ContactsContract.Contacts también tiene la columna LOOKUP_KEY, que es un enlace "permanente" con la fila de contacto. Como el proveedor de contactos mantiene contactos automáticamente, puede cambiar el valor _ID de una fila de contactos en respuesta a una adición o sincronización. Incluso si esto ocurre, el URI del contenido CONTENT_LOOKUP_URI combinado con la LOOKUP_KEY del contacto aún hará referencia a la fila de contacto de modo que puedas usar LOOKUP_KEY para mantener enlaces con los contactos "favoritos", etc. Esta columna tiene su propio formato, que no está relacionado con el formato de la columna _ID.

En la figura 3, se ve cómo las tres tablas principales se relacionan entre sí.

Tablas principales del proveedor de contactos

Figura 3: Relaciones de la tabla de contactos, contactos sin procesar y detalles

Advertencia: A partir del 7 de enero de 2019, caducará un conjunto limitado de contactos, campos de datos y métodos.

De forma periódica, el sistema borra los valores escritos en estos campos de datos:

También caducan las API usadas para establecer los campos de datos anteriores:

Además, los siguientes campos ya no devuelven contactos frecuentes. Ten presente que algunos de estos campos afectan el posicionamiento de los contactos solamente si estos forman parte de un tipo de datos específico.

Usa métodos alternativos si tus aplicaciones tienen acceso a estos campos o API, o si los actualizan. Por ejemplo, puedes completar algunos casos de uso mediante proveedores de contactos privados u otros datos almacenados en tu aplicación o sistemas de backend.

Para verificar que este cambio no afecte la funcionalidad de la aplicación, puedes borrar estos campos de datos manualmente. Para hacerlo, ejecuta el siguiente comando de ADB en un dispositivo con Android 4.1 (nivel de API 16) o versiones posteriores:

adb shell content delete \
--uri content://com.android.contacts/contacts/delete_usage

Datos de los adaptadores de sincronización

Los usuarios ingresan datos de contacto directamente en el dispositivo, pero los datos también se dirigen al proveedor de contactos desde servicios web a través de adaptadores de sincronización, que automatizan la transferencia de datos entre el dispositivo y los servicios. Los adaptadores de sincronización se ejecutan en segundo plano bajo el control del sistema y llaman a los métodos ContentResolver para administrar los datos.

En Android, el servicio web con el que trabaja un adaptador de sincronización se identifica mediante un tipo de cuenta. Cada adaptador de sincronización trabaja con un tipo de cuenta, pero puede admitir múltiples nombres de cuenta para ese tipo. Los tipos y los nombres de cuentas se describen brevemente en la sección Fuentes de datos de contactos sin procesar. Las siguientes definiciones ofrecen más detalles y describen cómo se relacionan el tipo y el nombre de cuenta con los servicios y los adaptadores de sincronización.

Tipo de cuenta
Identifica un servicio en el que el usuario tiene datos almacenados. Casi siempre, el usuario tiene que autenticarse con el servicio. Por ejemplo, Contactos de Google es un tipo de cuenta identificada con el código google.com. Este valor corresponde al tipo de cuenta que usa el AccountManager.
Nombre de cuenta
Identifica una cuenta específica o información de acceso para un tipo de cuenta. Las cuentas de Contactos de Google son las mismas que las cuentas de Google que tienen una dirección de correo electrónico como nombre de cuenta. Otros servicios pueden usar un nombre de usuario de una sola palabra o un identificador numérico.

No es necesario que los tipos de cuenta sean únicos. Un usuario puede configurar múltiples cuentas de Contactos de Google y descargar sus datos en el proveedor de contactos; esto puede ocurrir si el usuario tiene un grupo de contactos personales para un nombre de cuenta personal y otro grupo para el trabajo. Los nombres de cuenta generalmente son únicos. Juntos, identifican un flujo de datos específico entre el proveedor de contactos y un servicio externo.

Si quieres transferir los datos de tu servicio al proveedor de contactos, debes escribir tu propio adaptador de sincronización. Esto se describe más detalladamente en la sección Adaptadores de sincronización del proveedor de contactos.

En la figura 4, se ve cómo el proveedor de contactos se adapta al flujo de datos de personas. En la casilla denominada "adaptadores de sincronización", cada adaptador está etiquetado con su tipo de cuenta.

Flujo de datos sobre personas

Figura 4: Flujo de datos del proveedor de contactos

Permisos necesarios

Las aplicaciones que quieran acceder al proveedor de contactos deben solicitar los siguientes permisos:

Acceso de lectura a una o más tablas
READ_CONTACTS, especificado en AndroidManifest.xml con el elemento <uses-permission> como <uses-permission android:name="android.permission.READ_CONTACTS">.
Acceso de escritura a una o más tablas
WRITE_CONTACTS, especificado en AndroidManifest.xml con el elemento <uses-permission> como <uses-permission android:name="android.permission.WRITE_CONTACTS">.

Esos permisos no se extienden a los datos del perfil del usuario. El perfil del usuario y sus permisos necesarios se analizan en la siguiente sección, Perfil del usuario.

Recuerda que los datos de contacto del usuario son personales y sensibles. A los usuarios les preocupa su privacidad y no quieren que las aplicaciones recopilen datos acerca de ellos ni de sus contactos. Si no aclaras por qué necesitas permiso para acceder a sus datos de contacto, podrían darle a tu aplicación una calificación baja o simplemente negarse a instalarla.

Perfil del usuario

La tabla ContactsContract.Contacts tiene una sola fila que contiene datos de perfil del usuario del dispositivo. Esos datos describen el user del dispositivo en lugar de uno de los contactos del usuario. La fila de contactos del perfil está vinculada a una fila de contactos sin procesar para cada sistema que usa un perfil. Cada fila de contacto sin procesar del perfil puede tener múltiples filas de datos. Las constantes para acceder al perfil del usuario están disponibles en la clase ContactsContract.Profile.

El acceso al perfil del usuario requiere permisos especiales. Además de los permisos READ_CONTACTS y WRITE_CONTACTS necesarios para lectura y escritura, para acceder al perfil del usuario, se necesitan los permisos de acceso de lectura y escritura android.Manifest.permission#READ_PROFILE y android.Manifest.permission#WRITE_PROFILE, respectivamente.

Recuerda que debes tener en cuenta que los perfiles del usuario son sensibles. El permiso android.Manifest.permission#READ_PROFILE te permite acceder a los datos personales del usuario del dispositivo. Asegúrate de explicarle al usuario por qué necesitas permisos de acceso al perfil del usuario en la descripción de tu aplicación.

Para recuperar la fila de contacto que contiene el perfil del usuario, llama a ContentResolver.query(). Configura el URI de contenido en CONTENT_URI y no proporciones ningún criterio de selección. También puedes usar este URI de contenido como el URI de base para recuperar contactos sin procesar o datos para el perfil. Por ejemplo, este fragmento de código recupera datos para el perfil:

Kotlin

// Sets the columns to retrieve for the user profile
projection = arrayOf(
        ContactsContract.Profile._ID,
        ContactsContract.Profile.DISPLAY_NAME_PRIMARY,
        ContactsContract.Profile.LOOKUP_KEY,
        ContactsContract.Profile.PHOTO_THUMBNAIL_URI
)

// Retrieves the profile from the Contacts Provider
profileCursor = contentResolver.query(
        ContactsContract.Profile.CONTENT_URI,
        projection,
        null,
        null,
        null
)

Java

// Sets the columns to retrieve for the user profile
projection = new String[]
    {
        Profile._ID,
        Profile.DISPLAY_NAME_PRIMARY,
        Profile.LOOKUP_KEY,
        Profile.PHOTO_THUMBNAIL_URI
    };

// Retrieves the profile from the Contacts Provider
profileCursor =
        getContentResolver().query(
                Profile.CONTENT_URI,
                projection ,
                null,
                null,
                null);

Nota: Si recuperas múltiples filas de contacto y quieres determinar si una de ellas es el perfil del usuario, prueba la columna IS_USER_PROFILE de la fila. Si el contacto es el perfil del usuario, esta columna tiene un valor de "1".

Metadatos del proveedor de contactos

El proveedor de contactos administra datos que realizan un seguimiento del estado de los datos de los contactos en el repositorio. Esos metadatos acerca del repositorio se guardan en varios lugares, incluidas las filas "Contactos sin procesar", "Datos" y "Tabla de contactos"; la tabla ContactsContract.Settings; y la tabla ContactsContract.SyncState. La siguiente tabla muestra el efecto de cada una de esas porciones de metadatos:

Tabla 3: Metadatos del proveedor de contactos

Tabla Columna Valores Significado
ContactsContract.RawContacts DIRTY "0": no se modificó desde la última sincronización. Indica los contactos sin procesar que se modificaron en el dispositivo y deben volver a sincronizarse con el servidor. El proveedor de contactos establece automáticamente el valor cuando las aplicaciones de Android actualizan una fila.

Los adaptadores de sincronización que modifican las tablas de datos o de contactos sin procesar siempre deben anexar la cadena CALLER_IS_SYNCADAPTER al URI de contenido que usan. Esto evita que el proveedor marque filas como sucias. De lo contrario, las modificaciones del adaptador de sincronización parecen modificaciones locales y se envían al servidor, aunque el servidor haya sido el origen de la modificación.

"1": se modificó desde la última sincronización y debe volver a sincronizarse con el servidor.
ContactsContract.RawContacts VERSION El número de versión de esta fila. El proveedor de contactos aumenta automáticamente este valor siempre que la fila o los datos relacionados con ella cambian.
ContactsContract.Data DATA_VERSION El número de versión de esta fila. El proveedor de contactos aumenta automáticamente este valor siempre que se modifica la fila de datos.
ContactsContract.RawContacts SOURCE_ID Valor de string que identifica este contacto sin procesar de forma exclusiva para la cuenta en la que se creó. Cuando un adaptador de sincronización crea un contacto sin procesar nuevo, esta columna se debe configurar con el ID único del contacto sin procesar en el servidor. Cuando una aplicación de Android crea un contacto sin procesar nuevo, la aplicación debe dejar esta columna vacía. Esto le indica al adaptador de sincronización que debe crear un contacto sin procesar nuevo en el servidor y obtener un valor para SOURCE_ID.

Específicamente, el ID de origen debe ser único para cada tipo de cuenta y debe ser estable entre sincronizaciones:

  • Único: Cada contacto sin procesar para una cuenta debe tener su propio ID de origen. Si no implementas esto, crearás problemas en la aplicación de contactos. Ten en cuenta que dos contactos sin procesar para el mismo tipo de cuenta pueden tener el mismo ID de origen. Por ejemplo, el contacto sin procesar "Thomas Higginson" para la cuenta emily.dickinson@gmail.com puede tener el mismo ID de origen que el contacto sin procesar "Thomas Higginson" para la cuenta emilyd@gmail.com.
  • Estable: Los ID de origen son una parte permanente de los datos del servicio en línea para el contacto sin procesar. Por ejemplo, si el usuario limpia el almacenamiento de contactos desde la configuración de la aplicación y realiza una nueva sincronización, los contactos sin procesar restaurados deben tener los mismos ID de origen que antes. Si no implementas esto, los accesos directos dejarán de funcionar.
ContactsContract.Groups GROUP_VISIBLE "0": los contactos en este grupo no deben estar visibles en las IU de la aplicación de Android. Esta columna corresponde a la compatibilidad con servidores que le permiten al usuario ocultar los contactos de ciertos grupos.
"1": los contactos en este grupo pueden ser visibles en las IU de la aplicación de Android.
ContactsContract.Settings UNGROUPED_VISIBLE "0": para esta cuenta y este tipo de cuenta, los contactos que no pertenecen a un grupo son invisibles para las IU de la aplicación de Android. De forma predeterminada, los contactos son invisibles si ninguno de sus contactos sin procesar pertenece a un grupo (la membresía a un grupo de un contacto sin procesar se indica mediante una o más filas ContactsContract.CommonDataKinds.GroupMembership en la tabla ContactsContract.Data). Al configurar este marcador en la fila ContactsContract.Settings de la tabla para una cuenta y un tipo de cuenta, puedes forzar la visibilidad de los contactos sin grupos. Uno de los usos de este marcador es mostrar contactos de servidores que no usan grupos.
"1": para esta cuenta y este tipo de cuenta, los contactos que no pertenecen a un grupo son visibles para las IU de la aplicación.
ContactsContract.SyncState (todo) Usa esta tabla para guardar metadatos de tu adaptador de sincronización. Con esta tabla, puedes guardar el estado de la sincronización y otros datos relacionados con la sincronización de manera permanente en el dispositivo.

Acceso al proveedor de contactos

En esta sección, se describen pautas para acceder a datos del proveedor de contactos, específicamente lo siguiente:

  • Consultas a entidades.
  • Modificación por lotes.
  • Recuperación y modificación con intents.
  • Integridad de los datos.

La realización de modificaciones a partir de un adaptador de sincronización también se cubre más detalladamente en la sección Adaptadores de sincronización del proveedor de contactos.

Para saber cómo usar el proveedor de contactos durante la búsqueda de contactos, consulta el ejemplo Funciones "Contactable" básicas.

Consulta de entidades

El hecho de que las tablas del proveedor de contactos estén organizadas en una jerarquía facilita la recuperación de una fila y todas las filas "secundarias" vinculadas a ella. Por ejemplo, para mostrar toda la información de una persona, te recomendamos que recuperes todas las filas ContactsContract.RawContacts de una fila ContactsContract.Contacts individual o todas las filas ContactsContract.CommonDataKinds.Email de una fila ContactsContract.RawContacts individual. Para facilitar esto, el proveedor de contactos ofrece construcciones de entidad que actúan como uniones de la base de datos entre tablas.

Una entidad es como una tabla compuesta de columnas específicas de una tabla principal y su tabla secundaria. Cuando consultas una entidad, proporcionas una proyección y criterios de búsqueda en función de las columnas disponibles en la entidad. El resultado es un Cursor que contiene una fila para cada fila recuperada de la tabla secundaria. Por ejemplo, si consultas ContactsContract.Contacts.Entity de un nombre de contacto y todas las filas de ContactsContract.CommonDataKinds.Email de todos los contactos sin procesar relacionados con ese nombre, obtendrás un Cursor con una fila para cada fila de ContactsContract.CommonDataKinds.Email.

Las entidades simplifican las consultas. Al usar una entidad, puedes recuperar todos los datos de contacto de un contacto o contacto sin procesar a la vez, en lugar de tener que consultar primero la tabla principal para obtener un ID y, luego, la tabla secundaria con ese ID. El proveedor de contactos también procesa una consulta a una entidad en una sola transacción, lo cual garantiza que los datos recuperados sean coherentes a nivel interno.

Nota: Una entidad generalmente no contiene todas las columnas de la tabla principal y la tabla secundaria. Si intentas trabajar con un nombre de columna que no está en la lista de constantes de nombres de columna de la entidad, obtendrás una Exception.

El siguiente fragmento de código muestra cómo recuperar todas las filas de contacto sin procesar de un contacto. El fragmento de código es una parte de una aplicación más grande que tiene dos actividades: "principal" y "detalle". La actividad principal muestra una lista de filas de contacto; cuando el usuario selecciona una, la actividad envía su ID a la actividad de "detalle". La actividad de detalle usa la ContactsContract.Contacts.Entity para mostrar todas las filas de datos de todos los contactos sin procesar asociados con el contacto seleccionado.

Este fragmento de código pertenece a la actividad de "detalle":

Kotlin

...
    /*
     * Appends the entity path to the URI. In the case of the Contacts Provider, the
     * expected URI is content://com.google.contacts/#/entity (# is the ID value).
     */
    contactUri = Uri.withAppendedPath(
            contactUri,
            ContactsContract.Contacts.Entity.CONTENT_DIRECTORY
    )

    // Initializes the loader identified by LOADER_ID.
    loaderManager.initLoader(
            LOADER_ID,  // The identifier of the loader to initialize
            null,       // Arguments for the loader (in this case, none)
            this        // The context of the activity
    )

    // Creates a new cursor adapter to attach to the list view
    cursorAdapter = SimpleCursorAdapter(
            this,                       // the context of the activity
            R.layout.detail_list_item,  // the view item containing the detail widgets
            mCursor,                    // the backing cursor
            fromColumns,               // the columns in the cursor that provide the data
            toViews,                   // the views in the view item that display the data
            0)                          // flags

    // Sets the ListView's backing adapter.
    rawContactList.adapter = cursorAdapter
...
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
    /*
     * Sets the columns to retrieve.
     * RAW_CONTACT_ID is included to identify the raw contact associated with the data row.
     * DATA1 contains the first column in the data row (usually the most important one).
     * MIMETYPE indicates the type of data in the data row.
     */
    val projection: Array<String> = arrayOf(
            ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
            ContactsContract.Contacts.Entity.DATA1,
            ContactsContract.Contacts.Entity.MIMETYPE
    )

    /*
     * Sorts the retrieved cursor by raw contact id, to keep all data rows for a single raw
     * contact collated together.
     */
    val sortOrder = "${ContactsContract.Contacts.Entity.RAW_CONTACT_ID} ASC"

    /*
     * Returns a new CursorLoader. The arguments are similar to
     * ContentResolver.query(), except for the Context argument, which supplies the location of
     * the ContentResolver to use.
     */
    return CursorLoader(
            applicationContext, // The activity's context
            contactUri,        // The entity content URI for a single contact
            projection,         // The columns to retrieve
            null,               // Retrieve all the raw contacts and their data rows.
            null,               //
            sortOrder           // Sort by the raw contact ID.
    )
}

Java

...
    /*
     * Appends the entity path to the URI. In the case of the Contacts Provider, the
     * expected URI is content://com.google.contacts/#/entity (# is the ID value).
     */
    contactUri = Uri.withAppendedPath(
            contactUri,
            ContactsContract.Contacts.Entity.CONTENT_DIRECTORY);

    // Initializes the loader identified by LOADER_ID.
    getLoaderManager().initLoader(
            LOADER_ID,  // The identifier of the loader to initialize
            null,       // Arguments for the loader (in this case, none)
            this);      // The context of the activity

    // Creates a new cursor adapter to attach to the list view
    cursorAdapter = new SimpleCursorAdapter(
            this,                        // the context of the activity
            R.layout.detail_list_item,   // the view item containing the detail widgets
            mCursor,                     // the backing cursor
            fromColumns,                // the columns in the cursor that provide the data
            toViews,                    // the views in the view item that display the data
            0);                          // flags

    // Sets the ListView's backing adapter.
    rawContactList.setAdapter(cursorAdapter);
...
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {

    /*
     * Sets the columns to retrieve.
     * RAW_CONTACT_ID is included to identify the raw contact associated with the data row.
     * DATA1 contains the first column in the data row (usually the most important one).
     * MIMETYPE indicates the type of data in the data row.
     */
    String[] projection =
        {
            ContactsContract.Contacts.Entity.RAW_CONTACT_ID,
            ContactsContract.Contacts.Entity.DATA1,
            ContactsContract.Contacts.Entity.MIMETYPE
        };

    /*
     * Sorts the retrieved cursor by raw contact id, to keep all data rows for a single raw
     * contact collated together.
     */
    String sortOrder =
            ContactsContract.Contacts.Entity.RAW_CONTACT_ID +
            " ASC";

    /*
     * Returns a new CursorLoader. The arguments are similar to
     * ContentResolver.query(), except for the Context argument, which supplies the location of
     * the ContentResolver to use.
     */
    return new CursorLoader(
            getApplicationContext(),  // The activity's context
            contactUri,              // The entity content URI for a single contact
            projection,               // The columns to retrieve
            null,                     // Retrieve all the raw contacts and their data rows.
            null,                     //
            sortOrder);               // Sort by the raw contact ID.
}

Cuando termina la carga, LoaderManager invoca un método de devolución de llamada para onLoadFinished(). Uno de los argumentos entrantes de este método es un Cursor con los resultados de la consulta. En tu propia aplicación, puedes obtener los de este Cursor para mostrarlos o seguir trabajando con ellos.

Modificación por lotes

Siempre que sea posible, debes insertar, actualizar y eliminar datos que se encuentren en el proveedor de contactos en "modo por lotes" creando una ArrayList de objetos ContentProviderOperation y llamando a applyBatch(). Como el proveedor de contactos realiza todas las operaciones de un objeto applyBatch() en una sola transacción, tus modificaciones nunca saldrán del repositorio de contactos en un estado que no sea uniforme. Una modificación por lotes también facilita la inserción de un contacto sin procesar y sus datos detallados al mismo tiempo.

Nota: Para modificar un contacto sin procesar individual, considera la posibilidad de enviar una intent a la aplicación de contactos del dispositivo en lugar de manipular la modificación en tu aplicación. Puedes encontrar información detallada acerca de cómo hacer esto en la sección Recuperación y modificación con intents.

Puntos de productividad

Una modificación por lotes que contenga una gran cantidad de operaciones puede bloquear otros procesos y provocar que la experiencia general del usuario no sea buena. Para organizar todas las modificaciones que quieras hacer en la menor cantidad de listas independientes posible y, al mismo tiempo, evitar que bloqueen el sistema, debes configurar puntos de productividad de una o más operaciones. Un punto de productividad es un objeto ContentProviderOperation que tiene su valor isYieldAllowed() establecido en true. Cuando el proveedor de contactos encuentra un punto de productividad, pausa su trabajo para permitir que se ejecuten otros procesos y cierra la transacción actual. Cuando el proveedor se reinicia, continúa con la siguiente operación en la ArrayList e inicia una nueva transacción.

Los puntos de productividad generan más de una transacción por llamada a applyBatch(). Por este motivo, debes configurar un punto de productividad para un grupo de filas relacionadas de la última operación. Por ejemplo, debes configurar un punto de productividad para la última operación de un grupo que agrega filas de contacto sin procesar y sus filas de datos asociadas, o la última operación para un grupo de filas relacionadas con un contacto individual.

Los puntos de productividad también son una unidad de operación atómica. Todos los accesos entre dos puntos de productividad tienen éxito o fracasan como unidad. Si no configuras puntos de productividad, la más mínima operación atómica abarca el lote completo de operaciones. Si usas puntos de productividad, evitas que las operaciones degraden el rendimiento del sistema y, al mismo tiempo, te aseguras de que un subconjunto de operaciones sea atómico.

Referencias inversas de modificación

Cuando insertas un nuevo contacto sin procesar y sus datos asociados como un conjunto de objetos ContentProviderOperation, debes vincular las filas de datos con la fila del contacto sin procesar insertando el valor _ID del contacto sin procesar como el valor RAW_CONTACT_ID. No obstante, este valor no está disponible cuando creas la ContentProviderOperation para la fila de datos, ya que aún no aplicaste la ContentProviderOperation para la fila del contacto sin procesar. Para solucionar esto, la clase ContentProviderOperation.Builder tiene el método withValueBackReference(). Este método te permite insertar o modificar una columna con el resultado de una operación previa.

El método withValueBackReference() tiene dos argumentos:

key
La clave de un par clave-valor. El valor de este argumento debe ser el nombre de una columna de la tabla que estás modificando.
previousResult
El índice con base cero de un valor en el conjunto de objetos ContentProviderResult de applyBatch(). A medida que se aplican las operaciones del lote, el resultado de cada operación se guarda en un conjunto de resultados intermedio. El valor previousResult es el índice de uno de esos resultados, que se recupera y se guarda con el valor key. Esto te permite insertar un nuevo registro de contacto sin procesar y recuperar su valor _ID, para luego hacer una "referencia inversa" al valor cuando agregas una fila ContactsContract.Data.

El conjunto de resultados completo se crea cuando llamas por primera vez a applyBatch(), con un tamaño igual al de la ArrayList de objetos ContentProviderOperation que proporcionaste. No obstante, todos los elementos en el conjunto de resultados están configurados en null y, si intentas hacer una referencia inversa a un resultado de una operación que aún no se aplicó, withValueBackReference() lanza una Exception.

Los siguientes fragmentos de código muestran cómo insertar un nuevo contacto sin procesar y datos por lotes. Incluyen código que establece un punto de productividad y usa una referencia inversa.

El primer fragmento de código devuelve datos de contacto desde la IU. Para este momento, el usuario ya seleccionó la cuenta para la que se debe agregar el nuevo contacto sin procesar.

Kotlin

// Creates a contact entry from the current UI values, using the currently-selected account.
private fun createContactEntry() {
    /*
     * Gets values from the UI
     */
    val name = contactNameEditText.text.toString()
    val phone = contactPhoneEditText.text.toString()
    val email = contactEmailEditText.text.toString()

    val phoneType: String = contactPhoneTypes[mContactPhoneTypeSpinner.selectedItemPosition]

    val emailType: String = contactEmailTypes[mContactEmailTypeSpinner.selectedItemPosition]

Java

// Creates a contact entry from the current UI values, using the currently-selected account.
protected void createContactEntry() {
    /*
     * Gets values from the UI
     */
    String name = contactNameEditText.getText().toString();
    String phone = contactPhoneEditText.getText().toString();
    String email = contactEmailEditText.getText().toString();

    int phoneType = contactPhoneTypes.get(
            contactPhoneTypeSpinner.getSelectedItemPosition());

    int emailType = contactEmailTypes.get(
            contactEmailTypeSpinner.getSelectedItemPosition());

El siguiente fragmento de código crea una operación para insertar la fila de contacto sin procesar en la tabla ContactsContract.RawContacts:

Kotlin

    /*
     * Prepares the batch operation for inserting a new raw contact and its data. Even if
     * the Contacts Provider does not have any data for this person, you can't add a Contact,
     * only a raw contact. The Contacts Provider will then add a Contact automatically.
     */

    // Creates a new array of ContentProviderOperation objects.
    val ops = arrayListOf<ContentProviderOperation>()

    /*
     * Creates a new raw contact with its account type (server type) and account name
     * (user's account). Remember that the display name is not stored in this row, but in a
     * StructuredName data row. No other data is required.
     */
    var op: ContentProviderOperation.Builder =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.name)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.type)

    // Builds the operation and adds it to the array of operations
    ops.add(op.build())

Java

    /*
     * Prepares the batch operation for inserting a new raw contact and its data. Even if
     * the Contacts Provider does not have any data for this person, you can't add a Contact,
     * only a raw contact. The Contacts Provider will then add a Contact automatically.
     */

     // Creates a new array of ContentProviderOperation objects.
    ArrayList<ContentProviderOperation> ops =
            new ArrayList<ContentProviderOperation>();

    /*
     * Creates a new raw contact with its account type (server type) and account name
     * (user's account). Remember that the display name is not stored in this row, but in a
     * StructuredName data row. No other data is required.
     */
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
            .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.getType())
            .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.getName());

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

A continuación, el código crea filas de datos para el nombre para mostrar, el teléfono y el correo electrónico.

Cada objeto generador de la operación usa withValueBackReference() para obtener el RAW_CONTACT_ID. La referencia apunta al objeto ContentProviderResult de la primera operación, que agrega la fila de contacto sin procesar y devuelve su nuevo valor _ID. Como resultado, cada fila de datos se vincula automáticamente por su RAW_CONTACT_ID con la nueva fila ContactsContract.RawContacts a la que pertenece.

El objeto ContentProviderOperation.Builder que agrega la fila de correo electrónico está marcado con withYieldAllowed(), que establece un punto de productividad:

Kotlin

    // Creates the display name for the new raw contact, as a StructuredName data row.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * withValueBackReference sets the value of the first argument to the value of
             * the ContentProviderResult indexed by the second argument. In this particular
             * call, the raw contact ID column of the StructuredName data row is set to the
             * value of the result returned by the first operation, which is the one that
             * actually adds the raw contact row.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to StructuredName
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)

            // Sets the data row's display name to the name in the UI.
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name)

    // Builds the operation and adds it to the array of operations
    ops.add(op.build())

    // Inserts the specified phone number and type as a Phone data row
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Phone
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)

            // Sets the phone number and type
            .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
            .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType)

    // Builds the operation and adds it to the array of operations
    ops.add(op.build())

    // Inserts the specified email and type as a Phone data row
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Email
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)

            // Sets the email address and type
            .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
            .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType)

    /*
     * Demonstrates a yield point. At the end of this insert, the batch operation's thread
     * will yield priority to other threads. Use after every set of operations that affect a
     * single contact, to avoid degrading performance.
     */
    op.withYieldAllowed(true)

    // Builds the operation and adds it to the array of operations
    ops.add(op.build())

Java

    // Creates the display name for the new raw contact, as a StructuredName data row.
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * withValueBackReference sets the value of the first argument to the value of
             * the ContentProviderResult indexed by the second argument. In this particular
             * call, the raw contact ID column of the StructuredName data row is set to the
             * value of the result returned by the first operation, which is the one that
             * actually adds the raw contact row.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to StructuredName
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)

            // Sets the data row's display name to the name in the UI.
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

    // Inserts the specified phone number and type as a Phone data row
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Phone
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)

            // Sets the phone number and type
            .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
            .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneType);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

    // Inserts the specified email and type as a Phone data row
    op =
            ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            /*
             * Sets the value of the raw contact id column to the new raw contact ID returned
             * by the first operation in the batch.
             */
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)

            // Sets the data row's MIME type to Email
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)

            // Sets the email address and type
            .withValue(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
            .withValue(ContactsContract.CommonDataKinds.Email.TYPE, emailType);

    /*
     * Demonstrates a yield point. At the end of this insert, the batch operation's thread
     * will yield priority to other threads. Use after every set of operations that affect a
     * single contact, to avoid degrading performance.
     */
    op.withYieldAllowed(true);

    // Builds the operation and adds it to the array of operations
    ops.add(op.build());

El último fragmento de código muestra la llamada a applyBatch(), que inserta el nuevo contacto sin procesar y filas de datos.

Kotlin

    // Ask the Contacts Provider to create a new contact
    Log.d(TAG, "Selected account: ${mSelectedAccount.name} (${mSelectedAccount.type})")
    Log.d(TAG, "Creating contact: $name")

    /*
     * Applies the array of ContentProviderOperation objects in batch. The results are
     * discarded.
     */
    try {
        contentResolver.applyBatch(ContactsContract.AUTHORITY, ops)
    } catch (e: Exception) {
        // Display a warning
        val txt: String = getString(R.string.contactCreationFailure)
        Toast.makeText(applicationContext, txt, Toast.LENGTH_SHORT).show()

        // Log exception
        Log.e(TAG, "Exception encountered while inserting contact: $e")
    }
}

Java

    // Ask the Contacts Provider to create a new contact
    Log.d(TAG,"Selected account: " + selectedAccount.getName() + " (" +
            selectedAccount.getType() + ")");
    Log.d(TAG,"Creating contact: " + name);

    /*
     * Applies the array of ContentProviderOperation objects in batch. The results are
     * discarded.
     */
    try {

            getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
    } catch (Exception e) {

            // Display a warning
            Context ctx = getApplicationContext();

            CharSequence txt = getString(R.string.contactCreationFailure);
            int duration = Toast.LENGTH_SHORT;
            Toast toast = Toast.makeText(ctx, txt, duration);
            toast.show();

            // Log exception
            Log.e(TAG, "Exception encountered while inserting contact: " + e);
    }
}

Las operaciones por lotes también te permiten implementar control de simultaneidad optimista, un método de aplicación de transacciones de modificación sin tener que bloquear el repositorio subyacente. Para usar este método, debes aplicar la transacción y, después, comprobar si hay otras modificaciones que puedan haberse realizado al mismo tiempo. Si descubres que se produjo una modificación incoherente, puedes deshacer la transacción y volver a intentarlo.

El control de simultaneidad optimista es útil para un dispositivo móvil, en el que hay un solo usuario a la vez y los accesos simultáneos a un repositorio de datos son muy poco comunes. Como no se produce un bloqueo, no se pierde tiempo en configurar bloqueos ni en esperar que otras transacciones liberen sus bloqueos.

Para usar el control de simultaneidad optimista mientras actualizas una sola fila ContactsContract.RawContacts, sigue estos pasos:

  1. Recupera la columna VERSION del contacto sin procesar junto con los demás datos que obtengas.
  2. Crea un objeto ContentProviderOperation.Builder apto para implementar una restricción usando el método newAssertQuery(Uri). Para el URI de contenido, usa RawContacts.CONTENT_URI con el _ID del contacto sin procesar anexado.
  3. Para el objeto ContentProviderOperation.Builder, llama a withValue() para comparar la columna VERSION con el número de versión que acabas de recuperar.
  4. Para el mismo ContentProviderOperation.Builder, llama a withExpectedCount() para asegurarte de que esta aserción pruebe una sola fila.
  5. Llama a build() para crear el objeto ContentProviderOperation y, después, agrega este objeto como el primer objeto en la ArrayList que pasas a applyBatch().
  6. Aplica la transacción por lotes.

Si otra aplicación actualiza la fila de contacto sin procesar entre el momento en que lees la fila y el momento en que intentas modificarla, la "aserción" ContentProviderOperation fallará y se deshará todo el lote de operaciones. Luego, puedes volver a intentar el lote o realizar alguna otra acción.

El siguiente fragmento de código demuestra cómo crear una "aserción" ContentProviderOperation después de consultar por un solo contacto sin procesar usando un CursorLoader:

Kotlin

/*
 * The application uses CursorLoader to query the raw contacts table. The system calls this method
 * when the load is finished.
 */
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor) {
    // Gets the raw contact's _ID and VERSION values
    rawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID))
    mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION))
}

...

// Sets up a Uri for the assert operation
val rawContactUri: Uri = ContentUris.withAppendedId(
        ContactsContract.RawContacts.CONTENT_URI,
        rawContactID
)

// Creates a builder for the assert operation
val assertOp: ContentProviderOperation.Builder =
        ContentProviderOperation.newAssertQuery(rawContactUri).apply {
            // Adds the assertions to the assert operation: checks the version
            withValue(SyncColumns.VERSION, mVersion)

            // and count of rows tested
            withExpectedCount(1)
        }

// Creates an ArrayList to hold the ContentProviderOperation objects
val ops = arrayListOf<ContentProviderOperation>()

ops.add(assertOp.build())

// You would add the rest of your batch operations to "ops" here

...

// Applies the batch. If the assert fails, an Exception is thrown
try {
    val results: Array<ContentProviderResult> = contentResolver.applyBatch(AUTHORITY, ops)
} catch (e: OperationApplicationException) {
    // Actions you want to take if the assert operation fails go here
}

Java

/*
 * The application uses CursorLoader to query the raw contacts table. The system calls this method
 * when the load is finished.
 */
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {

    // Gets the raw contact's _ID and VERSION values
    rawContactID = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
    mVersion = cursor.getInt(cursor.getColumnIndex(SyncColumns.VERSION));
}

...

// Sets up a Uri for the assert operation
Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactID);

// Creates a builder for the assert operation
ContentProviderOperation.Builder assertOp = ContentProviderOperation.newAssertQuery(rawContactUri);

// Adds the assertions to the assert operation: checks the version and count of rows tested
assertOp.withValue(SyncColumns.VERSION, mVersion);
assertOp.withExpectedCount(1);

// Creates an ArrayList to hold the ContentProviderOperation objects
ArrayList ops = new ArrayList<ContentProviderOperation>;

ops.add(assertOp.build());

// You would add the rest of your batch operations to "ops" here

...

// Applies the batch. If the assert fails, an Exception is thrown
try
    {
        ContentProviderResult[] results =
                getContentResolver().applyBatch(AUTHORITY, ops);

    } catch (OperationApplicationException e) {

        // Actions you want to take if the assert operation fails go here
    }

Recuperación y modificación con intents

Enviar una intent a la aplicación de contactos del dispositivo te permite acceder al proveedor de contactos de forma indirecta. La intent inicia la IU de la aplicación de contactos del dispositivo, en la que los usuarios realizan tareas asociadas a los contactos. Con este tipo de acceso, los usuarios pueden hacer lo siguiente:

  • Seleccionar un contacto de una lista y hacer que se le devuelva a tu aplicación para continuar trabajando allí.
  • Editar datos de un contacto existente.
  • Insertar un nuevo contacto sin procesar para cualquiera de sus cuentas.
  • Eliminar un contacto o datos de contacto.

Si el usuario está insertando o actualizando datos, puedes recopilar los datos primero y, después, enviarlos como parte de la intent.

Cuando usas intents para acceder al proveedor de contactos a través de la aplicación de contactos del dispositivo, no necesitas escribir una IU ni un código propios para acceder al proveedor. Tampoco necesitas solicitar permiso para leer ni escribirle al proveedor. La aplicación de contactos del dispositivo puede delegarte el permiso de lectura para un contacto y, como estás modificando el proveedor mediante otra aplicación, no necesitas permisos de escritura.

El proceso general de enviar una intent para acceder a un proveedor se describe en detalle en la guía Conceptos básicos sobre el proveedor de contenido, en la sección "Acceso a datos mediante intents". La acción, el tipo de MIME y los valores de datos que usas para las tareas disponibles se resumen en la tabla 4, mientras que los valores adicionales que puedes usar con putExtra() se indican en la documentación de referencia para ContactsContract.Intents.Insert:

Tabla 4: Intents del proveedor de contactos

Tarea Acción Datos Tipo de MIME Notas
Selecciona un contacto de la lista ACTION_PICK Una de las siguientes opciones: Sin utilizar Muestra una lista de contactos sin procesar o una lista de datos de un contacto sin procesar, según el tipo de URI de contenido que proporciones.

Llama a startActivityForResult(), que devuelve el URI de contenido de la fila seleccionada. El formato del URI es el URI del contenido de la tabla con el LOOKUP_ID de la fila anexado. La aplicación de contactos del dispositivo delega los permisos de lectura y escritura a este URI de contenido durante todo el ciclo de vida de tu actividad. Consulta la guía Conceptos básicos sobre el proveedor de contenido para obtener más detalles.

Inserta un nuevo contacto sin procesar Insert.ACTION N/D RawContacts.CONTENT_TYPE, tipo de MIME de un conjunto de contactos sin procesar. Muestra la pantalla Agregar contacto de la aplicación de contactos del dispositivo. Se muestran los valores adicionales que agregas a la intent. Si se envió con startActivityForResult(), el URI de contenido del contacto sin procesar recientemente agregado regresará al método de devolución de llamada onActivityResult() de tu actividad en el argumento Intent del campo "datos". Para obtener el valor, llama a getData().
Edita un contacto ACTION_EDIT CONTENT_LOOKUP_URI del contacto. La actividad del editor le permitirá al usuario editar cualquier dato asociado con este contacto. Contacts.CONTENT_ITEM_TYPE, un contacto individual. Muestra la pantalla "Editar contacto" de la aplicación de contactos. Se muestran los valores adicionales que agregas a la intent. Cuando el usuario hace clic en Listo para guardar las ediciones, tu actividad vuelve a primer plano.
Muestra un selector que, además, permita agregar datos. ACTION_INSERT_OR_EDIT N/D CONTENT_ITEM_TYPE Esta intent siempre muestra la pantalla del selector de la aplicación de contactos. El usuario puede seleccionar un contacto para editarlo o agregar un nuevo contacto. Aparecerá la pantalla de edición o de adición (según la elección del usuario) y se mostrarán los datos adicionales que pases en la intent. Si tu aplicación muestra datos de contacto, como un correo electrónico o un número de teléfono, usa esta intent para permitirle al usuario agregar datos a un contacto existente.

Nota: No necesitas enviar un valor de nombre en los extras de esta intent, ya que el usuario siempre selecciona un nombre existente o agrega uno nuevo. Además, si envías un nombre y el usuario decide realizar una edición, la aplicación de contactos muestra el nombre que tú envías y sobrescribe el valor anterior. Si el usuario no se da cuenta y guarda la edición, se pierde el valor anterior.

La aplicación de contactos del dispositivo no permite usar una intent para eliminar un contacto sin procesar ni ninguno de sus datos. En cambio, para eliminar un contacto sin procesar, debes usar ContentResolver.delete() o ContentProviderOperation.newDelete().

El siguiente fragmento de código permite ver cómo construir y enviar una intent que inserte un nuevo contacto sin procesar y datos:

Kotlin

// Gets values from the UI
val name = contactNameEditText.text.toString()
val phone = contactPhoneEditText.text.toString()
val email = contactEmailEditText.text.toString()

val company = companyName.text.toString()
val jobtitle = jobTitle.text.toString()

/*
 * Demonstrates adding data rows as an array list associated with the DATA key
 */

// Defines an array list to contain the ContentValues objects for each row
val contactData = arrayListOf<ContentValues>()

/*
 * Defines the raw contact row
 */

// Sets up the row as a ContentValues object
val rawContactRow = ContentValues().apply {
    // Adds the account type and name to the row
    put(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.type)
    put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.name)
}

// Adds the row to the array
contactData.add(rawContactRow)

/*
 * Sets up the phone number data row
 */

// Sets up the row as a ContentValues object
val phoneRow = ContentValues().apply {
    // Specifies the MIME type for this data row (all data rows must be marked by their type)
    put(ContactsContract.Data.MIMETYPE,ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)

    // Adds the phone number and its type to the row
    put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
}

// Adds the row to the array
contactData.add(phoneRow)

/*
 * Sets up the email data row
 */

// Sets up the row as a ContentValues object
val emailRow = ContentValues().apply {
    // Specifies the MIME type for this data row (all data rows must be marked by their type)
    put(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)

    // Adds the email address and its type to the row
    put(ContactsContract.CommonDataKinds.Email.ADDRESS, email)
}

// Adds the row to the array
contactData.add(emailRow)

// Creates a new intent for sending to the device's contacts application
val insertIntent = Intent(ContactsContract.Intents.Insert.ACTION).apply {
    // Sets the MIME type to the one expected by the insertion activity
    type = ContactsContract.RawContacts.CONTENT_TYPE

    // Sets the new contact name
    putExtra(ContactsContract.Intents.Insert.NAME, name)

    // Sets the new company and job title
    putExtra(ContactsContract.Intents.Insert.COMPANY, company)
    putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle)

    /*
    * Adds the array to the intent's extras. It must be a parcelable object in order to
    * travel between processes. The device's contacts app expects its key to be
    * Intents.Insert.DATA
    */
    putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData)
}

// Send out the intent to start the device's contacts app in its add contact activity.
startActivity(insertIntent)

Java

// Gets values from the UI
String name = contactNameEditText.getText().toString();
String phone = contactPhoneEditText.getText().toString();
String email = contactEmailEditText.getText().toString();

String company = companyName.getText().toString();
String jobtitle = jobTitle.getText().toString();

// Creates a new intent for sending to the device's contacts application
Intent insertIntent = new Intent(ContactsContract.Intents.Insert.ACTION);

// Sets the MIME type to the one expected by the insertion activity
insertIntent.setType(ContactsContract.RawContacts.CONTENT_TYPE);

// Sets the new contact name
insertIntent.putExtra(ContactsContract.Intents.Insert.NAME, name);

// Sets the new company and job title
insertIntent.putExtra(ContactsContract.Intents.Insert.COMPANY, company);
insertIntent.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, jobtitle);

/*
 * Demonstrates adding data rows as an array list associated with the DATA key
 */

// Defines an array list to contain the ContentValues objects for each row
ArrayList<ContentValues> contactData = new ArrayList<ContentValues>();


/*
 * Defines the raw contact row
 */

// Sets up the row as a ContentValues object
ContentValues rawContactRow = new ContentValues();

// Adds the account type and name to the row
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_TYPE, selectedAccount.getType());
rawContactRow.put(ContactsContract.RawContacts.ACCOUNT_NAME, selectedAccount.getName());

// Adds the row to the array
contactData.add(rawContactRow);

/*
 * Sets up the phone number data row
 */

// Sets up the row as a ContentValues object
ContentValues phoneRow = new ContentValues();

// Specifies the MIME type for this data row (all data rows must be marked by their type)
phoneRow.put(
        ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
);

// Adds the phone number and its type to the row
phoneRow.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone);

// Adds the row to the array
contactData.add(phoneRow);

/*
 * Sets up the email data row
 */

// Sets up the row as a ContentValues object
ContentValues emailRow = new ContentValues();

// Specifies the MIME type for this data row (all data rows must be marked by their type)
emailRow.put(
        ContactsContract.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
);

// Adds the email address and its type to the row
emailRow.put(ContactsContract.CommonDataKinds.Email.ADDRESS, email);

// Adds the row to the array
contactData.add(emailRow);

/*
 * Adds the array to the intent's extras. It must be a parcelable object in order to
 * travel between processes. The device's contacts app expects its key to be
 * Intents.Insert.DATA
 */
insertIntent.putParcelableArrayListExtra(ContactsContract.Intents.Insert.DATA, contactData);

// Send out the intent to start the device's contacts app in its add contact activity.
startActivity(insertIntent);

Integridad de los datos

Como el repositorio de contactos contiene datos importantes y sensibles que los usuarios prevén que serán correctos y estarán actualizados, el proveedor de contactos tiene reglas bien definidas para la integridad de los datos. Es tu responsabilidad cumplir con esas reglas cuando modificas datos de contactos. Aquí te indicamos las reglas importantes:

Agrega siempre una fila ContactsContract.CommonDataKinds.StructuredName para cada fila ContactsContract.RawContacts que agregues.
Una fila ContactsContract.RawContacts sin una fila ContactsContract.CommonDataKinds.StructuredName en la tabla ContactsContract.Data puede provocar problemas durante la agregación.
Vincula siempre las filas ContactsContract.Data nuevas con su fila ContactsContract.RawContacts principal.
Una fila ContactsContract.Data que no esté vinculada a un ContactsContract.RawContacts no estará visible en la aplicación de contactos del dispositivo y podría causar problemas con los adaptadores de sincronización.
Modifica datos solo para los contactos sin procesar que te pertenecen.
Recuerda que, generalmente, el proveedor de contactos administra datos de muchos tipos de cuenta o servicios en línea diferentes. Debes asegurarte de que tu aplicación solo modifique o elimine datos de las filas que te pertenecen, y de que solo inserte datos con un tipo y un nombre de cuenta que tú controles.
Usa siempre las constantes que se definen en ContactsContract y sus subclases para autoridades, URI de contenido, rutas de acceso de URI, nombres de columna, tipos de MIME y valores TYPE.
Usar estas constantes ayuda a evitar errores. También se te notificará con advertencias del compilador si alguna de las constantes deja de estar disponible.

Filas de datos personalizados

Al crear y usar tus propios tipos de MIME, puedes insertar, editar, eliminar y recuperar tus propias filas de datos en la tabla ContactsContract.Data. Tus filas están limitadas a usar la columna que se define en ContactsContract.DataColumns, aunque puedes asignar tus propios nombres de columna específicos para un tipo determinado a los nombres predeterminados de las columnas. En la aplicación de contactos del dispositivo, se muestran los datos de tus filas, pero no pueden editarse ni eliminarse, y los usuarios no pueden agregar datos adicionales. Para permitirles a los usuarios modificar tus filas de datos personalizados, debes proporcionar una actividad de editor en tu aplicación.

Para mostrar tus datos personalizados, proporciona un archivo contacts.xml que contenga un elemento <ContactsAccountType> y uno o más de sus elementos <ContactsDataKind> secundarios. Esto se describe más detalladamente en la sección <ContactsDataKind> element.

Para obtener más información acerca de los tipos de MIME personalizados, consulta la guía Crear un proveedor de contenido.

Adaptadores de sincronización del proveedor de contactos

El proveedor de contactos está específicamente diseñado para manipular la sincronización de datos de contactos entre un dispositivo y un servicio en línea. Esto les permite a los usuarios descargar datos existentes a un dispositivo nuevo y cargar datos existentes a una cuenta nueva. La sincronización también garantiza que los usuarios tengan datos actualizados a mano, independientemente del origen de las adiciones y los cambios. Otra ventaja de la sincronización es que permite que los datos de los contactos estén disponibles incluso cuando el dispositivo no está conectado a la red.

Si bien puedes implementar la sincronización de diversas formas, el sistema Android ofrece un marco de trabajo de sincronización mediante complementos que automatiza las siguientes tareas:

  • Comprobación de la disponibilidad de la red.
  • Programación y ejecución de la sincronización en función de las preferencias del usuario.
  • Reinicio de sincronizaciones detenidas.

Para usar este marco de trabajo, debes proporcionar un complemento de adaptador de sincronización. Cada adaptador de sincronización es exclusivo de un servicio y proveedor de contenido, pero puede manipular múltiples nombres de cuenta para el mismo servicio. El marco de trabajo también admite múltiples adaptadores de sincronización para el mismo servicio y proveedor.

Clases y archivos del adaptador de sincronización

Cuando implementas un adaptador de sincronización, lo haces como una subclase de AbstractThreadedSyncAdapter y lo instalas como parte de una aplicación de Android. El sistema sabe de la existencia del adaptador de sincronización a partir de elementos del manifiesto de tu aplicación y de un archivo XML especial al que hace referencia el manifiesto. El archivo XML define el tipo de cuenta del servicio en línea y la autoridad del proveedor de contenido, que juntos identifican de forma única al adaptador. El adaptador de sincronización no se activa hasta que el usuario agrega una cuenta para el tipo de cuenta del adaptador de sincronización y habilita la sincronización para el proveedor de contenido con el que se sincroniza el adaptador de sincronización. En ese momento, el sistema comienza a administrar el adaptador, llamándolo cuando sea necesario para establecer una sincronización entre el proveedor de contenido y el servidor.

Nota: Usar un tipo de cuenta como parte de la identificación del adaptador de sincronización le permite al sistema detectar y agrupar adaptadores de sincronización que acceden a diferentes servicios de la misma organización. Por ejemplo, los adaptadores de sincronización para los servicios en línea de Google tienen todos el mismo tipo de cuenta com.google. Cuando los usuarios agregan una cuenta de Google a sus dispositivos, todos los adaptadores de sincronización instalados para los servicios de Google se indican juntos; cada adaptador de sincronización indicado se sincroniza con un proveedor de contenido diferente en el dispositivo.

Como la mayoría de los servicios requieren que los usuarios verifiquen su identidad antes de acceder a datos, el sistema Android ofrece un marco de trabajo de autenticación que es similar al del adaptador de sincronización y, a menudo, se usa junto a ese marco de trabajo. El marco de trabajo de autenticación usa autenticadores de complementos que son subclases de AbstractAccountAuthenticator. Un autenticador verifica la identidad del usuario mediante los siguientes pasos:

  1. Recopila el nombre del usuario, la contraseña o información similar (las credenciales del usuario).
  2. Envía las credenciales al servicio.
  3. Examina la respuesta del servicio.

Si el servicio acepta las credenciales, el autenticador puede guardarlas para usarlas más adelante. Gracias al marco de trabajo del autenticador de complementos, el AccountManager puede proporcionar acceso a cualquier token de autenticación que un autenticador admita y decida exponer, como los tokens de autenticación Oauth2.

Si bien la autenticación no es obligatoria, la mayoría de los servicios de contacto la utilizan. No obstante, no es necesario que uses el marco de trabajo de autenticación de Android para realizar autenticaciones.

Implementación de un adaptador de sincronización

Para implementar un adaptador de sincronización del proveedor de contactos, debes comenzar por crear una aplicación para Android que tenga lo siguiente:

Un componente Service que responda a solicitudes del sistema para enlazar con el adaptador de sincronización.
Cuando el sistema quiere ejecutar una sincronización, llama al método onBind() del servicio para obtener un IBinder del adaptador de sincronización. Esto le permite al sistema realizar llamadas entre procesos a los métodos del adaptador.
El adaptador de sincronización real, implementado como una subclase concreta de AbstractThreadedSyncAdapter.
Esta clase tiene la función de descargar datos del servidor, cargar datos del dispositivo y resolver conflictos. El trabajo principal del adaptador se realiza en el método onPerformSync(). Esta clase se debe iniciar como un singleton.
Una subclase de Application.
Esta clase actúa como una fábrica para el singleton del adaptador de sincronización. Usa el método onCreate() para crear una instancia del adaptador de sincronización y proporciona un "captador" para devolver el singleton al método onBind() del servicio del adaptador de sincronización.
Opcional: Un componente Service que responde a solicitudes del sistema para la autenticación de usuarios.
AccountManager inicia este servicio para comenzar el proceso de autenticación. El método onCreate() del servicio crea una instancia de un objeto autenticador. Cuando el sistema quiere autenticar una cuenta de usuario del adaptador de sincronización de una aplicación, llama al método onBind() del servicio para obtener un IBinder del autenticador. Esto le permite al sistema realizar llamadas entre procesos a los métodos del autenticador.
Opcional: Una subclase concreta de AbstractAccountAuthenticator que aborda las solicitudes de autenticación.
Esta clase proporciona métodos que el AccountManager invoca para autenticar las credenciales del usuario con el servidor. Los detalles del proceso de autenticación varían ampliamente en función de la tecnología del servidor que se esté utilizando. Debes consultar la documentación del software de tu servidor para obtener más información acerca de la autenticación.
Archivos XML que definen el adaptador de sincronización y el autenticador en el sistema.
Los componentes del servicio de adaptador de sincronización y autenticador antes descritos se definen en elementos <service> del manifiesto de la aplicación. Esos elementos contienen elementos secundarios <meta-data> que le proporcionan datos específicos al sistema:
  • El elemento <meta-data> del servicio de adaptador de sincronización apunta al archivo XML res/xml/syncadapter.xml. A la vez, este archivo especifica un URI del servicio web que se sincroniza con el proveedor de contactos, y el tipo de cuenta del servicio web.
  • Opcional: El elemento <meta-data> del autenticador apunta al archivo XML res/xml/authenticator.xml. A la vez, este archivo especifica el tipo de cuenta que admite este autenticador, así como recursos de la IU que aparecen durante el proceso de autenticación. El tipo de cuenta especificado en este elemento debe ser el mismo que el tipo de cuenta especificado para el adaptador de sincronización.

Datos de redes sociales

Las tablas android.provider.ContactsContract.StreamItems y android.provider.ContactsContract.StreamItemPhotos administran los datos entrantes de las redes sociales. Puedes escribir un adaptador de sincronización que agregue datos transmitidos desde tu propia red a estas tablas, puedes leer datos transmitidos desde estas tablas y mostrarlos en tu aplicación, o puedes realizar ambas opciones. Con estas funciones, puedes integrar los servicios y las aplicaciones de tus redes sociales a la experiencia de redes sociales de Android.

Texto de redes sociales

Los elementos de la secuencia siempre están asociados con un contacto sin procesar. El android.provider.ContactsContract.StreamItemsColumns#RAW_CONTACT_ID establece un enlace con el valor _ID para el contacto sin procesar. El tipo y el nombre de cuenta del contacto sin procesar también se guardan en la fila del elemento de la secuencia.

Guarda los datos de la secuencia en las siguientes columnas:

android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_TYPE
Obligatorio: El tipo de cuenta del usuario del contacto sin procesar asociado con este elemento de la secuencia. Recuerda establecer este valor cuando insertes un elemento de la secuencia.
android.provider.ContactsContract.StreamItemsColumns#ACCOUNT_NAME
Obligatorio: El nombre de cuenta del usuario del contacto sin procesar asociado con este elemento de la secuencia. Recuerda establecer este valor cuando insertes un elemento de la secuencia.
Columnas identificadoras
Obligatorio: Debes insertar las siguientes columnas identificadoras cuando insertas un elemento de la secuencia:
  • android.provider.ContactsContract.StreamItemsColumns#CONTACT_ID: El valor android.provider.BaseColumns#_ID del contacto con el que está asociado este elemento de la secuencia.
  • android.provider.ContactsContract.StreamItemsColumns#CONTACT_LOOKUP_KEY: El valor android.provider.ContactsContract.ContactsColumns#LOOKUP_KEY del contacto con el que está asociado este elemento de la secuencia.
  • android.provider.ContactsContract.StreamItemsColumns#RAW_CONTACT_ID: El valor android.provider.BaseColumns#_ID del contacto sin procesar con el que está asociado este elemento de la secuencia.
android.provider.ContactsContract.StreamItemsColumns#COMMENTS
Opcional: Guarda información resumida que puedas mostrar al comienzo de un elemento de flujo.
android.provider.ContactsContract.StreamItemsColumns#TEXT
El texto del elemento de la secuencia, ya sea el contenido publicado por la fuente del elemento o una descripción de alguna acción que generó el elemento de la secuencia. Esta columna puede contener cualquier formato e imágenes de recursos insertadas que fromHtml() puede representar. El proveedor podría truncar o contraer contenido extenso, pero intentará evitar romper las etiquetas.
android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP
Una cadena de texto que contiene la hora en la que se insertó o actualizó el elemento de la secuencia en forma de milisegundos desde el epoch. Las aplicaciones que insertan o actualizan elementos de una secuencia son responsables de mantener esta columna; el proveedor de contactos no la mantiene automáticamente.

A fin de mostrar información que identifique tus elementos de la secuencia, usa android.provider.ContactsContract.StreamItemsColumns#RES_ICON, android.provider.ContactsContract.StreamItemsColumns#RES_LABEL y android.provider.ContactsContract.StreamItemsColumns#RES_PACKAGE para establecer enlaces con recursos de tu aplicación.

La tabla android.provider.ContactsContract.StreamItems también contiene las columnas android.provider.ContactsContract.StreamItemsColumns#SYNC1 mediante android.provider.ContactsContract.StreamItemsColumns#SYNC4 para uso exclusivo de los adaptadores de sincronización.

Fotos de redes sociales

En la tabla android.provider.ContactsContract.StreamItemPhotos, se guardan fotos asociadas con un elemento de la secuencia. La columna android.provider.ContactsContract.StreamItemPhotosColumns#STREAM_ITEM_ID de la tabla establece enlaces con valores de la columna _ID de la tabla android.provider.ContactsContract.StreamItems. Las referencias a las fotos se guardan en las siguientes columnas de la tabla:

Columna android.provider.ContactsContract.StreamItemPhotos#PHOTO (un BLOB).
Una representación binaria de la foto, con el tamaño modificado por el proveedor para poder guardarla y mostrarla. Esta columna es compatible con versiones anteriores del proveedor de contactos que la utilizó para guardar fotos. No obstante, en la versión actual, no debes usar esta columna para guardar fotos. En su lugar, usa android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID o android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI (ambos se describen en los siguientes puntos) para guardar fotos en un archivo. Ahora, esta columna contiene una miniatura de la foto, que admite lectura.
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_FILE_ID
Un identificador numérico de una foto de un contacto sin procesar. Anexa este valor a la constante DisplayPhoto.CONTENT_URI para obtener el URI de contenido que haga referencia a un solo archivo de foto y, después, llama a openAssetFileDescriptor() a fin de obtener un identificador para el archivo de foto.
android.provider.ContactsContract.StreamItemPhotosColumns#PHOTO_URI
Un URI de contenido que hace referencia directa al archivo de foto correspondiente a la foto representada por esta fila. Llama a openAssetFileDescriptor() con este URI para obtener un identificador del archivo de foto.

Uso de tablas de redes sociales

Estas tablas funcionan de la misma manera que las otras tablas principales en el proveedor de contactos, excepto por lo siguiente:

  • Estas tablas requieren permisos de acceso adicionales. Para leerlos, tu aplicación debe tener el permiso android.Manifest.permission#READ_SOCIAL_STREAM. Para modificarlos, debe tener el permiso android.Manifest.permission#WRITE_SOCIAL_STREAM.
  • Para la tabla android.provider.ContactsContract.StreamItems, la cantidad de filas guardadas para cada contacto sin procesar es limitada. Una vez que se alcanza este límite, el proveedor de contactos hace espacio para una nueva fila de elementos del flujo borrando automáticamente las filas con android.provider.ContactsContract.StreamItemsColumns#TIMESTAMP más antiguos. Para obtener el límite, envía una consulta al URI de contenido android.provider.ContactsContract.StreamItems#CONTENT_LIMIT_URI. Puedes abandonar todos los argumentos excepto el URI de contenido establecido en null. La consulta devuelve un cursor que contiene una sola fila con la columna individual android.provider.ContactsContract.StreamItems#MAX_ITEMS.

La clase android.provider.ContactsContract.StreamItems.StreamItemPhotos define una subtabla de android.provider.ContactsContract.StreamItemPhotos con las filas de fotos para un solo elemento de la secuencia.

Interacciones con redes sociales

Los datos de redes sociales que administra el proveedor de contactos, junto con la aplicación de contactos del dispositivo, ofrece un método sólido para conectar tu sistema de redes sociales con los contactos existentes. Están disponibles las siguientes funciones:

  • Sincronizando tu servicio de redes sociales con el proveedor de contactos mediante un adaptador de sincronización, puedes recuperar la actividad reciente de los contactos de un usuario y guardarla en las tablas android.provider.ContactsContract.StreamItems y android.provider.ContactsContract.StreamItemPhotos para usarla más adelante.
  • Además de la sincronización común, puedes programar tu adaptador de sincronización para que recupere datos adicionales cuando el usuario seleccione un contacto para visualizar. Esto le permite a tu adaptador de sincronización recuperar fotos de alta calidad y los elementos más recientes de la secuencia para el contacto.
  • Al registrar una notificación con la aplicación de contactos del dispositivo y el proveedor de contactos, puedes recibir una intent cuando se visualice un contacto y, en ese momento, actualizar el estado del contacto desde tu servicio. Este enfoque puede ser más rápido y usar menos ancho de banda que si realizaras una sincronización completa con un adaptador de sincronización.
  • Los usuarios pueden agregar un contacto a tu servicio de redes sociales mientras miras el contacto en la aplicación de contactos del dispositivo. Puedes habilitar esto con la función "invitar contacto", que habilitas con una combinación de una actividad que agrega un contacto existente a tu red y un archivo XML que proporciona la aplicación de contactos del dispositivo y el proveedor de contactos con los detalles de tu aplicación.

La sincronización común de los elementos de flujo con el proveedor de contactos es igual a otras sincronizaciones. Para obtener más información acerca de la sincronización, consulta la sección Adaptadores de sincronización del proveedor de contactos. El registro de invitaciones y la invitación de contactos se analizan en las siguientes dos secciones.

Registro para administrar visualizaciones de redes sociales

A fin de registrar tu adaptador de sincronización para que reciba notificaciones cuando el usuario visualiza un contacto administrado por tu adaptador de sincronización:

  1. Crea un archivo llamado contacts.xml en el directorio res/xml/ de tu proyecto. Si ya tienes este archivo, puedes omitir este paso.
  2. En este archivo, agrega el elemento <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">. Si el elemento ya existe, puedes omitir este paso.
  3. Para registrar un servicio que reciba una notificación cuando el usuario abra la página de detalles de un contacto en la aplicación de contactos del dispositivo, agrega el atributo viewContactNotifyService="serviceclass" al elemento, en el que serviceclass es el nombre de clase completo del servicio que debe recibir la intent de la aplicación de contactos del dispositivo. Para el servicio de notificación, usa una clase que extienda IntentService a fin de permitir que el servicio reciba intents. Los datos de la intent entrante contienen el URI de contenido del contacto sin procesar en el que el usuario hizo clic. Desde el servicio de notificación, puedes establecer un enlace con el adaptador de sincronización y, después, llamarlo para actualizar los datos del contacto sin procesar.

Para registrar una actividad a la que se llamará cuando el usuario haga clic en un elemento o una foto de la secuencia (o en ambos):

  1. Crea un archivo llamado contacts.xml en el directorio res/xml/ de tu proyecto. Si ya tienes este archivo, puedes omitir este paso.
  2. En este archivo, agrega el elemento <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">. Si el elemento ya existe, puedes omitir este paso.
  3. A fin de registrar una de tus actividades para que manipule al usuario que hace clic en un elemento de la secuencia en la aplicación de contactos del dispositivo, agrega el atributo viewStreamItemActivity="activityclass" al elemento, en el que activityclass es el nombre de clase completo de la actividad que debe recibir la intent de la aplicación de contactos del dispositivo.
  4. A fin de registrar una de tus actividades para que administre al usuario que hace clic en una foto de una secuencia en la aplicación de contactos del dispositivo, agrega el atributo viewStreamItemPhotoActivity="activityclass" al elemento, en el que activityclass es el nombre de clase completo de la actividad que debe recibir la intent de la aplicación de contactos del dispositivo.

El elemento <ContactsAccountType> se describe más detalladamente en la sección Elemento <ContactsAccountType>.

La intent entrante contiene el URI de contenido del elemento o la foto en los que el usuario hizo clic. Para tener actividades independientes para los elementos de texto y las fotos, usa ambos atributos en el mismo archivo.

Interacción con tu servicio de redes sociales

No es necesario que los contactos salgan de la aplicación de contactos del dispositivo para invitar a un contacto a tu sitio de redes sociales. En su lugar, puedes hacer que la aplicación de contactos del dispositivo envíe una intent para invitar al contacto a una de tus actividades. Para configurarlo:

  1. Crea un archivo llamado contacts.xml en el directorio res/xml/ de tu proyecto. Si ya tienes este archivo, puedes omitir este paso.
  2. En este archivo, agrega el elemento <ContactsAccountType xmlns:android="http://schemas.android.com/apk/res/android">. Si el elemento ya existe, puedes omitir este paso.
  3. Agrega los siguientes atributos:
    • inviteContactActivity="activityclass"
    • inviteContactActionLabel="@string/invite_action_label"
    El valor activityclass es el nombre de clase completo de la actividad que debe recibir la intent. El valor invite_action_label es una cadena de texto que se muestra en el menú Agregar conexión en la aplicación de contactos del dispositivo.

Nota: ContactsSource es un nombre de etiqueta obsoleto para ContactsAccountType.

Referencia contacts.xml

El archivo contacts.xml contiene elementos XML que controlan la interacción de tu adaptador de sincronización y tu aplicación con la aplicación de contactos y el proveedor de contactos. Estos elementos se describen en las siguientes secciones.

Elemento <ContactsAccountType>

El elemento <ContactsAccountType> controla la interacción de tu aplicación con la aplicación de contactos. Tiene la siguiente sintaxis:

<ContactsAccountType
        xmlns:android="http://schemas.android.com/apk/res/android"
        inviteContactActivity="activity_name"
        inviteContactActionLabel="invite_command_text"
        viewContactNotifyService="view_notify_service"
        viewGroupActivity="group_view_activity"
        viewGroupActionLabel="group_action_text"
        viewStreamItemActivity="viewstream_activity_name"
        viewStreamItemPhotoActivity="viewphotostream_activity_name">

contenido en:

res/xml/contacts.xml

puede contener:

<ContactsDataKind>

Descripción:

Declara componentes de Android y etiquetas de la IU que permiten a los usuarios invitar a uno de sus contactos a una red social, notificar a los usuarios cuando se actualiza una secuencia de una de sus redes sociales, etc.

Ten presente que el prefijo de atributo android: no es necesario para los atributos de <ContactsAccountType>.

Atributos:

inviteContactActivity
El nombre de clase completo de la actividad de tu aplicación que quieres activar cuando el usuario selecciona Agregar conexión en la aplicación de contactos del dispositivo.
inviteContactActionLabel
Una cadena de texto que se muestra para la actividad especificada en inviteContactActivity, en el menú Agregar conexión. Por ejemplo, puedes usar la cadena "Sigue mi red". Puedes usar un identificador de recursos de cadena para esta etiqueta.
viewContactNotifyService
El nombre de clase completo de un servicio de tu aplicación que debe recibir notificaciones cuando el usuario visualiza un contacto. Esta notificación, enviada por la aplicación de contactos del dispositivo, le permite a tu aplicación posponer operaciones que involucran una gran cantidad de datos hasta que sean necesarias. Por ejemplo, tu aplicación puede responder a esta notificación leyendo y mostrando la foto de alta resolución y los elementos más recientes de la red social del contacto. Esta función se describe más detalladamente en la sección Interacciones con redes sociales.
viewGroupActivity
El nombre de clase completo de una actividad de tu aplicación que puede mostrar información de un grupo. Cuando el usuario hace clic en la etiqueta del grupo en la aplicación de contactos del dispositivo, se muestra la IU para esta actividad.
viewGroupActionLabel
La etiqueta que muestra la aplicación de contactos de un control de la IU que le permite al usuario visualizar grupos en tu aplicación.

Se puede usar un identificador de recursos de cadena para este atributo.

viewStreamItemActivity
El nombre de clase completo de una actividad de tu aplicación que la aplicación de contactos del dispositivo lanza cuando el usuario hace clic en un elemento de la secuencia de un contacto sin procesar.
viewStreamItemPhotoActivity
El nombre de clase completo de una actividad de tu aplicación que la aplicación de contactos del dispositivo lanza cuando el usuario hace clic en una foto del elemento de la secuencia para un contacto sin procesar.

Elemento <ContactsDataKind>

El elemento <ContactsDataKind> controla la exhibición de las filas de datos personalizados de tu aplicación en la IU de la aplicación de contactos. Tiene la siguiente sintaxis:

<ContactsDataKind
        android:mimeType="MIMEtype"
        android:icon="icon_resources"
        android:summaryColumn="column_name"
        android:detailColumn="column_name">

contenido en:

<ContactsAccountType>

Descripción:

Usa este elemento para que la aplicación de contactos muestre el contenido de una fila de datos personalizados como parte de los detalles de un contacto sin procesar. Cada elemento secundario <ContactsDataKind> de <ContactsAccountType> representa un tipo de fila de datos personalizados que tu adaptador de sincronización agrega a la tabla ContactsContract.Data. Agrega un elemento <ContactsDataKind> para cada tipo de MIME personalizado que uses. No necesitas agregar el elemento si tienes una fila de datos personalizados para la que no quieres mostrar datos.

Atributos:

android:mimeType
El tipo de MIME personalizado que definiste para uno de los tipos de fila de datos personalizados en la tabla ContactsContract.Data. Por ejemplo, el valor vnd.android.cursor.item/vnd.example.locationstatus podría ser un tipo de MIME personalizado de una fila de datos que registre la última ubicación conocida de un contacto.
android:icon
Un recurso de elemento de diseño de Android que la aplicación de contactos muestra junto a tus datos. Úsalo para indicarle al usuario que los datos provienen de tu servicio.
android:summaryColumn
El nombre de columna de los primeros dos valores recuperados de la fila de datos. El valor se muestra como la primera línea de la entrada de esta fila de datos. Se prevé que la primera línea se use como resumen de los datos, pero es opcional. Consulta también android:detailColumn.
android:detailColumn
El nombre de columna de los segundos dos valores recuperados de la fila de datos. El valor se muestra como la segunda línea de la entrada de esta fila de datos. Consulta también android:summaryColumn.

Otras funciones del proveedor de contactos

Además de las funciones principales descritas en las secciones anteriores, el proveedor de contactos ofrece estas funciones útiles para trabajar con datos de contactos:

  • Grupos de contactos
  • Funciones de fotografía

Grupos de contactos

Opcionalmente, el proveedor de contactos puede etiquetar conjuntos de contactos relacionados con datos de grupo. Si el servidor asociado con una cuenta de usuario quiere mantener los grupos, el adaptador de sincronización del tipo de cuenta de la cuenta debe transferir datos de grupo entre el proveedor de contactos y el servidor. Cuando los usuarios agregan un nuevo contacto al servidor y lo colocan en un grupo nuevo después, el adaptador de sincronización debe agregar el nuevo grupo a la tabla ContactsContract.Groups. El grupo o los grupos a los que pertenece un contacto sin procesar se guardan en la tabla ContactsContract.Data usando el tipo de MIME ContactsContract.CommonDataKinds.GroupMembership.

Si estás diseñando un adaptador de sincronización que agregará datos de contactos sin procesar del servidor al proveedor de contactos y no estás usando grupos, debes indicarle al proveedor que permita que tus datos estén visibles. En el código que se ejecuta cuando un usuario agrega una cuenta al dispositivo, actualiza la fila ContactsContract.Settings que el proveedor de contactos agrega para la cuenta. En esta fila, fija el valor de la columna Settings.UNGROUPED_VISIBLE en 1. Cuando hagas esto, el proveedor de contactos siempre permitirá que los datos de tus contactos estén visibles, incluso si no usas grupos.

Fotos de contactos

La tabla ContactsContract.Data guarda fotos como filas con el tipo de MIME Photo.CONTENT_ITEM_TYPE. La columna CONTACT_ID de la fila está vinculada a la columna _ID del contacto sin procesar al que pertenece. La clase ContactsContract.Contacts.Photo define una subtabla de ContactsContract.Contacts que contiene información de fotografía de la foto principal de un contacto, que es la foto principal del contacto sin procesar principal del contacto. De igual manera, la clase ContactsContract.RawContacts.DisplayPhoto define una subtabla de ContactsContract.RawContacts que contiene información de fotografía de la foto principal de un contacto sin procesar.

La documentación de referencia para ContactsContract.Contacts.Photo y ContactsContract.RawContacts.DisplayPhoto contiene ejemplos de recuperación de información de fotografía. No hay una clase que resulte más conveniente para recuperar la miniatura principal de un contacto sin procesar, pero puedes enviar una consulta a la tabla ContactsContract.Data seleccionando en el _ID del contacto sin procesar Photo.CONTENT_ITEM_TYPE y la columna IS_PRIMARY para buscar la fila de foto principal del contacto sin procesar.

Los datos de redes sociales de una persona también pueden incluir fotos. Esas fotos se guardan en la tabla android.provider.ContactsContract.StreamItemPhotos, que se describe más detalladamente en la sección Fotos de redes sociales.