Blog technique sur mes expériences de développeur.
31 août 2014
Comme le disait Zidane il y a quelques années dans une pub pour Canal Sat’ “Eh oui, c’est la reprise !”. Pour cette reprise, l’objectif est de faire des articles plus régulièrement sur ce blog. Soyons fous, on va tenter de publier un nouvel article par semaine !
Dans ce premier article de la rentrée, nous allons voir comment accéder aux différentes informations des contacts enregistrés sur un téléphone Android.
Cet article peut également vous intéresser : Accéder aux SMS depuis une application Android.
Il semble qu’il existe sur internet des centaines de façons de récupérer la liste des contacts du téléphone. Je te prétends nullement posséder la meilleure méthode, mais elle a le mérite de fonctionner.
La petite application d’exemple que nous allons développer au cours de ce tutoriel s’appuie sur la classe ContentResolver
. Cette classe permet, notamment, via des requêtes de demander des informations aux systèmes comment par exemple la liste des SMS ou la liste des contacts.
Attention : pour des raisons de lisibilité du code, tout sera écrit dans le thread de l’UI ce qui est bien évidemment une très mauvaise pratique !
Afin de récupérer la liste des contacts, nous allons donc devoir utiliser la classe ContentResolver
et plus précisément la méthode query
. Je vous propose de faire un petit tour sur la documentation officielle pour voir les paramètres que prend cette méthode.
Comme vous pouvez le constater, il existe deux signatures pour cette méthode. Intéressons-nous à celle qui accepte le moins de paramètres :
public final Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
Cette méthode accepte cinq paramètres. Dans le cadre de ce tutoriel, nous allons nous attarder uniquement sur les deux premiers :
Je vous propose de débuter en douceur en créant une méthode privée dans un Fragment
ou une Activity
qui accepte pour paramètre un objet de type ContentResolver
:
private void retrieveContacts(ContentResolver contentResolver)
{
}
Dans mon cas, cette méthode sera appelée depuis la méthode onCreateView
d’un Fragment
, aussi, elle sera appelée de la façon suivante : retrieveContacts(getActivity().getContentResolver());
. Je vous laisse bien évidemment faire les vérifications d’usage quant à l’appel de la méthode getActivity()
;) .
Dans le cadre de cette première version, nous allons nous contenter d’appeler la méthode query
avec le paramètre minimum, à savoir l’URI. L’URI permettant de récupérer les contacts est accessible via la constante ContactsContract.Contacts.CONTENT_URI
.
Notre appel à la méthode query
ressemble donc à ça :
contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
Complétons notre méthode retrieveContacts
en exploitant le résultat de la méthode query
qui est un cursor :
private void retrieveContacts(ContentResolver contentResolver)
{
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
}
Avant d’exploiter le cursor
, je vous propose de vérifier qu’il n’est pas null
. Si c’est le cas, nous quittons la méthode :
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
Dans le cas contraire, nous allons vérifier qu’il contient au moins un résultat en le déplaçant sur son premier élément :
if (cursor.moveToFirst() == true)
{
}
S’il contient au moins un élément, nous allons parcourir tous les éléments à l’aide d’une boucle do...while
:
if (cursor.moveToFirst() == true)
{
do
{
}
while (cursor.moveToNext() == true);
}
Finalement, nous allons, avant de quitter notre méthode retrieveContacts
, fermer le cursor
:
if (cursor.isClosed() == false)
{
cursor.close();
}
Voici ce à quoi doit ressembler la méthode retrieveContacts
pour le moment :
private void retrieveContacts(ContentResolver contentResolver)
{
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
if (cursor.moveToFirst() == true)
{
do
{
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
}
Il convient donc de compléter la boucle do...while
afin d’exploiter réellement les éléments du cursor
. Pour ça, nous allons devoir exploiter l’une des méthodes suivantes du cursor
: getDouble()
, getFloat()
, getInt()
, getLong()
, getShort()
, getString()
. Chacune de ces méthodes prend un seul paramètre qui correspond à l’index de la colonne dont on souhaite récupérer l’information.
Par exemple, si je souhaite récupérer le nom du contact voici ce que l’on doit écrire :
cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
Par exemple, si je souhaite récupérer l’identifiant du contact, voici ce que l’on doit écrire :
cursor.getLong(cursor.getColumnIndex(ContactsContract.Data._ID));
La liste complète des constantes est disponible dans la documentation officielle.
Je vous propose alors de compléter notre méthode retrieveContacts
en affichant dans le logcat l’identifiant et le nom des contacts :
private void retrieveContacts(ContentResolver contentResolver)
{
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = cursor.getLong(cursor.getColumnIndex(ContactsContract.Data._ID));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
Log.d("retrieveContacts", "The contact with id + '" + id + "' and name '" + name + "' has been retrieved");
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
}
Si vous exécutez l’application Android, vous allez vous rendre compte que tout fonctionne, mais on peut déjà y apercevoir deux défauts :
Pour le premier point négatif, nous allons pouvoir procéder à une petite optimisation. Comme je vous le disais un peu plus haut, il est possible d’optimiser un peu l’appel à la méthode query
du ContentResolver
en précisant en filtrant les colonnes que nous souhaitons récupérer. Dans le cadre de notre première version de la méthode retrieveContacts
, nous nous intéressons uniquement à deux colonnes : ContactsContract.Data._ID
et ContactsContract.Data.DISPLAY_NAME
.
C’est pourquoi, notre appel à la méthode query
peut être légèrement modifié :
contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data._ID}, null, null, null);
Le reste ne changeant pas, voici ce à quoi ressemble la première version de notre méthode retrieveContacts
:
private void retrieveContacts(ContentResolver contentResolver)
{
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data._ID}, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = cursor.getLong(cursor.getColumnIndex(ContactsContract.Data._ID));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
Log.d("retrieveContacts", "The contact with id + '" + id + "' and name '" + name + "' has been retrieved");
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
}
Dans le cadre de la deuxième version de notre méthode retrieveContacts
nous allons nous attaquer au problème lié au fait que l’on récupère bien plus de contacts que ceux réellement dans le téléphone. Malheureusement je n’ai pas solution miracle. C’est pourquoi, dans le cadre de cet article, nous allons récupérer uniquement le nom des contacts qui possèdent au moins un numéro de téléphone.
Pour savoir si un contact possède un numéro de téléphone, il convient d’accéder à la colonne du cursor
dont l’index est donné par la constante ContactsContract.Data.HAS_PHONE_NUMBER
. Nous allons donc devoir modifier notre méthode query
pour ajouter cette information dans la liste des champs auxquels nous souhaitons accéder. Nous allons également devoir exploiter le résultat de ce nouveau champ. C’est assez simple, on récupère un entier. S’il est supérieur à zéro, le contact possède un numéro de téléphone.
Voici alors ce à quoi pourrait ressembler notre méthode retrieveContacts
dans sa version 2 :
private void retrieveContacts(ContentResolver contentResolver)
{
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data._ID, ContactsContract.Contacts.HAS_PHONE_NUMBER }, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = cursor.getLong(cursor.getColumnIndex(ContactsContract.Data._ID));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
final int hasPhoneNumber = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data.HAS_PHONE_NUMBER));
if (hasPhoneNumber > 0)
{
Log.d("retrieveContacts", "The contact with id + '" + id + "' and name '" + name + "' has been retrieved");
}
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
}
Si vous exécutez la méthode, vous allez alors vous rendre compte qu’il y a encore quelques doublons au niveau des ContactsContract.Data.DISPLAY_NAME
. Ces doublons sont tout simplement dû au fait qu’un contact peut avoir plusieurs numéros de téléphone. Pour chacun de ces numéros, le même ContactsContract.Data.DISPLAY_NAME
est utilisé. Nous allons donc modifier notre méthode et supprimer ces doublons à l’aide d’un HashSet
. Pour rappel, un HashSet
est une implémentantation de l’interface Set
qui est une structure de données ne supportant pas les doublons.
Voici ce à quoi pourrait ressembler notre méthode :
private void retrieveContacts(ContentResolver contentResolver)
{
final Set<String> contacts = new HashSet<String>();
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data._ID, ContactsContract.Contacts.HAS_PHONE_NUMBER }, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = Long.parseLong(cursor.getString(cursor.getColumnIndex(ContactsContract.Data._ID)));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
final int hasPhoneNumber = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data.HAS_PHONE_NUMBER));
if (hasPhoneNumber > 0)
{
contacts.add(name);
}
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
for (final String contact : contacts)
{
Log.d("retrieveContacts", "The contact '" + contact + "' has been retrieved");
}
}
Maintenant que nous sommes au point avec notre liste de contacts, je vous propose de les afficher dans une ListView
. Pour celà, nous allons devoir modifier notre méthode pour qu’elle renvoie maintenant notre liste de contacts sous la forme d’un ArrayList
.
La première étape consiste donc à modifier notre méthode retrieveContacts
. Dorénavant, sa signature est la suivante :
private List<String> retrieveContacts(ContentResolver contentResolver)
{
}
Celà nous oblige à retourner un résultat à deux endroits de la fonction :
if
qui vérifie que le cursor
n’est pas null
;Pour le if
allons au plus simple et renvoyons null
:
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return null;
}
Pour le renvoie en fin de méthode, la seule difficulté est de transformer notre HashSet
en ArrayList
. Mais le constructeur d’un ArrayList
acceptant une collection, la difficulté est très rapidement contournée :
return new ArrayList<String>(contacts);
Finalement, après avoir ajouté une petite ligne permettant de trier les contacts, voici ce à quoi ressemble notre méthode retrieveContacts
:
private List<String> retrieveContacts(ContentResolver contentResolver)
{
final Set<String> contacts = new HashSet<String>();
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data._ID, ContactsContract.Contacts.HAS_PHONE_NUMBER }, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return null;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = Long.parseLong(cursor.getString(cursor.getColumnIndex(ContactsContract.Data._ID)));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
final int hasPhoneNumber = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data.HAS_PHONE_NUMBER));
if (hasPhoneNumber > 0)
{
contacts.add(name);
}
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
final List<String> sortedContacts = new ArrayList<String>(contacts);
Collections.sort(sortedContacts);
return sortedContacts;
}
Pour afficher les contacts, nous allons utiliser une ListView
avec un ArrayAdapter
. Voici alors ce à quoi ressemble le layout du Fragment
:
<ListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
Finalement, voici ce à quoi ressemble la méthode onCreateView
du Fragment
qui affiche la liste des contacts :
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
final ListView list = (ListView) rootView.findViewById(android.R.id.list);
final List<String> contacts = retrieveContacts(getActivity().getContentResolver());
if (contacts != null)
{
list.setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, contacts));
}
return rootView;
}
Attention : une fois de plus je tiens à rappeler que je vais au plus simple ! Il ne faut surtout pas tout faire dans le thread de l’UI !
Après exécution de l’application, vous devriez avoir quelque chose comme ça :
Pour terminer cet article, je vous propose de modifier notre application pour qu’elle puisse également afficher la photo du contact. Nous allons donc devoir faire quelques modifications à notre petit programme !
Avant de modifier la méthode retrieveContacts
, il convient d’écrire une petite méthode permettant de récupérer la photo d’un contact en fonction de son identifiant. Voici la signature de la méthode getPhoto
:
private Bitmap getPhoto(ContentResolver contentResolver, long contactId)
{
}
Cette méthode prend donc un contentResolver
ainsi que l’identifiant du contact dont on souhaite récupérer la photo.
Dans cette méthode, nous allons une nouvelle fois utiliser la méthode query
du contentResolver
, mais cette fois, la construction de l’URI est un peu plus compliquée car construite en deux temps. Dans un premier temps, nous allons devoir dire que nous souhaitons récupérer les informations relatives à un contact spécifique à l’aide de la méthode statique withAppendedId
de la classe ContentUris
:
final Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
Nous allons ensuite spécifier que c’est aux photos que nous souhaitons accéder via la méthode statique withAppendedPath
de la classe Uri
:
final Uri photoUri = Uri.withAppendedPath(contactUri, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
Finalement, nous allons limiter notre recherche au champ ContactsContract.Contacts.Photo.DATA15
qui correspond à la photo de profile :
final Cursor cursor = contentResolver.query(photoUri, new String[] { ContactsContract.Contacts.Photo.DATA15 }, null, null, null);
Nous allons donc pouvoir exploiter le résultat, mais avant ça, vérifions que le cursor
n’est pas null
:
if (cursor == null)
{
Log.e("getPhoto", "Cannot retrieve the photo of the contact with id '" + contactId + "'");
return null;
}
Nous allons maintenant vérifier que le cursor
possède au moins un élément en le déplaçant dessus. S’il possède bien un élément, nous allons en conclure qu’il s’agit de la photo que nous voulons. Nous allons alors récupérer le tableau de byte
qui compose la photo à l’aide de la méthode getBlob
du cursor
. Finalement, nous allons la transformer en Bitmap
:
if (cursor.moveToFirst() == true)
{
final byte[] data = cursor.getBlob(0);
if (data != null)
{
final Bitmap photo = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
}
}
Finalement, avant de retourner notre photo, il convient de fermer le cursor
:
if (cursor.isClosed() == false)
{
cursor.close();
}
Finalement, voici la méthode complète :
private Bitmap getPhoto(ContentResolver contentResolver, long contactId)
{
Bitmap photo = null;
final Uri contactUri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
final Uri photoUri = Uri.withAppendedPath(contactUri, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
final Cursor cursor = contentResolver.query(photoUri, new String[] { ContactsContract.Contacts.Photo.DATA15 }, null, null, null);
if (cursor == null)
{
Log.e("getPhoto", "Cannot retrieve the photo of the contact with id '" + contactId + "'");
return null;
}
if (cursor.moveToFirst() == true)
{
final byte[] data = cursor.getBlob(0);
if (data != null)
{
photo = BitmapFactory.decodeStream(new ByteArrayInputStream(data));
}
}
if (cursor.isClosed() == false)
{
cursor.close();
}
return photo;
}
Nous allons maintenant modifier notre méthode retrieveContacts
pour qu’elle ne renvoie plus une Collection
de String
, mais une Collection
de Map
. En effet, pour afficher nos contacts, nous utiliserons un SimpleAdapter
. Nous allons également lui faire consommer notre méthode getPhoto
fraîchement écrite.
Avant toute chose, voici la nouvelle signature de la méthode retrieveContacts
:
private List<Map<String, Object>> retrieveContacts(ContentResolver contentResolver)
{
}
Nous allons donc devoir alimenter la liste à retourner. Avant toute chose, il convient de modifier le type de notre liste de contacts :
final List<Map<String, Object>> contacts = new ArrayList<Map<String, Object>>();
Pour le reste tout se passe dans la boucle do...while
. Pour chaque contact, on récupère le nom et la photo que l’on stocke dans une Map
, puis on stocke la Map
dans notre variable contacts
:
do
{
final long id = Long.parseLong(cursor.getString(cursor.getColumnIndex(ContactsContract.Data._ID)));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
final int hasPhoneNumber = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data.HAS_PHONE_NUMBER));
if (hasPhoneNumber > 0)
{
final Bitmap photo = getPhoto(contentResolver, id);
final Map<String, Object> contact = new HashMap<String, Object>();
contact.put("name", name);
contact.put("photo", photo);
contacts.add(contact);
}
}
while (cursor.moveToNext() == true);
Voici alors ce à quoi ressemble la méthode complète :
private List<Map<String, Object>> retrieveContacts(ContentResolver contentResolver)
{
final List<Map<String, Object>> contacts = new ArrayList<Map<String, Object>>();
final Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, new String[] { ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data._ID, ContactsContract.Contacts.HAS_PHONE_NUMBER }, null, null, null);
if (cursor == null)
{
Log.e("retrieveContacts", "Cannot retrieve the contacts");
return null;
}
if (cursor.moveToFirst() == true)
{
do
{
final long id = Long.parseLong(cursor.getString(cursor.getColumnIndex(ContactsContract.Data._ID)));
final String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME));
final int hasPhoneNumber = cursor.getInt(cursor.getColumnIndex(ContactsContract.Data.HAS_PHONE_NUMBER));
if (hasPhoneNumber > 0)
{
final Bitmap photo = getPhoto(contentResolver, id);
final Map<String, Object> contact = new HashMap<String, Object>();
contact.put("name", name);
contact.put("photo", photo);
contacts.add(contact);
}
}
while (cursor.moveToNext() == true);
}
if (cursor.isClosed() == false)
{
cursor.close();
}
return contacts;
}
Attention : il y a une régression (volontaire) ! Ici, il y a un retour des doublons et les contacts ne sont plus triés ! Je vous laisse prendre les mesures nécessaires à la correction de ce problème ;) !
Il est temps de conclure par l’affichage des contacts ! La première étape consiste à créer le layout d’un contact qui permettra d’afficher la photo et le nom du contact. Voici ce que vous pourriez faire :
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:id="@+id/photo"
android:layout_width="150dip"
android:layout_height="150dip"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
/>
<TextView
android:id="@+id/name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dip"
/>
</LinearLayout>
Nous allons donc modifier la méthode onCreateView
du Fragment
pour utiliser maintenant un SimpleAdapter
:
final List<Map<String, Object» contacts = retrieveContacts(getActivity().getContentResolver());
if (contacts != null)
{
final SimpleAdapter adapter = new SimpleAdapter(getActivity(), contacts, R.layout.contact, new String[] { "name", "photo" }, new int[] { R.id.name,
R.id.photo });
list.setAdapter(adapter);
}
Le problème c’est que si on s’arrête là, ça ne fonctionnera pas ! En effet, nous allons devoir écrire un ViewBinder
permettant à notre SimpleAdapter
d’afficher correctement les photos. Son code étant relativement simple, je vous le fournis directement :
adapter.setViewBinder(new ViewBinder()
{
@Override
public boolean setViewValue(View view, Object data, String textRepresentation)
{
if ((view instanceof ImageView) & (data instanceof Bitmap))
{
final ImageView image = (ImageView) view;
final Bitmap photo = (Bitmap) data;
image.setImageBitmap(photo);
return true;
}
return false;
}
});
Voici à quoi ressemble la méthode onCreateView
entière :
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout.fragment_main, container, false);
final ListView list = (ListView) rootView.findViewById(android.R.id.list);
final List<Map<String, Object>> contacts = retrieveContacts(getActivity().getContentResolver());
if (contacts != null)
{
final SimpleAdapter adapter = new SimpleAdapter(getActivity(), contacts, R.layout.contact, new String[] { "name", "photo" }, new int[] { R.id.name,
R.id.photo });
adapter.setViewBinder(new ViewBinder()
{
@Override
public boolean setViewValue(View view, Object data, String textRepresentation)
{
if ((view instanceof ImageView) & (data instanceof Bitmap))
{
final ImageView image = (ImageView) view;
final Bitmap photo = (Bitmap) data;
image.setImageBitmap(photo);
return true;
}
return false;
}
});
list.setAdapter(adapter);
}
return rootView;
}
Si vous exécutez l’application, vous devriez alors voir quelque chose comme ça à l’écran de votre téléphone :
Comme vous pouvez le constater, les photos des contacts qui en ont une sont bien affichées à l’écran !
Une dernière fois j’insiste sur le fait que la méthode retrieveContacts
ne doit absolument pas être appelé dans le thread de l’UI !