Tutoriel Android : Apprendre à utiliser un fournisseur de contenu avec les ContentProvider

Chapitre 5

Android2ee

Mathias Séguy

Un fournisseur de contenu (ContentProvider) est un objet à qui on passe une URI (Unified Resource Identifier) et qui renvoie un flux de données. À partir d'une URI, on peut effectuer les opérations CRUD sur les objets récupérés.

- Chapitre 0 : Installation et préparation de l'environnement de développement

- Chapitre 1 : Premiers pas avec Android

- Chapitre 1.1 : Cycle de vie de l'activité

- Chapitre 2.1 : Les éléments graphiques fondamentaux

- Chapitre 2.2 : Composants graphiques complexes

- Chapitre 3.3 : Intent et IntentFilters

- Chapitre 4.1 : Les Ressources

- Chapitre 4.2 : SharedPreferences et PreferenceActivity

- Chapitre 4.3 : Base de données SQLite

- Chapitre 4.4 : Gestion des fichiers

- Chapitre 5 : Content Provider

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum : Commentez

- Titre : Tutoriel Android : Apprendre à utiliser un fournisseur de contenu avec les ContentProvider

- Auteur : Mathias Séguy

- Parution : 27 octobre 2016

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2016 Mathias Séguy. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Mon application est un ContentProvider (fournisseur de contenu)

- I-A. Utilisation d'un ContentProvider

- I-A-1. Récupérer des données

- I-A-2. Insérer des données

- I-A-3. Supprimer des données

- I-A-4. Mettre à jour des données

- I-A-5. Les ContentProviders Android

- I-A-5-a. La classe ContactsContract

- I-A-5-b. La classe MediaStore

- I-B. Définition d'un ContentProvider

- I-B-1. Création de MyProvider extends ContentProvider

- I-B-1-a. Les Constantes implémentent BaseColumns

- I-B-1-b. onCreate

- I-B-1-c. getType

- I-B-1-d. query

- I-B-1-e. Insert

- I-B-1-f. Update

- I-B-1-g. Delete

- I-B-1-h. Manifest

- I-B-1-i. Mise en place de listeners de modifications

- I-B-1-j. Exemple avec une base de données

- II. Remerciements

I - Mon application est un ContentProvider (fournisseur de contenu)

Un fournisseur de contenu est un objet à qui on passe une URI (Unified Resource Identifier) et qui renvoie un flux de données. À partir d'une URI, on peut effectuer les opérations CRUD sur les objets récupérés.

Ce chapitre se scinde en deux parties. La première consiste à décrire leur utilisation. La seconde à décrire comment en construire un.

I-A - Utilisation d'un ContentProvider

Une URI (Android) est construite de plusieurs morceaux qui, comme pour le chemin absolu d'un fichier, se décompose comme suit :

- content://identifiant d'une URI de type contenu ;

- espace de nom des données (ex. : net.stinfoservices.grh ou com.android.contacts/contacts) ;

- identifiant de l'instance de l'objet (ex. : 5).

Ainsi, content://com.android.contacts/contacts représente une liste d'instances de contacts et content://com.android.contacts/contacts/5 représente le contact dont l'identifiant est 5.

La méthode statique Uri.parse permet de construire un objet URI à partir d'une chaîne de caractères.

Les fournisseurs définissent leurs URI en tant que constante publique de leur classe, CONTENT_URI, ce qui est une bonne pratique.

À partir de cette URI, il est alors possible d'obtenir les données du fournisseur associé. Ce mécanisme d'appel est très proche du SQL. Deux façons de l'aborder sont possibles :

- L'une est une méthode de la classe Activity et permet simplement d'accéder aux données, manageQuery().

- L'autre est plus adaptée à la manipulation des ContentProvider et passe par la classe ContentResolver et sa méthode query().

La deuxième solution est celle qui est préconisée, car avec l'objet ContentResolver, il est possible de consulter, de mettre à jour ou de détruire les données. Ces deux méthodes prennent en paramètres les mêmes éléments.

I-A-1 - Récupérer des données

1. ContentResolver cr = getContentResolver();

2. cr.query(uri, projection, selection, selectionArgs, sortOrder)

Ou bien

1. managedQuery(uri, projection, selection, selectionArgs, sortOrder)

Où les paramètres sont :

- l'URI des données du ContentProvider ;

- une projection listant les colonnes à rapatrier ;

- une clause where avec des jokers (?) facultatifs ;

- un tableau de paramètres remplaçant les jokers (?) ;

- une clause Ordrer by.

Ce qui donne, par exemple :

1. ContentResolver cr= getContentResolver();

