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

Chapitre 5
Image non disponible
Android2ee

Un fournisseur de contenu (ContentProvider) est un objet à qui on passe une URIUnified Resource Identifier (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.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum : 3 commentaires Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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

Un fournisseur de contenu est un objet à qui on passe une URIUnified Resource Identifier (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

 
Sélectionnez
1.
2.
ContentResolver cr = getContentResolver();
cr.query(uri, projection, selection, selectionArgs, sortOrder)

Ou bien

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
ContentResolver cr= getContentResolver();
//Pour ramener toutes les lignes
cr.query(MyProvider.CONTENT_URI, null, null, null, null);
//Pour ramener un sous-ensemble :
String[] projection = {Key_ColHuman_FirstName, Key_ColHuman_Name };
String where = Key_ColHuman_EyesColor + "=? AND" + Key_ColHuman_HairColor + "=?";
String[] whereParam = {"blue","brown"};
String order = Key_ColHuman_Name;
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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
ContentResolver cr = getContentResolver();

//Pour insérer une ligne
ContentValues newLine = new ContentValues();
newLine.put(Column_Name, newValue);
Uri myDataURI=cr.insert(MyProvied.CONTENT_URI, newLine);

//Pour insérer n lignes
ContentValues[]newLines=new ContentValues[5];
//Remplir le tableau… et insérer les valeurs
Int insertedLines=cr.bulkInsert(MyProvied.CONTENT_URI, newLines);

Javadoc :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
ContentResolver cr = getContentResolver();
//Pour supprimer une ligne connue
cr.delete(myRowURI,null,null);
//Pour supprimer un sous-ensemble :
String where = Key_ColHuman_EyesColor+"=? AND"+ Key_ColHuman_HairColor+"=?";
String[] whereParam = {"blue","brown"};
cr.delete(MyProvider.CONTENT_URI, where, whereParam);

Javadoc :

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
ContentResolver cr = getContentResolver();
ContentValues newLineValues = new ContentValues();
//Pour mettre à jour une ligne connue
cr.update(myRowURI, newLineValues, null, null);
//Pour mettre à jour un sous-ensemble :
String where = Key_ColHuman_EyesColor + "=? AND"+ Key_ColHuman_HairColor + "=?";
String[] whereParam = {"blue","brown"};
cr.update(MyProvider.CONTENT_URI, newLineValues, where, whereParam);

Javadoc :

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
//Récupérer la liste des contacts
Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
while (cursor.moveToNext()) {
    String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
    String hasPhone = cursor.getString(cursor.getColumnIndex(ContactsContract.
        Contacts.HAS_PHONE_NUMBER));
    if (Boolean.parseBoolean(hasPhone)) {
        // You know have the number so now query it like this
        Cursor phones = getContentResolver().query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,
            ContactsContract.CommonDataKinds.Phone.CONTACT_ID + " = " + contactId, null, null);
        while (phones.moveToNext()) {
            String phoneNumber = phones.getString(phones
                .getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
        }
        phones.close();
    }

    Cursor emails = getContentResolver().query(
        ContactsContract.CommonDataKinds.Email.CONTENT_URI, null,
        ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + contactId, null, null);
    while (emails.moveToNext()) {
        // This would allow you get several email addresses
        String emailAddress = emails.getString(emails
            .getColumnIndex(ContactsContract.CommonDataKinds.CommonDataColumns.DATA));
    }
    emails.close();
}
cursor.close();

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
Cursor people = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
while (people.moveToNext()) {
    int nameFieldColumnIndex = people.getColumnIndex(PhoneLookup.DISPLAY_NAME);
    String contact = people.getString(nameFieldColumnIndex);
    int numberFieldColumnIndex = people.getColumnIndex(PhoneLookup.NUMBER);
    String number = people.getString(nameFieldColumnIndex);
}
people.close();

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

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
private String getNote(long contactId) {
    String note = null;
    String[] columns = new String[] { ContactsContract.CommonDataKinds.Note.NOTE };
    String where = ContactsContract.Data.RAW_CONTACT_ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
    String[] whereParameters = new String[] { Long.toString(contactId), ContactsContract.CommonDataKinds.Note.CONTENT_ITEM_TYPE };
    Cursor contacts = getContentResolver().query(ContactsContract.Data.CONTENT_URI, projection, where, whereParameters, null);
    if (contacts.moveToFirst()) {
        note = contacts.getString(0);
    }
    contacts.close();
    return note;
}

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 :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
Uri media = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
String[] projection = { MediaStore.Audio.Media._ID,           // 0
                        MediaStore.Audio.Media.ARTIST,        // 1
                        MediaStore.Audio.Media.TITLE,         // 2
                        MediaStore.Audio.Media.ALBUM_ID,      // 3
                        MediaStore.Audio.Media.ALBUM,         // 4
                        MediaStore.Audio.Media.DATA,          // 5
                        MediaStore.Audio.Media.DISPLAY_NAME,  // 6
                        MediaStore.Audio.Media.DURATION };    // 7
 
String selection = MediaStore.Audio.Media.IS_MUSIC + " != 0";
 
Cursor cursor = query(c,media,projection,selection,null,null);
while(cursor.moveToNext()){
     songs.add(new Music(cursor.getString(1),
                         cursor.getString(2),
                         cursor.getString(4),
                         cursor.getString(7),
                         cursor.getString(5),
                         cursor.getLong(0),
                         cursor.getLong(3)));
}
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 :

Image non disponible

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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
public class MyDataProvider extends ContentProvider {
    // Mon Authority, mes paths to data et mon URI
    // The Authority
    public static final String AUTHORITY = "myAuthority";//package de la classe provider est préconisé (utilise des .)
    // The path to the data… and explain 
    public static final String PATH_TO_DATA = "myPath"; //Vous pouvez déclarer plusieurs paths (les paths utilisent les /)

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

    public class MyDatas extends BaseColumns {

        // The URI and explain, with example if you want 
        public static final URI CONTENT_URI = URI.parse("content://" + MyDataProvider.AUTHORITY + "/" + MyDataProvider.PATH_TO_DATA);

        // Mon MIME type
        // My Column ID and the associated explanation 
        public static final String MIME_COLLECTION = "vnd.android.cursor.dir/vnd.myCompany.contentType"

        // My Column ID and the associated explanation
        public static final String MIME_ITEM = "vnd.android.cursor.item/vnd.myCompany.contentType"

        // Noms de colonnes
        //  /!\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.

        // My Column ID and the associated explanation for end-users
        public static final String KEY_COL_ID = "_id"; // Mandatory

        // My Column Name and the associated explanation for end-users 
        public static final String KEY_ COL_NAME = "name";

        // My Column First Name and the associated explanation for end-users 
        public static final String KEY_ COL_FIRSTNAME = "firstName";

        // My Column Eyes Color and the associated explanation for end-users 
        public static final String KEY_ COL_EYES_COLOR = "eyesColor";

        // My Column Hair color and the associated explanation for end-users 
        public static final String KEY_ COL_HAIR_COLOR = "hairColor";

        // My Column age and the associated explanation for end-users 
        public static final String KEY_ COL_AGE = "age";

        // Index des colonnes

        // The index of the column ID
        public static final int ID_COLUMN = 1;
        // The index of the column NAME
        public static final int NAME_COLUMN = 2;
        // The index of the column FIRST NAME
        public static final int FIRSTNAME_COLUMN = 3;
        // The index of the column EYES COLOR
        public static final int EYES_COLOR_COLUMN = 4;
        // The index of the column HAIR COLOR
        public static final int HAIR_COLOR_COLUMN = 5;
        // The index of the column AGE
        public static final int AGE_COLUMN = 6; 
    }
    ...
}

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).

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)) {
        case COLLECTION: return "vnd.android.cursor.dir/vnd.myCompany.contentType ";
        case ITEM: return "vnd.android.cursor.item/vnd.myCompany.contentType ";
        default: throw new IllegalArgumentException("URI non supportée : " + uri);
    }
}

