L'utilisation des contacts est une opération commune à de nombreuses applications y compris parmi celles fournies par le plate-forme elle-même. Il est donc utile de savoir comment est structuré le modèle et comment y accéder. Le modèle des contact a beaucoup évolué au cours des versions de la plate-forme et il n'est pas vraiment intuitif désormais.
Dans la première version du modèle, les contacts étaient représentés par des séries de caractères dans des champs de longueur fixe. Avec ce système, chaque représentation d'un contact a une taille fixe ce qui permet d'accéder très rapidement à la liste. L'inconvénient majeur de ce modèle et sa faible capacité d'évolution tout en conservant une compatibilité ascendante.
Désormais, les contacts sont stockés dans une base de données. Il existe une base de données des contacts qui est structurée pour contenir toute les informations de chacun d'entre eux. Cette base de donnée peut être listée via le client SQLite : sqlite3 /data/data/com.android.providers.contacts/database/contacts2.db. Notez que toute les valeurs indiquée ici peuvent être appelées à évoluer avec les futures versions d'Android. La première table nommée "contacts" comporte un entier comme clef primaire et fourni un certain nombre d'informations. La seconde table nommée "data" dispose d'une colonne (la 4ème précisément) qui joue le rôle de "clef étrangère" (foreign key). Elle contient la clef primaire du tuple de la table "contacts" permettant ainsi de faire le lien entre les tuples des deux tables.
L'intérêt de ce modèle est immédiat : si on souhaite ajouter de nouvelles informations, on créé un table dédié dans cette base de données et on associe à chaque tuple de cette table la clef primaire du tuple de la table "contacts" qui lui correspond.
Cette capacité est utilisée par Android pour gérer les contacts lorsque l'appareil doit synchroniser les contacts depuis plusieurs comptes (gmail, facebook, exchange, etc.). Une table "raw_data" contient la clef primaire de chaque contact de l'ensemble des comptes de l'utilisateur. Chaque compte alimente la table raw_data avec l'ensemble des ses contacts (un tuple par contact) en attribuant à chaque fois une clef primaire différente. Android concatène alors dans la table "data" les informations de tuples différents ayant un même nom de contact. Notez que ce mécanisme n'est pas exempt d'erreur lorsque des contacts différents sont réellement homonymes (nom et prénom). Vous devez le garder à l'esprit lorsque vous déclarez vos contacts depuis différents comptes.
Il y a deux façons d'accéder aux contacts :
Nous nous intéressons ici au second moyen.
Tout d'abord, il faut autoriser Android à accéder en lecture et/ou en écriture à la base de données des contacts:
<uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.WRITE_CONTACTS" />
La classe ContactsContract.Data fournie les URI d'accès à la base de données. Celle qui accède au contenu est ContactsContract.Data.CONTENT_URI (table data). Il est alors aisé d'écrire un code qui effectue a lecture de ce contenu :
private void readContacts()
{
ContentResolver crl = getContentResolver();
Uri uri = ContactsContract.Data.CONTENT_URI;
Cursor crs = crl.query(uri, null, null, null, null);
int iColumns = crs.getColumnCount();
while (crs.moveToNext())
{
StringBuilder sbl = new StringBuilder();
for (int k = 0; k < iColumns; k++)
{
String sColName = crs.getColumnName(k);
int iType = crs.getType(k);
String sValue;
switch(iType)
{
case Cursor.FIELD_TYPE_NULL:
sValue = "null";
break;
case Cursor.FIELD_TYPE_INTEGER:
sValue = String.format("%d", crs.getInt(k));
break;
case Cursor.FIELD_TYPE_FLOAT:
sValue = String.format("%.3f", crs.getFloat(k));
break;
case Cursor.FIELD_TYPE_STRING:
sValue = crs.getString(k);
break;
default:
sValue = Converter.toHexaLowerCase(crs.getBlob(k));
}
sbl.append(String.format("%s=%s|", sColName, sValue));
}
Log.d(TAG, sbl.toString());
}
}Hélas, les valeurs retournées ne sont pas simples à interpréter car le curseur retourne des lignes de 57 colonnes et les données d'un même contact peuvent nécessiter un nombre variable de lignes. Nous donnons ci-après un exemple d'une seule ligne :
data_version=1 phonetic_name=null data_set=null phonetic_name_style=3 contact_id=61 sim_id=-1 lookup=3176r61-4B2F512F4937412F send_to_voicemail_sip=0 data12=null data11=3 data10=1 mimetype=vnd.android.cursor.item/name data15=null data14=null data13=null display_name_source=40 photo_uri=null data_sync1=null data_sync3=null data_sync2=null is_additional_number=0 contact_chat_capability=null data_sync4=null account_type=Local Phone Account account_type_and_data_set=Local Phone Account custom_ringtone=null photo_file_id=null has_phone_number=1 status=null data1=Séverine chat_capability=null data4=null data5=null data2=Séverine data3=null data8=null data9=null data6=null group_sourceid=null account_name=Phone data7=null display_name=Séverine raw_contact_is_user_profile=0 in_visible_group=1 display_name_alt=Séverine contact_status_res_package=null is_primary=0 contact_status_ts=null raw_contact_id=61 times_contacted=42 contact_status=null index_in_sim=-1 status_res_package=null status_icon=null contact_status_icon=null version=5 mode=null last_time_contacted=1711884045 timestamp=null res_package=null _id=121 name_verified=0 dirty=1 status_ts=null is_super_primary=0 photo_thumb_uri=null photo_id=null send_to_voicemail=0 send_to_voicemail_vt=0 name_raw_contact_id=61 contact_status_label=null status_label=null sort_key_alt=Séverine starred=0 indicate_phone_or_sim_contact=-1 sort_key=Séverine contact_presence=null sourceid=null is_sdn_contact=0
La lecture des données complètes associées à ce contact nécessite 4 lignes pour 2 numéro de telephone et une adresse courriel, soit 228 champs à analyser! En plus, Nous n'avons pas cherché ici à examiner les éventuelles données supplémentaires nommées "extras" et qui peuvent être associées à chaque ligne.
Fort heureusement, la documentation de la classe ContactsContract.Datadonne quelques renseignements utiles.
Le type de données stockée par une ligne est donné par la colonne MIMETYPE (dans l'exemple ci-dessus on voit que MIMETYPE=vnd.android.cursor.item/name. Ce type permet l'interprétation des colonnes DATA1 à DATA15. Il est à noter que DATA1 est une colonne indexée et doit donc être réservée à l'enregistrement de données fréquemment accédées. De plus et par convention, DATA15 est réservé au stockage d'un BLOB. Vous trouverez dans la documentation de la classe ContactsContract.Data (section COLUMNS pour la colonne MIMETYPE) la liste des types MIME pré-définis. Chacun d'eux est repéré par une constante symbolique associé à un line hypertexte qui renvoi sur la manière d'interpréter les données des colonnes DATA1 à DATA15.
Maintenant que nous en savons un peu plus, nous pouvons écrire une méthode chargée de lister les contacts et leur identifiant numérique. Pour cela, nous allons utiliser un curseur dans une forme un peu plus élaborée. Un curseur est une forme pré analysée de requête SQL à laquelle nous pouvons transmettre :
Nous donnons ci-après le code source d'une méthode qui retourne un dictionnaire dont chaque entrée et l'identifiant du contact auquel correspond son "nom d'affichage" (DISPLAY_NAME).
private HashMap<Integer, String> readContactNames()
{
HashMap<Integer, String> hmp = new HashMap<>();
ContentResolver crl = getContentResolver();
Uri uri = ContactsContract.Data.CONTENT_URI;
// Création des colonnes à projeter
String[] asProjection = new String[] {ContactsContract.Data._ID, ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME};
// Création de la clause WHERE
String sSelection = ContactsContract.Data.MIMETYPE + "='" + ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE +"'";
// Création du curseur
Cursor crs = crl.query(uri, asProjection, sSelection, null, null);
while (crs.moveToNext())
{
int iID = crs.getInt(0);
String sName = crs.getString(1);
hmp.put(iID, sName.trim().replaceAll("\n", " "));
}
return hmp;
}Rédaction par Jean-Marie Piatte (1983-2021)