2. //Pour ramener toutes les lignes

3. cr.query(MyProvider.CONTENT_URI, null, null, null, null);

4. //Pour ramener un sous-ensemble :

5. String[] projection = {Key_ColHuman_FirstName, Key_ColHuman_Name };

6. String where = Key_ColHuman_EyesColor + "=? AND" + Key_ColHuman_HairColor + "=?";

7. String[] whereParam = {"blue","brown"};

8. String order = Key_ColHuman_Name;

9. cr.query(MyProvider.CONTENT_URI, projection, where, whereParam, order);

I-A-2 - Insérer des données

Il faut utiliser les méthodes insert ou bulkInsert, l'une ajoute un élément, l'autre un tableau d'éléments. L'une renvoie l'URI de la donnée insérée, l'autre le nombre de données insérées.

1. ContentResolver cr = getContentResolver();

3. //Pour insérer une ligne

4. ContentValues newLine = new ContentValues();

5. newLine.put(Column_Name, newValue);

6. Uri myDataURI=cr.insert(MyProvied.CONTENT_URI, newLine);

8. //Pour insérer n lignes

9. ContentValues[]newLines=new ContentValues[5];

10. //Remplir le tableau… et insérer les valeurs

11. Int insertedLines=cr.bulkInsert(MyProvied.CONTENT_URI, newLines);

Javadoc :

1. public final Uri insert (Uri url, ContentValues values)

Inserts a row into a table at the given URL. If the content provider supports transactions the insertion will be atomic.

Parameters

url

The URL of the table to insert into.

values

The initial values for the newly inserted row. The key is the column name for the field. Passing an empty ContentValues will create an empty row.

Returns the URL of the newly created row.

1. public final int bulkInsert (Uri url, ContentValues[] values)

Inserts multiple rows into a table at the given URL. This function make no guarantees about the atomicity of the insertions.

Parameters

url

The URL of the table to insert into.

values

The initial values for the newly inserted rows. The key is the column name for the field. Passing null will create an empty row.

Returns the number of newly created rows.

I-A-3 - Supprimer des données

Il faut utiliser la méthode delete :

1. ContentResolver cr = getContentResolver();

2. //Pour supprimer une ligne connue

3. cr.delete(myRowURI,null,null);

4. //Pour supprimer un sous-ensemble :

5. String where = Key_ColHuman_EyesColor+"=? AND"+ Key_ColHuman_HairColor+"=?";

6. String[] whereParam = {"blue","brown"};

7. cr.delete(MyProvider.CONTENT_URI, where, whereParam);

Javadoc :

1. public final int delete (Uri url, String where, String[] selectionArgs)

Deletes row(s) specified by a content URI. If the content provider supports transactions, the deletion will be atomic.

Parameters

url

The URL of the row to delete.

where

A filter to apply to rows before deleting, formatted as an SQL WHERE clause (excluding the WHERE itself).

Returns The number of rows deleted.

I-A-4 - Mettre à jour des données

Il faut utiliser la méthode update :

1. ContentResolver cr = getContentResolver();

2. ContentValues newLineValues = new ContentValues();

3. //Pour mettre à jour une ligne connue

4. cr.update(myRowURI, newLineValues, null, null);

5. //Pour mettre à jour un sous-ensemble :

6. String where = Key_ColHuman_EyesColor + "=? AND"+ Key_ColHuman_HairColor + "=?";

7. String[] whereParam = {"blue","brown"};

8. cr.update(MyProvider.CONTENT_URI, newLineValues, where, whereParam);

Javadoc :

1. public final int update (Uri uri, ContentValues values, String where, String[] selectionArgs)

Update row(s) in a content URI. If the content provider supports transactions the update will be atomic.

Parameters

uri

The URI to modify.

values

The new field values. The key is the column name for the field. A null value will remove an existing field value.

where

A filter to apply to rows before updating, formatted as an SQL WHERE clause (excluding the WHERE itself).

Returns The number of rows updated.

Throws NullPointerException

if uri or values are null

I-A-5 - Les ContentProviders Android

Les URI suivantes sont disponibles sur tous les systèmes Android :

- Browser.BOOKMARKS_URI pour la gestion des favoris du navigateur web ;

- Browser.SEARCHES_URI pour la gestion des recherches du navigateur web ;

- CallLog.CONTENT_URI pour la gestion des appels entrants, sortants ou manqués avec l'identifiant du numéro et la durée de l'appel ;

- Settings.CONTENT_URI pour la gestion des préférences de l'appareil, mais il est plus pertinent de faire appel aux Intents de cette classe pour laisser l'utilisateur modifier lui-même ses préférences ;