// Crée les constantes utilisées pour différencier les requêtes URI
private static final int ITEM = 1;

private static final int COLLECTION = 2;

private static final UriMatcher uriMatcher;

// Alloue l'objet UriMatcher. 
// Une URI terminée par myPathToData correspondra à une requête sur une collection.
// Une URI terminée par'/[rowID]' correspondra à un item.
static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI("MyAuthority", "myPathToData", COLLECTION);
    uriMatcher.addURI("MyAuthority ", " myPathToData /#", ITEM);
}

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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
2.
<provider android:name=".FullyQualifiedNameOfTheContentProvider"
          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 :

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

Ce qui donne, dans le ContentProvider :

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

Dans l'écouteur :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
public class MyActivity extends Activity {
    ...
    // The URI of the ContentProvider
    URI ContentProviderURI=new URI(...);

    // The handler to manage modifications of data in a thread safe way
    Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //update your data using the Handler Android pattern
            ...
        }
    };

    ...

    @Override
    public void onCreate(Bundle icicle) {
        ...
        getContentResolver().registerContentObserver(ContentProviderURI, true, 
        new myContentObserver(myHandler);
        ...
    }
    ...

    //The inner public class to observe the change on the data of the content Provider
    public class MyContentObserver extends ContentObserver {
        @Override
        public boolean deliverSelfNotifications() {
            return super.deliverSelfNotifications();
        }


        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            //And then make a call to your handler
            handler.sendMessage(new Message());
        }
    }
    ...
}

