Accueil / Articles PiApplications. / La plate-forme Android

Le modèle des contacts.

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.

Description sommaire du modèle.

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.

Utilisation du modèle.

Il y a deux façons d'accéder aux contacts :

  1. en utilisant l'application des contacts propre à Android par le bias d'une "intention" ;
  2. en accédant directement à la base de données en faisant de l'application un "client" de cette dernière.

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;
  }

(c) PiApplications 2016