- UserDictionary.CONTENT_URI pour la gestion des mots définis par l'utilisateur ;

- UserDictionary.Words.CONTENT_URI pour la gestion des mots définis par l'utilisateur.

Deux autres classes possèdent un ensemble d'URI permettant une gestion fine de leurs entités. Les URI sont les suivantes, à vous de les découvrir et de chercher leur utilisation la plus pertinente.

I-A-5-a - La classe ContactsContract

La classe ContactsContract qui permet de gérer les Contacts :

- ContactsContract.AUTHORITY_URI ;

- ContactsContract.Contacts.CONTENT_URI ;

- ContactsContract.Contacts.CONTENT_FILTER_URI ;

- ContactsContract.Contacts.CONTENT_GROUP_URI ;

- ContactsContract.Contacts.CONTENT_LOOKUP_URI ;

- ContactsContract.Contacts.CONTENT_STREQUENT_FILTER_URI ;

- ContactsContract.Contacts.CONTENT_STREQUENT_URI ;

- ContactsContract.Contacts.CONTENT_VCARD_URI ;

- ContactsContract.AggregationExceptions.CONTENT_URI ;

- ContactsContract.Data.CONTENT_URI ;

- ContactsContract.Groups.CONTENT_SUMMARY_URI ;

- ContactsContract.Groups.CONTENT_URI ;

- ContactsContract.PhoneLookup.CONTENT_FILTER_URI ;

- ContactsContract.RawContacts.CONTENT_URI ;

- ContactsContract.RawContactsEntity.CONTENT_URI ;

- ContactsContract.Settings.CONTENT_URI ;

- ContactsContract.StatusUpdates.CONTENT_URI ;

- ContactsContract.SyncState.CONTENT_URI.

Quelques exemples d'utilisation du ContractsContact :

Récupérer tous les numéros de téléphone et les adresses mail de vos contacts :

1. //Récupérer la liste des contacts

2. Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

3. while (cursor.moveToNext()) {

4. String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));

5. String hasPhone = cursor.getString(cursor.getColumnIndex(ContactsContract.

6. Contacts.HAS_PHONE_NUMBER));

7. if (Boolean.parseBoolean(hasPhone)) {

8. // You know have the number so now query it like this

9. Cursor phones = getContentResolver().query(

10. ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,

11. ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null, null);

12. while (phones.moveToNext()) {

13. String phoneNumber = phones.getString(phones

14. .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));

15. }

16. phones.close();

17. }

19. Cursor emails = getContentResolver().query(

20. ContactsContract.CommonDataKinds.Email.CONTENT_URI, null,

21. ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + contactId, null, null);

22. while (emails.moveToNext()) {

23. // This would allow you get several email addresses

24. String emailAddress = emails.getString(emails

25. .getColumnIndex(ContactsContract.CommonDataKinds.CommonDataColumns.DATA));

26. }

27. emails.close();

28. }

29. cursor.close();

Récupérer tous les numéros de téléphone et les noms de vos contacts :

1. Cursor people = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

2. while (people.moveToNext()) {

3. int nameFieldColumnIndex = people.getColumnIndex(PhoneLookup.DISPLAY_NAME);

4. String contact = people.getString(nameFieldColumnIndex);

5. int numberFieldColumnIndex = people.getColumnIndex(PhoneLookup.NUMBER);

6. String number = people.getString(nameFieldColumnIndex);

7. }

8. people.close();

Ou pour récupérer les notes d'un contact :

1. private String getNote(long contactId) {

2. String note = null;

3. String[] columns = new String[] { ContactsContract.CommonDataKinds.Note.NOTE };

4. String where = ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";

5. String[] whereParameters = new String[] { Long.toString(contactId), ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE };

6. Cursor contacts = getContentResolver().query(ContactsContract.Data.CONTENT_URI, projection, where, whereParameters, null);

7. if (contacts.moveToFirst()) {

8. note = contacts.getString(0);

9. }

10. contacts.close();

11. return note;

12. }

I-A-5-b - La classe MediaStore

La classe MediaStore qui permet la gestion du contenu multimédia de l'appareil. Chaque URI se décline en Internal ou External (internal ou external storage), l'une pour les ressources internes de l'appareil, l'autre pour les ressources externes.

- MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI ;

- MediaStore.Audio.Albums.INTERNAL_CONTENT_URI ;

- MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI ;

- MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI ;

- MediaStore.Audio.Media.EXTERNAL_CONTENT_URI ;

- MediaStore.Audio.Playlists.EXTERNAL_CONTENT_URI ;