Ou de manière plus synthétique :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
public class MyActivity extends Activity {
    ...
    // The URI of the ContentProvider
    URI ContentProviderURI = new URI(...);

    // The handler to manage modifications of data in a thread safe way
    Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            // Update your data using the Handler Android pattern
            ...
        }
    };

    ...
    @Override
    public void onCreate(Bundle icicle) {
        ... // TheContent Observer
        ContentObserver myContentObserver = new ContentObserver (myHandler){
            @Override
            public void onChange(boolean selfChange) {
                super.onChange(selfChange);
                // And then make a call to your handler
                handler.sendMessage(new Message());
            };

            // Then register your Activity as a content observer

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

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.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
87.
88.
89.
90.
91.
92.
93.
94.
95.
96.
97.
98.
99.
100.
101.
102.
103.
104.
105.
106.
107.
108.
109.
110.
111.
112.
113.
114.
115.
116.
117.
118.
119.
120.
121.
122.
123.
124.
125.
126.
127.
128.
129.
130.
131.
132.
133.
134.
135.
136.
137.
138.
139.
140.
141.
142.
143.
144.
145.
146.
147.
148.
149.
150.
151.
152.
153.
154.
155.
156.
157.
158.
159.
160.
161.
162.
163.
164.
165.
166.
167.
168.
169.
170.
171.
172.
173.
174.
175.
176.
177.
178.
179.
180.
181.
182.
183.
184.
185.
186.
187.
188.
189.
190.
191.
192.
193.
194.
195.
196.
197.
198.
199.
200.
201.
202.
203.
204.
205.
206.
207.
208.
209.
210.
211.
212.
213.
214.
215.
216.
217.
218.
219.
220.
221.
222.
223.
224.
225.
226.
227.
228.
229.
230.
231.
232.
233.
234.
235.
236.
237.
238.
239.
240.
241.
242.
/** 
* @author mSeguy
* @goals This class aims to make a exemple of a ContentProvider that uses SQLite
* <ul>
* <li></li>
* </ul>
*/
public class MyDataProvider extends ContentProvider {
    // *************************************************************************************
    // The Constants declaration ************************************************************
    // *************************************************************************************
    // Mon Authority, mes paths to data et mon URI

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

    // The path to the data  and explain 
    public static final String PATH_TO_DATA = "myPath"; // Vous pouvez déclarer plusieurs paths (les paths utilisent les /)

    /** 
    * @author mSeguy
    * @goals This class aims to show the constant to use for the MyDataProvider
    */
    public static class Constants implements BaseColumns {
        // The URI and explain, with example if you want 
        public static final Uri CONTENT_URI = Uri.parse("content://" + MyDataProvider.AUTHORITY + "/" + MyDataProvider.PATH_TO_DATA);

        // Mon MIME type
        // My Column ID and the associated explanation 
        public static final String MIME_COLLECTION = "vnd.android.cursor.dir/vnd.myCompany.contentType";

        // My Column ID and the associated explanation 
        public static final String MIME_ITEM = "vnd.android.cursor.item/vnd.myCompany.contentType";

        // Noms de colonnes
        // /!\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.
        // My Column ID and the associated explanation for end-users 
        public static final String KEY_COL_ID = "_id";// Mandatory

        // My Column Name and the associated explanation for end-users 
        public static final String KEY_COL_NAME = "name";

        // My Column First Name and the associated explanation for end-users 
        public static final String KEY_COL_FIRSTNAME = "firstName";

        // My Column Eyes Color and the associated explanation for end-users 
        public static final String KEY_COL_EYES_COLOR = "eyesColor";

        // My Column Hair color and the associated explanation for end-users 
        public static final String KEY_COL_HAIR_COLOR = "hairColor";

        // My Column age and the associated explanation for end-users 
        public static final String KEY_COL_AGE = "age";

        // Indexes des colonnes
        // The index of the column ID 
        public static final int ID_COLUMN = 1;

        // The index of the column NAME 
        public static final int NAME_COLUMN = 2;

        // The index of the column FIRST NAME 
        public static final int FIRSTNAME_COLUMN = 3;

        // The index of the column EYES COLOR 
        public static final int EYES_COLOR_COLUMN = 4;

        // The index of the column HAIR COLOR 
        public static final int HAIR_COLOR_COLUMN = 5;

        // The index of the column AGE 
        public static final int AGE_COLUMN = 6;
    }

    // Back To our provider
    @Override
    public boolean onCreate() {
        Context context = getContext();
        MyDatabaseHelper dbHelper = new MyDatabaseHelper(context, DATABASE_NAME, null,
        DATABASE_VERSION);
        myDB = dbHelper.getWritableDatabase();
        return (myDB == null) ? false : true;
    }

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

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(MY_TABLE);
        // S'il s'agit d'une requête sur une ligne, on limite le résultat.
        switch (uriMatcher.match(uri)) {
        case ITEM:
            qb.appendWhere(Constants.KEY_COL_ID + "=" + uri.getPathSegments().get(1));
            break;
        default:
            break;
        }

        // Si aucun ordre de tri n'est spécifié, tri par date/heure
        String orderBy;
        if (TextUtils.isEmpty(sort)) {
            orderBy = Constants.KEY_COL_NAME;
        } else {
            orderBy = sort;
        }

        // Applique la requête à la base.
        Cursor c = qb.query(myDB, projection, selection, selectionArgs, null, null, orderBy);

        // Enregistre le ContextResolver pour qu'il soit averti
        // si le résultat change.
        c.setNotificationUri(getContext().getContentResolver(), uri);

        // Renvoie un curseur.
        return c;
    }

    @Override
    public Uri insert(Uri _uri, ContentValues _initialValues) {
        // Insère la nouvelle ligne. Renvoie son numéro en cas de succès
        long rowID = myDB.insert(MY_TABLE, "data", _initialValues);

        // Renvoie l'URI de la nouvelle ligne.
        if (rowID > 0) {
            Uri uri = ContentUris.withAppendedId(Constants.CONTENT_URI, rowID);
            getContext().getContentResolver().notifyChange(uri, null);
            return uri;
        }
        throw new SQLException("Echec de l'ajout d'une ligne dans " + _uri);
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        int count;

        switch (uriMatcher.match(uri)) {
        case COLLECTION:
            count = myDB.delete(MY_TABLE, where, whereArgs);
            break;

        case ITEM:
            String segment = uri.getPathSegments().get(1);
            count = myDB.delete(MY_TABLE, KEY_ID + "=" + segment
            + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
            break;

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

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        int count;
        switch (uriMatcher.match(uri)) {
        case COLLECTION:
            count = myDB.update(MY_TABLE, values, where, whereArgs);
            break;

        case ITEM:
            String segment = uri.getPathSegments().get(1);
            count = myDB.update(MY_TABLE, values, Constants.KEY_COL_ID + "=" + segment
            + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""), whereArgs);
            break;

        default:
            throw new IllegalArgumentException("URI inconnue " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }

    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)) {
        case COLLECTION:
            return "vnd.android.cursor.dir/vnd.myCompany.contentType ";
        case ITEM:
            return "vnd.android.cursor.item/vnd.myCompany.contentType ";
        default:
            throw new IllegalArgumentException("URI non supportée : " + uri);
        }
    }

    // Crée les constantes utilisées pour différencier les requêtes URI
    private static final int ITEM = 1;
    private static final int COLLECTION = 2;

    private static final UriMatcher uriMatcher;

    // Alloue l'objet UriMatcher.
    // Une URI terminée par myPathToData correspondra à une requête sur une collection.
    // Une URI terminée par'/[rowID]' correspondra à un item.
    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("MyAuthority", "myPathToData", COLLECTION);
        uriMatcher.addURI("MyAuthority ", " myPathToData /#", ITEM);
    }

    // Managing the database

    // La base de données
    private SQLiteDatabase myDB;

    private static final String TAG = "MyProvider";
    private static final String DATABASE_NAME = "myData.db";
    private static final int DATABASE_VERSION = 1;
    private static final String MY_TABLE = "MyTable";

    // Classe helper pour ouvrir, créer et gérer le contrôle de version de la base
    private static class MyDatabaseHelper extends SQLiteOpenHelper {
        private static final String DATABASE_CREATE = "create table " + MY_TABLE + " ("
            + Constants.KEY_COL_ID + " integer primary key autoincrement, "
            + Constants.KEY_COL_AGE + " INTEGER, " 
            + Constants.KEY_COL_NAME + " TEXT, "
            + Constants.KEY_COL_FIRSTNAME + " TEXT, " 
            + Constants.KEY_COL_EYES_COLOR+ " TEXT, " 
            + Constants.KEY_COL_HAIR_COLOR + " TEXT) ";

        public MyDatabaseHelper(Context context, String name, CursorFactory factory, int version) {
            super(context, name, factory, version);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Mise à jour de la version " + oldVersion + " vers la version " + newVersion
                + ", les anciennes données seront détruites ");
            db.execSQL("DROP TABLE IF EXISTS " + MY_TABLE);
            onCreate(db);
        }
    }
}

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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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.