- MediaStore.Images.Media.EXTERNAL_CONTENT_URI ;

- MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI ;

- MediaStore.Video.Media.EXTERNAL_CONTENT_URI ;

- MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI ;

- MediaStore.getMediaScannerUri() pour la gestion des différents médias.

Un exemple d'utilisation de MediaStore pour récupérer la liste des chansons du lecteur externe :

1. Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;

2. String[] projection = { MediaStore.Audio.Media._ID,           // 0

3. MediaStore.Audio.Media.ARTIST,        // 1

4. MediaStore.Audio.Media.TITLE,         // 2

5. MediaStore.Audio.Media.ALBUM_ID,      // 3

6. MediaStore.Audio.Media.ALBUM,         // 4

7. MediaStore.Audio.Media.DATA,          // 5

8. MediaStore.Audio.Media.DISPLAY_NAME,  // 6

9. MediaStore.Audio.Media.DURATION };    // 7

11. String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";

13. Cursor cursor = query(c,media,projection,selection,null,null);

14. while(cursor.moveToNext()){

15. songs.add(new Music(cursor.getString(1),

16. cursor.getString(2),

17. cursor.getString(4),

18. cursor.getString(7),

19. cursor.getString(5),

20. cursor.getLong(0),

21. cursor.getLong(3)));

22. }

23. return songs;

I-B - Définition d'un ContentProvider

Construire un fournisseur de contenu est une activité qui se doit d'être très structurée et de répondre à un cahier des charges très strict.

Tout d'abord, la définition de votre (vos) URI. Elle se compose de la manière suivante : content://MyAuthority/pathToData/data, où :

- MyAuthority est le « nom » de votre fournisseur de contenu ;

- pathToData peut être vide ou pas (on peut avoir une arborescence pour organiser ses données) ;

- data pointe sur une instance de l'objet.

Ainsi, vous pouvez avoir des URI à rallonge ou toutes simples, cela dépend de vos données et de la structuration que vous mettez en place. Android préconise de mettre comme Authority le package de la classe qui implémente votre provider pour assurer l'unicité. Pour une URI qui possède des sous-tables, l'utilisation des pathToData est obligatoire, elle assure l'unicité de l'Authority.

Vous devez toujours mettre vos URI en final static URI CONTENT_URI pour permettre à vos utilisateurs d'avoir une visibilité simple sur celles-ci. Si vous avez plusieurs URI, faites comme ContactsContract et mettez en place des sous-classes publiques à votre fournisseur et pour chacune, déclarez le CONTENT_URI correspondant.

L'exemple d'Android est parfait :

Où :

1. Est le préfixe standard qui indique que les données sont gérées par un ContentProvider ;

2. L'Authority du ContentProvider, elle définit ce provider. Elle se retrouve dans votre manifest.xml ainsi que dans votre classe sous la constante CONTENT_URI. Par convention, elle est le package de votre provider (pour assurer l'unicité) ;

3. Le chemin utilisé par le content provider pour déterminer quel type de données est demandé ;

4. L'identifiant (the ID) d'un enregistrement spécifique.

Chaque URI est mappée à un type MIME. Ainsi, une URI qui pointe vers un ensemble de données aura pour type dir, et une URI qui pointe vers une donnée aura pour type item. Pour être plus précis, le premier aura un type vnd.X.cursor.dir/Y, le second un type vnd.X.cursor.item/Y où :

- X est le nom du projet (un seul nom, pas de slash, pas de point ; si celui-ci est spécifique à Android, pourquoi ne pas mettre android ?) ;

- Y est de type vnd.myCompany.contentType (vnd.net.stinfoservces.employee).

Dans la suite de ce chapitre, nous nous appuyons sur l'exemple où vous exposez des données nommées MyData au moyen d'un ContentProvider appelé MyDataProvider.

Enfin, toutes vos données ont un identifiant (ID). En d'autres termes, chacune de vos données exposées possède un champ ID. Si vous travaillez avec une base de données, chacune de vos tables possède donc une colonne ID (qui est de type Integer et s'auto-incrémente).

I-B-1 - Création de MyProvider extends ContentProvider

Étendre la classe ContentProvider impose l'implémentation des six méthodes suivantes :

- onCreate ;

- getType ;

- query ;

- insert ;

- update ;

- delete.

I-B-1-a - Les Constantes implémentent BaseColumns

Votre fournisseur de données se doit d'exposer toutes ces constantes utiles. Au lieu de les exposer en tant que constantes simples de votre classe, une bonne pratique est de les encapsuler dans une classe public static qui implémente BaseColumns. Vous pouvez appeler cette classe du nom de vos données ou juste Constant. C'est une bonne pratique, mais personne ne vous en voudra si vous les mettez à la racine de votre provider.

1. public class MyDataProvider extends ContentProvider {

2. // Mon Authority, mes paths to data et mon URI

3. // The Authority

4. public static final String AUTHORITY = "myAuthority";//package de la classe provider est préconisé (utilise des .)

5. // The path to the data… and explain

6. public static final String PATH_TO_DATA = "myPath"; //Vous pouvez déclarer plusieurs paths (les paths utilisent les /)

8. // This class aims to show the constant to use for the MyDataProvider

10. public class MyDatas extends BaseColumns {

12. // The URI and explain, with example if you want

13. public static final URI CONTENT_URI = URI.parse("content://" + MyDataProvider.AUTHORITY + "/" + MyDataProvider.PATH_TO_DATA);

15. // Mon MIME type

16. // My Column ID and the associated explanation

17. public static final String MIME_COLLECTION = "vnd.android.cursor.dir/vnd.myCompany.contentType"

19. // My Column ID and the associated explanation

20. public static final String MIME_ITEM = "vnd.android.cursor.item/vnd.myCompany.contentType"

22. // Noms de colonnes

23. //  /!\Si vous utilisez une base de données, les noms des colonnes sont les mêmes que ceux de votre base, de même pour les index.

25. // My Column ID and the associated explanation for end-users

26. public static final String KEY_COL_ID = "_id"; // Mandatory

28. // My Column Name and the associated explanation for end-users

29. public static final String KEY_ COL_NAME = "name";

31. // My Column First Name and the associated explanation for end-users

32. public static final String KEY_ COL_FIRSTNAME = "firstName";

34. // My Column Eyes Color and the associated explanation for end-users

35. public static final String KEY_ COL_EYES_COLOR = "eyesColor";

37. // My Column Hair color and the associated explanation for end-users

38. public static final String KEY_ COL_HAIR_COLOR = "hairColor";

40. // My Column age and the associated explanation for end-users

41. public static final String KEY_ COL_AGE = "age";

43. // Index des colonnes

45. // The index of the column ID

46. public static final int ID_COLUMN = 1;

47. // The index of the column NAME

48. public static final int NAME_COLUMN = 2;

49. // The index of the column FIRST NAME

50. public static final int FIRSTNAME_COLUMN = 3;

51. // The index of the column EYES COLOR

52. public static final int EYES_COLOR_COLUMN = 4;

53. // The index of the column HAIR COLOR

54. public static final int HAIR_COLOR_COLUMN = 5;

55. // The index of the column AGE

56. public static final int AGE_COLUMN = 6;

57. }

58. ...

59. }

I-B-1-b - onCreate

Vous devez mettre en place l'initialisation de votre fournisseur de données dans cette classe. En d'autres termes, vous initialisez l'accès à vos données : soit vers votre base de données, soit vers un fichier, un flux RSS, un site web…

I-B-1-c - getType

Vous devez renvoyer le type associé à l'URI (et donc collection ou item).

1. public String getType(Uri uri) {

2. switch (uriMatcher.match(uri)) {

3. case COLLECTION: return "vnd.android.cursor.dir/vnd.myCompany.contentType ";

4. case ITEM: return "vnd.android.cursor.item/vnd.myCompany.contentType ";

5. default: throw new IllegalArgumentException("URI non supportée : " + uri);

6. }

7. }

9. // Crée les constantes utilisées pour différencier les requêtes URI

10. private static final int ITEM = 1;

12. private static final int COLLECTION = 2;

14. private static final UriMatcher uriMatcher;

16. // Alloue l'objet UriMatcher.

17. // Une URI terminée par myPathToData correspondra à une requête sur une collection.

18. // Une URI terminée par'/[rowID]' correspondra à un item.

19. static {

20. uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

21. uriMatcher.addURI("MyAuthority", "myPathToData", COLLECTION);

22. uriMatcher.addURI("MyAuthority ", " myPathToData /#", ITEM);

23. }

I-B-1-d - query

Cette méthode doit renvoyer un Cursor en fonction de l'URI passée en paramètre, de la projection, de la clause where ainsi que de la clause OrderBy.

Quand votre provider s'appuie sur une base de données, cela est assez simple, l'utilisation du SQLiteBuilder est préconisée (cf. l'exemple). Sinon, c'est à vous de vous lancer dans une analyse de la requête pour obtenir les résultats attendus.

L'URI permet de savoir si on se place dans le contexte d'une collection ou pas. Dans une collection, l'utilisation paraît adaptée ; pour un item, la requête vise à vérifier que l'item passé en paramètre correspond à la clause where. Il n'y a pas de préconisation globale, à vous de réfléchir à ce problème. Ce questionnement se pose pour les méthodes insert, update, query et delete.

I-B-1-e - Insert

Cette méthode insère un objet dans votre contenu. C'est à vous de le mettre en place.

Si vous ne voulez pas d'insertion, vous pouvez renvoyer l'exception :

1. throw new UnsupportedOperationException();

I-B-1-f - Update

Cette méthode met à jour un objet ou une collection.

I-B-1-g - Delete

Cette méthode supprime un objet ou une collection.

I-B-1-h - Manifest

Il faut modifier votre manifeste de manière à référencer votre provider de la manière suivante :

1. <provider android:name=".FullyQualifiedNameOfTheContentProvider"

2. android:authorities="TheAuthorityOfTheProvider" />

Vous pouvez mettre une liste d'autorités séparées par des points-virgules.

I-B-1-i - Mise en place de listeners de modifications

Les clients de votre fournisseur de contenu peuvent s'abonner aux modifications des données.

Pour cela votre ContentProvider doit appeler sa méthode getContext().getContentResolver().notifyChange() quand l'une de ses données est mise à jour. Il faut passer à cette méthode l'URI du contenu modifié ainsi qu'un ContentObserver. Ce ContentObserver ne sera pas notifié de la modification. Ainsi, si vous n'implémentez pas de modification de données via une partie tierce de votre application, ce paramètre vaudra null.

Les utilisateurs du provider, eux, se doivent d'effectuer plusieurs opérations.

D'une part, il faut qu'ils possèdent une classe (interne ou pas) qui étende ContentObserver. Cette classe a en charge l'écoute des changements et possède deux méthodes.

L'une, deliverSelfNotifications, lui permet de spécifier si elle est intéressée par les changements qu'elle fait sur les curseurs sur lesquels l'observer est enregistré.

L'autre, onChange, lui permet d'être informée qu'un changement vient de se produire sur les données qu'elle observe.

D'autre part, il faut que l'activité qui écoute instancie un Handler qui sera informé des modifications via le ContentObserver et mettra à jour votre activité. Il faut donc lui réécrire sa méthode handleMessage.

Enfin, votre activité doit s'enregistrer comme écouteuse de changement de votre provider :

1. getContentResolver().registerContentObserver(URI, true, new MyContentObserver(handler));

Ce qui donne, dans le ContentProvider :

1. getContext().getContentResolver().notifyChange(CONTENT_URI\pathToData\Item);

Dans l'écouteur :

1. public class MyActivity extends Activity {

2. ...

3. // The URI of the ContentProvider

4. URI ContentProviderURI=new URI(...);

6. // The handler to manage modifications of data in a thread safe way

7. Handler myHandler = new Handler() {

8. @Override

9. public void handleMessage(Message msg) {

10. super.handleMessage(msg);

11. //update your data using the Handler Android pattern

12. ...

13. }

14. };

16. ...

18. @Override

19. public void onCreate(Bundle icicle) {

20. ...

21. getContentResolver().registerContentObserver(ContentProviderURI, true,

22. new myContentObserver(myHandler);

23. ...

24. }

25. ...

27. //The inner public class to observe the change on the data of the content Provider

28. public class MyContentObserver extends ContentObserver {

29. @Override

30. public boolean deliverSelfNotifications() {

31. return super.deliverSelfNotifications();

32. }

35. @Override

36. public void onChange(boolean selfChange) {

37. super.onChange(selfChange);

38. //And then make a call to your handler

39. handler.sendMessage(new Message());

40. }

41. }

42. ...

43. }

Ou de manière plus synthétique :

1. public class MyActivity extends Activity {

2. ...

3. // The URI of the ContentProvider

4. URI ContentProviderURI = new URI(...);

6. // The handler to manage modifications of data in a thread safe way

7. Handler myHandler = new Handler() {

8. @Override

9. public void handleMessage(Message msg) {

10. super.handleMessage(msg);

11. // Update your data using the Handler Android pattern

12. ...

13. }

14. };

16. ...

17. @Override

18. public void onCreate(Bundle icicle) {

19. ... // TheContent Observer

20. ContentObserver myContentObserver = new ContentObserver (myHandler){

21. @Override

22. public void onChange(boolean selfChange) {

23. super.onChange(selfChange);

24. // And then make a call to your handler

25. handler.sendMessage(new Message());

26. };

28. // Then register your Activity as a content observer

30. getContentResolver().registerContentObserver(ContentProviderURI, true, myContentObserver);

31. ...

32. };

33. ...

34. }

I-B-1-j - Exemple avec une base de données

L'intérêt de cet exemple est double : d'une part, il présente un ContentProvider complet, de l'autre il montre l'utilisation de la classe SQLiteQueryBuilder.

1. /**

2. * @author mSeguy

3. * @goals This class aims to make a exemple of a ContentProvider that uses SQLite

4. * <ul>

5. * <li></li>

6. * </ul>

7. */

8. public class MyDataProvider extends ContentProvider {

9. // *************************************************************************************

10. // The Constants declaration ************************************************************

11. // *************************************************************************************

12. // Mon Authority, mes paths to data et mon URI

14. // The Authority

15. public static final String AUTHORITY = "myAuthority";// package de la classe provider est préconisé (utilise des .)

17. // The path to the data … and explain

18. public static final String PATH_TO_DATA = "myPath"; // Vous pouvez déclarer plusieurs paths (les paths utilisent les /)

20. /**

21. * @author mSeguy

22. * @goals This class aims to show the constant to use for the MyDataProvider

23. */

24. public static class Constants implements BaseColumns {

25. // The URI and explain, with example if you want

26. public static final Uri CONTENT_URI = Uri.parse("content://" + MyDataProvider.AUTHORITY + "/" + MyDataProvider.PATH_TO_DATA);

28. // Mon MIME type

29. // My Column ID and the associated explanation

30. public static final String MIME_COLLECTION = "vnd.android.cursor.dir/vnd.myCompany.contentType";

32. // My Column ID and the associated explanation

33. public static final String MIME_ITEM = "vnd.android.cursor.item/vnd.myCompany.contentType";

35. // Noms de colonnes

36. // /!\Si vous utilisez une base de données, les noms des colonnes sont les mêmes que ceux de

37. // votre base, de même pour les index.

38. // My Column ID and the associated explanation for end-users

39. public static final String KEY_COL_ID = "_id";// Mandatory

41. // My Column Name and the associated explanation for end-users

42. public static final String KEY_COL_NAME = "name";

44. // My Column First Name and the associated explanation for end-users

45. public static final String KEY_COL_FIRSTNAME = "firstName";

47. // My Column Eyes Color and the associated explanation for end-users

48. public static final String KEY_COL_EYES_COLOR = "eyesColor";

50. // My Column Hair color and the associated explanation for end-users

51. public static final String KEY_COL_HAIR_COLOR = "hairColor";

53. // My Column age and the associated explanation for end-users

54. public static final String KEY_COL_AGE = "age";

56. // Indexes des colonnes

57. // The index of the column ID

58. public static final int ID_COLUMN = 1;

60. // The index of the column NAME

61. public static final int NAME_COLUMN = 2;

63. // The index of the column FIRST NAME

64. public static final int FIRSTNAME_COLUMN = 3;

66. // The index of the column EYES COLOR

67. public static final int EYES_COLOR_COLUMN = 4;

69. // The index of the column HAIR COLOR

70. public static final int HAIR_COLOR_COLUMN = 5;

72. // The index of the column AGE

73. public static final int AGE_COLUMN = 6;

74. }

76. // Back To our provider

77. @Override

78. public boolean onCreate() {

79. Context context = getContext();

80. MyDatabaseHelper dbHelper = new MyDatabaseHelper(context, DATABASE_NAME, null,

81. DATABASE_VERSION);

82. myDB = dbHelper.getWritableDatabase();

83. return (myDB == null) ? false : true;

84. }

86. @Override

87. public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sort) {

89. SQLiteQueryBuilder qb = new SQLiteQueryBuilder();

90. qb.setTables(MY_TABLE);

91. // S'il s'agit d'une requête sur une ligne, on limite le résultat.

92. switch (uriMatcher.match(uri)) {

93. case ITEM:

94. qb.appendWhere(Constants.KEY_COL_ID + "=" + uri.getPathSegments().get(1));

95. break;

96. default:

97. break;

98. }

100. // Si aucun ordre de tri n'est spécifié, tri par date/heure

101. String orderBy;

102. if (TextUtils.isEmpty(sort)) {

103. orderBy = Constants.KEY_COL_NAME;

104. } else {

105. orderBy = sort;

106. }

108. // Applique la requête à la base.

109. Cursor c = qb.query(myDB, projection, selection, selectionArgs, null, null, orderBy);

111. // Enregistre le ContextResolver pour qu'il soit averti

112. // si le résultat change.

113. c.setNotificationUri(getContext().getContentResolver(), uri);

115. // Renvoie un curseur.

116. return c;

117. }

119. @Override

120. public Uri insert(Uri _uri, ContentValues _initialValues) {

121. // Insère la nouvelle ligne. Renvoie son numéro en cas de succès

122. long rowID = myDB.insert(MY_TABLE, "data", _initialValues);

124. // Renvoie l'URI de la nouvelle ligne.

125. if (rowID > 0) {

126. Uri uri = ContentUris.withAppendedId(Constants.CONTENT_URI, rowID);

127. getContext().getContentResolver().notifyChange(uri, null);

128. return uri;

129. }

130. throw new SQLException("Echec de l'ajout d'une ligne dans " + _uri);

131. }

133. @Override

134. public int delete(Uri uri, String where, String[] whereArgs) {

135. int count;

137. switch (uriMatcher.match(uri)) {

138. case COLLECTION:

139. count = myDB.delete(MY_TABLE, where, whereArgs);

140. break;

142. case ITEM:

143. String segment = uri.getPathSegments().get(1);

144. count = myDB.delete(MY_TABLE, KEY_ID + "=" + segment

145. + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);

146. break;

148. default:

149. throw new IllegalArgumentException("URI non supportée : " + uri);

150. }

152. getContext().getContentResolver().notifyChange(uri, null);

153. return count;

154. }

156. @Override

157. public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {

158. int count;

159. switch (uriMatcher.match(uri)) {

160. case COLLECTION:

161. count = myDB.update(MY_TABLE, values, where, whereArgs);

162. break;

164. case ITEM:

165. String segment = uri.getPathSegments().get(1);

166. count = myDB.update(MY_TABLE, values, Constants.KEY_COL_ID + "=" + segment

167. + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);

168. break;

170. default:

171. throw new IllegalArgumentException("URI inconnue " + uri);

172. }

174. getContext().getContentResolver().notifyChange(uri, null);

175. return count;

176. }

178. @Override

179. public String getType(Uri uri) {

180. switch (uriMatcher.match(uri)) {

181. case COLLECTION:

182. return "vnd.android.cursor.dir/vnd.myCompany.contentType ";

183. case ITEM:

184. return "vnd.android.cursor.item/vnd.myCompany.contentType ";

185. default:

186. throw new IllegalArgumentException("URI non supportée : " + uri);

187. }

188. }

190. // Crée les constantes utilisées pour différencier les requêtes URI

191. private static final int ITEM = 1;

192. private static final int COLLECTION = 2;

194. private static final UriMatcher uriMatcher;

196. // Alloue l'objet UriMatcher.

197. // Une URI terminée par myPathToData correspondra à une requête sur une collection.

198. // Une URI terminée par'/[rowID]' correspondra à un item.

199. static {

200. uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

201. uriMatcher.addURI("MyAuthority", "myPathToData", COLLECTION);

202. uriMatcher.addURI("MyAuthority ", " myPathToData /#", ITEM);

203. }

205. // Managing the database

207. // La base de données

208. private SQLiteDatabase myDB;

210. private static final String TAG = "MyProvider";

211. private static final String DATABASE_NAME = "myData.db";

212. private static final int DATABASE_VERSION = 1;

213. private static final String MY_TABLE = "MyTable";

215. // Classe helper pour ouvrir, créer et gérer le contrôle de version de la base

216. private static class MyDatabaseHelper extends SQLiteOpenHelper {

217. private static final String DATABASE_CREATE = "create table " + MY_TABLE + " ("

218. + Constants.KEY_COL_ID + " integer primary key autoincrement, "

219. + Constants.KEY_COL_AGE + " INTEGER, "

220. + Constants.KEY_COL_NAME + " TEXT, "

221. + Constants.KEY_COL_FIRSTNAME + " TEXT, "

222. + Constants.KEY_COL_EYES_COLOR+ " TEXT, "

223. + Constants.KEY_COL_HAIR_COLOR + " TEXT) ";

225. public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {

226. super(context, name, factory, version);

227. }

229. @Override

230. public void onCreate(SQLiteDatabase db) {

231. db.execSQL(DATABASE_CREATE);

232. }

234. @Override

235. public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

236. Log.w(TAG, "Mise à jour de la version " + oldVersion + " vers la version " + newVersion

237. + ", les anciennes données seront détruites ");

238. db.execSQL("DROP TABLE IF EXISTS " + MY_TABLE);

239. onCreate(db);

240. }

241. }

242. }

II - Remerciements

Cet article a été publié avec l'aimable autorisation de la société Android2EE.

Nous tenons à remercier ced pour la relecture orthographique de cet article et Djibril et Mickael Baron pour la mise au format Developpez.com.