Licence CC BY-NC-SA

Le partage de contenus entre applications

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

L'avantage des bases de données, c'est qu'elles facilitent le stockage de données complexes et structurées. Cependant, le problème qu'on rencontre avec ces bases, c'est qu'il n'est pas possible d'accéder à la base de données d'une application qui ne nous appartient pas. Néanmoins, il peut arriver qu'on ait vraiment besoin de partager du contenu entre plusieurs applications. Un exemple simple et courant est de pouvoir consulter les contacts de l'utilisateur qui sont enregistrés dans l’application « Carnet d'adresses ». Ces accès aux données d'une application différente de la nôtre se font à l'aide des fournisseurs de contenu ou content providers en anglais.

Les fournisseurs de contenu sont le quatrième et dernier composant des applications que nous verrons. Techniquement, un fournisseur de contenu est découpé en deux éléments distincts :

  • Le fournisseur de contenu, qui sera utilisé dans l'application qui distribue son contenu aux autres applications.
  • Un client, qui permettra aux autres applications de demander au fournisseur les informations voulues.

Ensemble, les fournisseurs et les clients offrent une interface standardisée permettant l'échange sécurisé de données, ainsi que les communications inter-processus, de façon à faciliter les transactions entre applications. Ils permettent entre autres d'effectuer des copier/coller de données complexes depuis votre application vers d'autres applications.

Pour être tout à fait franc, il n'est pas rare qu'une application ne développe pas son propre fournisseur de contenu, car ils ne sont nécessaires que pour des besoins bien spécifiques, mais il se pourrait bien qu'un jour vous rencontriez ce type de difficultés.

Je reprends ici la même base de données qui représente les membres du Site du Zéro qui participent à l'écriture de ce tutoriel. N'hésitez pas à aller relire complètement le chapitre sur les bases de données afin de vous familiariser avec cette architecture et vous remémorer les différentes techniques et termes techniques, ce chapitre-là étant intimement lié au présent chapitre.

Côté client : accéder à des fournisseurs

Les fournisseurs de contenu permettent l'encapsulation de données, et, pour accéder à ces données, il faudra utiliser les fameuses URI. Ici, nous ne saurons pas où ni comment les données sont stockées. Dans une base de données, dans un fichier, sur un serveur distant ? Cela ne nous regarde pas, du moment que les données nous sont mises à disposition.

Cependant, quel que soit le type de stockage, les données nous sont toujours présentées de la même manière. En effet, et un peu comme une base de données relationnelle, un fournisseur de contenu présente les données à une application extérieure dans une ou plusieurs tables. Chaque entrée dans la table est représentée dans une ligne et chaque colonne représente un attribut.

Une chose importante à savoir avant de faire appel à un fournisseur de contenu : vous devez savoir par avance la structure des tables (ses attributs et les valeurs qu'ils peuvent prendre), car vous en aurez besoin pour exploiter correctement ce fournisseur de contenu. Il n'y a pas moyen d'obtenir ce genre d'informations, il faut que le développeur du fournisseur vous communique cette information.

Un fournisseur n'a pas besoin d'avoir une clé primaire. S'il en a une, elle peut l'appeler _ID, même si ce n'est pas nécessaire. Si vous le faites, alors Android pourra faire quelques traitements automatiquement. Par exemple, si vous voulez lier des données depuis un fournisseur vers une ListView, il vaut mieux que la clé primaire s'appelle _ID de façon à ce que le ListView puisse deviner tout seul qu'il s'agit d'une clé primaire.

Examinons les éléments architecturaux des fournisseurs de contenu, ainsi que la relation qui existe entre les fournisseurs de contenu et les autres abstractions qui permettent l'accès aux données.

Accéder à un fournisseur

Il est possible d'accéder aux données d'une autre application avec un objet client ContentResolver. Cet objet a des méthodes qui appellent d'autres méthodes, qui ont le même nom, mais qui se trouvent dans un objet fournisseur, c'est-à-dire l'objet qui met à disposition le contenu pour les autres applications. Les objets fournisseurs sont de type ContentProvider. Aussi, si votre ContentResolver a une méthode qui s'appelle myMethod, alors le ContentProvider aura aussi une méthode qui s'appelle myMethod, et quand vous appelez myMethod sur votre ContentResolver, il fera en sorte d'appeler myMethod sur le ContentProvider.

Pourquoi je n'irais pas appeler ces méthodes moi-même ? Cela irait plus vite et ce serait plus simple !

Parce que ce n'est pas assez sécurisé ! Avec ce système, Android est certain que vous avez reçu les autorisations nécessaires à l'exécution de ces opérations.

Vous vous rappelez ce qu'on avait fait pour les bases de données ? On avait écrit des méthodes qui permettent de créer, lire, mettre à jour ou détruire des informations dans une base de données. Eh bien, ces méthodes, appelées méthodes CRUD, sont fournies par le ContentResolver. Ainsi, si mon ContentResolver demande poliment à un ContentProvider de lire des entrées dans la base de données de l'application dans laquelle se trouve ce ContentProvider, il appellera sur lui-même la méthode lireCesDonnées pour que soit appelée sur le ContentProvider la même méthode lireCesDonnées.

L'objet de type ContentResolver dans le processus de l'application cliente et l'objet de type ContentProvider de l'application qui fournit les données gèrent automatiquement les communications inter-processus, ce qui est bien parce que ce n'est pas une tâche aisée du tout. ContentProvider sert aussi comme une couche d'abstraction entre le référentiel de données et l'apparence extérieure des données en tant que tables.

Pour accéder à un fournisseur, votre application a besoin de certaines permissions. Vous ne pouvez bien entendu pas utiliser n'importe quel fournisseur sans l'autorisation de son application mère ! Nous verrons comment utiliser ou créer une permission par la suite.

Pour récupérer le gestionnaire des fournisseurs de contenu, on utilise la méthode de Context appelée ContentResolver getContentResolver (). Vous aurez ensuite besoin d'une URI pour déterminer à quel fournisseur de contenu vous souhaitez accéder.

L'URI des fournisseurs de contenu

Le schéma d'une URI qui représente un fournisseur de contenu est content. Ainsi, ce type d'URI commence par content://.

Après le schéma, on trouve l'information. Comme dans le cas des URL sur internet, cette information sera un chemin. Ce chemin est dit hiérarchique : plus on rajoute d'informations, plus on devient précis sur le contenu voulu. La première partie du chemin s'appelle l'autorité. Elle est utilisée en tant qu'identifiant unique afin de pouvoir différencier les fournisseurs dans le registre des fournisseurs que tient Android. Un peu comme un nom de domaine sur internet. Si vous voulez aller sur le Site du Zéro , vous utiliserez le nom de domaine www.siteduzero.com. Ici, le schéma est http (dans le cas d'une URL, le schéma est le protocole de communication utilisé pour recevoir et envoyer des informations) et l'autorité est www.siteduzero.com, car elle permet de retrouver le site de manière unique. Il n'y a aucun autre site auquel vous pourrez accéder en utilisant l'adresse www.siteduzero.com.

Si on veut rentrer dans une partie spécifique du Site du Zéro, on va ajouter des composantes au chemin et chaque composante permet de préciser un peu plus l'emplacement ciblé : http:/www.siteduzero.com/forum/android/demande_d_aide.html (cette URL est bien entendu totalement fictive :D ).

Comme vous pouvez le voir, les composantes sont séparées par des « / ». Ces composantes sont appelées des segments. On retrouve ainsi le segment forum qui nous permet de savoir qu'on se dirige vers les forums, puis android qui permet de savoir qu'on va aller sur un forum dédié à Android, et enfin demande_d_aide.html qui permet de se diriger vers le forum Android où on peut demander de l'aide.

Les URI pour les fournisseurs de contenu sont similaires. L'autorité seule est totalement nécessaire et chaque segment permet d'affiner un peu la recherche. Par exemple, il existe une API pour accéder aux données associées aux contacts enregistrés dans le téléphone : ContactsContract. Elle possède plusieurs tables, dont ContactsContract.Data qui contient des données sur les contacts (numéros de téléphone, adresses e-mail, comptes Facebook, etc.), ContactsContract.RawContacts qui contient les contacts en eux-mêmes, et enfin ContactsContract.Contacts qui fait le lien entre ces deux tables, pour lier un contact à ses données personnelles.

Pour accéder à ContactsContract, on peut utiliser l'URI content://com.android.contacts/. Si je cherche uniquement à accéder à la table Contact, je peux utiliser l'URI content://com.android.contacts/contact. Néanmoins, je peux affiner encore plus la recherche en ajoutant un autre segment qui indiquera l'identifiant du contact recherché : content://com.android.contacts/contact/18.

Ainsi, si j'effectue une recherche avec content://com.android.contacts/contact sur mon téléphone, j'aurai 208 résultats, alors que si j'utilise content://com.android.contacts/contact/18 je n'aurai qu'un résultat, celui d'identifiant 18.

De ce fait, le schéma sera content:// et l'autorité sera composée du nom du package. Le premier segment indiquera la table dans laquelle il faut chercher et le deuxième la composante de la ligne à récupérer : content://sdz.chapitreQuatre.Provider/Client/5. Ici, je récupère la cinquième entrée de ma table Client dans mon application Provider qui se situe dans le package sdz.chapitreQuatre.

On ne pourra retrouver une ligne que si l'on a défini un identifiant en lui donnant comme nom de colonne _ID. Dans l'exemple précédent, on cherche dans la table Client celui qui a pour valeur 5 dans la colonne _ID.

Android possède nativement un certain nombre de fournisseurs de contenu qui sont décrits dans android.provider. Vous trouverez une liste de ces fournisseurs sur la documentation. On trouve parmi ces fournisseurs des accès aux données des contacts, des appels, des médias, etc. Chacune de ces classes possède une constante appelée CONTENT_URI qui est en fait l'URI pour accéder au fournisseur qu'elles incarnent. Ainsi, pour accéder au fournisseur de contenu de ContactsContract.Contacts, on pourra utiliser l'URI ContactsContract.Contacts.CONTENT_URI.

Vous remarquerez que l'autorité des fournisseurs de contenu d'Android ne respecte pas la tradition qui veut qu'on ait le nom du package ainsi que le nom du fournisseur. Google peut se le permettre mais pas vous, alors n'oubliez pas la bonne procédure à suivre.

On trouve par exemple :

Nom

Description

Interface

Contact

Permet l'accès aux données des contacts de l'utilisateur.

La base est ContactsContract, mais il existe une vingtaine de façons d'accéder à ces informations.

Magasin multimédia

Liste les différents médias disponibles sur le support, tels que les images, vidéos, fichiers audios, etc.

La base est MediaStore, mais il existe encore une fois un bon nombre de dérivés, par exemple MediaStore.Audio.Artists liste tous les artistes dans votre magasin.

Navigateur

Les données de navigation telles que l'historique ou les archives des recherches.

On a Browser.SearchColumns pour les historiques des recherches et Browser.BookmarkColumns pour les favoris de l'utilisateur.

Appel

Appels passés, reçus et manqués par l'utilisateur.

On peut trouver ces appels dans CallLog.Calls.

Dictionnaire

Les mots que connaît le dictionnaire utilisateur.

Ces mots sont gérés avec UserDictionary.Words.

Pour avoir accès à ces contenus natifs, il faut souvent demander une permission. Si vous voulez par exemple accéder aux contacts, n'oubliez pas de demander la permission adaptée : android.permission.READ_CONTACTS.

Il existe des API pour vous aider à construire les URI pour les fournisseurs de contenu. Vous connaissez déjà Uri.Builder, mais il existe aussi ContentUris rien que pour les fournisseurs de contenu. Il contient par exemple la méthode statique Uri ContentUris.withAppendedId(Uri contentUri, long id) avec contentUri l'URI et id l'identifiant de la ligne à récupérer :

1
Uri client = ContentUris.withAppendedId(Uri.parse("content://sdz.chapitreQuatre.Provider/Client/"), 5);

Effectuer des opérations sur un fournisseur de contenu

Vous verrez ici d'énormes ressemblances avec la manipulation des bases de données, c'est normal, les deux API se fondent sur les mêmes principes fondamentaux. Il existe deux objets sur lesquels on peut effectuer les requêtes. Soit directement sur le ContentResolver, auquel cas vous devrez fournir à chaque fois l'URI du fournisseur de contenu visé. Soit, si vous effectuez les opérations sur le même fournisseur à chaque fois, vous pouvez utiliser plutôt un ContentProviderClient, afin de ne pas avoir à donner l'URI à chaque fois. On peut obtenir un ContentProviderClient en faisant ContentProviderClient acquireContentProviderClient(String name) sur un ContentResolver, name étant l'autorité du fournisseur.

Il n'est pas nécessaire de fermer un ContentResolver, cependant il faut appliquer boolean release() sur un ContentProviderClient pour aider le système à libérer de la mémoire. Exemple :

1
2
3
4
5
ContentProviderClient client = getContentResolver().acquireContentProviderClient("content://sdz.chapitreQuatre.Provider/Client/");

  // …   

client.release();

Les méthodes à utiliser entre les deux objets sont similaires, ils ont les mêmes paramètres, même si ContentProviderClient n'a pas besoin qu'on précise d'URI systématiquement. Je ne présenterai d'ailleurs que les méthodes de ContentProvider, retenez simplement qu'il suffit de ne pas passer le paramètre de type URI pour utiliser la méthode sur un ContentProviderClient.

Ajouter des données

Il existe deux méthodes pour ajouter des données. Il y a Uri insert(Uri url, ContentValues values), qui permet d'insérer une valeur avec un ContentValues que nous avons appris à utiliser avec les bases de données. L'URI retournée représente la nouvelle ligne insérée.

1
2
3
4
5
6
ContentValues values = new ContentValues();

values.put(DatabaseHandler.METIER_INTITULE, "Autre");
values.put(DatabaseHandler.METIER_SALAIRE, 0);

contentResolver.insert(MetierProvider.CONTENT_URI, values);

Il est aussi possible d'utiliser int bulkInsert(Uri url, ContentValues[] initialValues) pour insérer plusieurs valeurs à la fois. Cette méthode retourne le nombre de lignes créées.

Récupérer des données

Il n'existe qu'une méthode cette fois : Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) avec les mêmes paramètres que d'habitude :

  • uri indique le fournisseur de contenu.
  • project est un tableau des colonnes de la table à récupérer.
  • selection correspond à la valeur du WHERE.
  • selectionArgs permet de remplacer les « ? » dans selection par des valeurs.
  • sortOrder peut valoir « ASC » pour ranger les lignes retournées dans l'ordre croissant et « DESC » pour l'ordre décroissant.

Les résultats sont présentés dans un Cursor.

Par exemple, pour récupérer tous les utilisateurs dont le nom est « Apol » on peut faire :

1
Cursor c = contentResolver.query(Uri.parse("content://sdz.chapitreTrois.Membre"), null, "nom = ?", new String[] {"Apol"}, null);

Mettre à jour des données

On utilise int update(Uri uri, ContentValues values, String where, String[] selectionArgs) qui retourne le nombre de lignes mises à jour. Par exemple, pour changer le nom du métier « Autre » en « Les autres encore », on fera :

1
2
3
4
5
ContentValues values = new ContentValues();

values.put(DatabaseHandler.METIER_INTITULE, "Les autres encore");

int nombre = contentResolver.update(Uri.parse("content://sdz.chapitreTrois.Membre"), values, "metier = ?", new String[]{"Autre"});

Supprimer des données

Pour cela, il existe int delete(Uri url, String where, String[] selectionArgs) qui retourne le nombre de lignes mises à jour. Ainsi, pour supprimer les membres de nom « Apol » et de prénom « Lidore », on fera :

1
int nombre = contentResolver.delete(Uri.parse("content://sdz.chapitreTrois.Membre"), "nom = ?, prenom = ?", new String[] {"Apol", "Lidore"});

Créer un fournisseur

Maintenant que vous savez exploiter les fournisseurs de contenu, on va apprendre à en créer pour que vous puissiez mettre vos bases de données à disposition d'autres applications. Comme je l'ai déjà dit, il n'est pas rare qu'une application n'ait pas de fournisseur, parce qu'on les utilise uniquement pour certaines raisons particulières :

  • Vous voulez permettre à d'autres applications d'accéder à des données complexes ou certains fichiers.
  • Vous voulez permettre à d'autres applications de pouvoir copier des données complexes qui vous appartiennent.
  • Enfin, vous voulez peut-être aussi permettre à d'autres applications de faire des recherches sur vos données complexes.

Une autre raison de ne construire un fournisseur que si nécessaire est qu'il ne s'agit pas d'une tâche triviale : la quantité de travail peut être énorme et la présence d'un fournisseur peut compromettre votre application si vous ne vous protégez pas.

Si vous voulez juste une base de données, n'oubliez pas que vous pouvez très bien le faire sans fournisseur de contenu. Je dis cela parce que certains ont tendance à confondre les deux concepts.

La préparation de la création d'un fournisseur de contenu se fait en plusieurs étapes.

L'URI

Vous l'avez bien compris, pour identifier les données à récupérer, l'utilisateur aura besoin d'une URI. Elle contiendra une autorité afin de permettre la récupération du fournisseur de contenu et un chemin pour permettre d'affiner la sélection et choisir une table, un fichier ou encore une ligne dans une table.

Le schéma

Il permet d'identifier quel type de contenu désigne l'URI. Vous le savez déjà, dans le cas des fournisseurs de contenu, ce schéma sera content://.

L'autorité

Elle sera utilisée comme identifiant pour Android. Quand on déclare un fournisseur dans le Manifest, elle sera inscrite dans un registre qui permettra de la distinguer parmi tous les fournisseurs quand on y fera appel. De manière standard, on utilise le nom du package dans l'autorité afin d'éviter les conflits avec les autres fournisseurs. Ainsi, si le nom de mon package est sdz.chapitreQuatre.example, alors pour le fournisseur j'utiliserai sdz.chapitreQuatre.example.provider.

Le chemin

Il n'y a rien d'obligatoire, mais en général le premier segment de chemin est utilisé pour identifier une table et le second est utilisé comme un identifiant. De ce fait, si on a deux tables table1 et table2, on peut envisager d'y accéder avec sdz.chapitreQuatre.example.provider/table1 et sdz.chapitreQuatre.example.provider/table2. Ensuite, pour avoir le cinquième élément de table1, on fait sdz.chapitreQuatre.example.provider/table1/5 .

Vous pouvez avoir plusieurs segments ou faire en sorte qu'un segment ne corresponde pas à une table, c'est votre choix.

UriMatcher

Comme il existe beaucoup d'URI, il va falloir une technique pour toutes les gérer. C'est pourquoi je vais vous apprendre à utiliser UriMatcher qui analysera tout seul les URI et prendra les décisions pour vous.

On crée un UriMatcher toujours de la même manière :

1
UriMatcher membreMatcher = new UriMatcher(UriMatcher.NO_MATCH);

Cependant on n'utilisera qu'un seul UriMatcher par classe, alors on le déclarera en tant qu'attribut de type static final :

1
private static final UriMatcher membreMatcher = new UriMatcher(UriMatcher.NO_MATCH);

On va ensuite ajouter les différentes URI que pourra accepter le fournisseur, et on associera à chacune de ces URI un identifiant. De cette manière, on donnera des URI à notre UriMatcher et il déterminera tout seul le type de données associé.

Pour ajouter une URI, on utilise void addURI(String authority, String path, int code), avec l'autorité dans authority, path qui incarne le chemin (on peut mettre # pour symboliser un nombre et * pour remplacer une quelconque chaîne de caractères) et enfin code l'identifiant associé à l'URI. De plus, comme notre UriMatcher est statique, on utilise ces ajouts dans un bloc static dans la déclaration de notre classe :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class maClass {
    // Autorité de ce fournisseur
    public static final String AUTHORITY = "sdz.chapitreQuatre.provider.MembreProvider";

    private static final int DIR = 0;
    private static final int ITEM = 1;

    private static final UriMatcher membreMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        // Correspondra à content://sdz.chapitreQuatre.provider.MembreProvider/metier
        membreMatcher.addURI(AUTHORITY, "metier", DIR);
        // Correspondra à content://sdz.chapitreQuatre.provider.MembreProvider/metier/un_nombre
        membreMatcher.addURI(AUTHORITY, "metier/#", ITEM);
    }

    // ...

}

Enfin, on vérifie si une URI correspond aux filtres installés avec int match(Uri uri), la valeur retournée étant l'identifiant de l'URI analysée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
switch(membreMatcher.match(Uri.parse("content://sdz.chapitreQuatre.provider.MembreProvider/metier/5") {
  case -1:
  // Si l'URI passée ne correspond à aucun filtre
  break;

  case 0:
  // Si l'URI passée est content://sdz.chapitreQuatre.provider.MembreProvider/metier
  break;

  case 1:
  // C'est le cas ici ! Notre URI est de la forme content://sdz.chapitreQuatre.provider.MembreProvider/metier/#
  break;
}

Le type MIME

Android a besoin de connaître le type MIME des données auxquelles donne accès votre fournisseur de contenu, afin d'y accéder sans avoir à préciser leur structure ou leur implémentation. On a de ce fait besoin d'une méthode qui indique ce type (String getType(Uri uri)) dont le retour est une chaîne de caractères qui contient ce type MIME.

Cette méthode devra être capable de retourner deux formes de la même valeur en fonction de ce que veut l'utilisateur : une seule valeur ou une collection de valeurs. En effet, vous vous souvenez, un type MIME qui n'est pas officiel doit prendre sous Android la forme vnd.android.cursor.X avec X qui vaut item pour une ligne unique et dir pour une collection de lignes. Il faut ensuite une chaîne qui définira le type en lui-même, qui doit respecter la forme vnd.<nom unique>.<type>.

Voici ce que j'ai choisi :

1
2
vnd.android.cursor.item/vnd.sdz.chapitreQuatre.example.provider.table1
vnd.android.cursor.dir/vnd.sdz.chapitreQuatre.example.provider.table1

C'est ici que l'UriMatcher prendra tout son intérêt :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public static final String AUTHORITY = "sdz.chapitreQuatre.provider.MembreProvider";
public static final String TABLE_NAME = "metier";

public static final String TYPE_DIR =
    "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + TABLE_NAME;
public static final String TYPE_ITEM =
    "vnd.android.cursor.item/vnd." + AUTHORITY + "." + TABLE_NAME;

public String getType(Uri uri) {
    // Regardez dans l'exemple précédent, pour toute une table on avait la valeur 0
    if (membreMatcher.match(uri) == 0) {
        return(TYPE_DIR);
    }

    // Et si l'URI correspondait à une ligne précise dans une table, elle valait 1
    return(TYPE_ITEM);
}

Le stockage

Comment allez-vous stocker les données ? En général, on utilise une base de données, mais vous pouvez très bien opter pour un stockage sur support externe. Je vais me concentrer ici sur l'utilisation des bases de données.

On va avoir une classe qui représente la base de données et, à l'intérieur de cette classe, des classes internes constantes qui représenteront chaque table. Une classe constante est une classe déclarée avec les modificateurs static final. Cette classe contiendra des attributs constants (donc qui possèdent aussi les attributs static final) qui définissent les URI, le nom de la table, le nom de ses colonnes, les types MIME ainsi que toutes les autres données nécessaires à l'utilisation du fournisseur. L'objectif de cette classe, c'est d'être certains que les applications qui feront appel au fournisseur pourront le manipuler aisément, même si certains changements sont effectués au niveau de la valeur des URI, du nom des colonnes ou quoi que ce soit d'autre. De plus, les classes constantes aident les développeurs puisque les constantes ont des noms mnémoniques plus pratiques à utiliser que si on devait retenir toutes les valeurs.

Bien entendu, comme les développeurs n'auront pas accès au code en lui-même, c'est à vous de bien documenter le code pour qu'ils puissent utiliser vos fournisseurs de contenu.

 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
import android.content.UriMatcher;
import android.net.Uri;
import android.provider.BaseColumns;

public class MembreDatabase {
    // Autorité de ce fournisseur
    public static final String AUTHORITY = "sdz.chapitreQuatre.provider.MembreProvider";
    // Nom du fichier qui représente la base
    public static final String NAME = "membre.db";
    // Version de la base
    public static final int VERSION = 1;

    private static final int DIR = 0;
    private static final int ITEM = 1;

    private static final UriMatcher membreMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    public static final class Metier implements BaseColumns {
        static {
          membreMatcher.addURI(AUTHORITY, "metier", DIR);
          membreMatcher.addURI(AUTHORITY, "metier/#", ITEM);
        } 

        // Nom de la table
        public static final String TABLE_NAME = "metier";

        // URI
        public static final Uri CONTENT_URI =
            Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);

        // Types MIME
        public static final String TYPE_DIR =
            "vnd.android.cursor.dir/vnd." + AUTHORITY + "." + TABLE_NAME;
        public static final String TYPE_ITEM =
            "vnd.android.cursor.item/vnd." + AUTHORITY + "." + TABLE_NAME;

        // Attributs de la table
        public static final String INTITULE = "intitule";
        public static final String SALAIRE = "salaire";
    }
}

Comme vous pouvez le voir, ma classe Metier dérive de BaseColumns. Il s'agit d'une petite classe qui définit deux attributs indispensables : _ID (qui représente l'identifiant d'une ligne) et _COUNT (qui représente le nombre de lignes dans une requête).

Le Manifest

Chaque fournisseur de contenu s'enregistre sur un appareil à l'aide du Manifest. On aura besoin de préciser une autorité ainsi qu'un identifiant et la combinaison des deux se doit d'être unique. Cette combinaison n'est que la base utilisée pour constituer les requêtes de contenu. Le nœud doit être de type provider, puis on verra ensuite deux attributs : android:name pour le nom du composant (comme pour tous les composants) et android:authorities pour l'autorité.

1
2
<provider android:name=".MembreProvider"
    android:authorities="sdz.chapitreQuatre.provider.MembreProvider" />

La programmation

On fait dériver une classe de ContentProvider pour gérer les requêtes qui vont s'effectuer sur notre fournisseur de contenu. Chaque opération qu'effectuera une application sur votre fournisseur de contenu sera à gérer dans la méthode idoine. Je vais donc vous présenter le détail de chaque méthode.

boolean onCreate()

Cette méthode de callback est appelée automatiquement dès qu'un ContentResolver essaie d'y accéder pour la première fois.

Le plus important ici est d'éviter les opérations qui prennent du temps, puisqu'il s'agit du démarrage, sinon celui-ci durera trop longtemps. Je pense par exemple à éviter les initialisations qui pourraient prendre du temps (comme créer, ouvrir, mettre à jour ou analyser la base de données), de façon à permettre aux applications de se lancer plus vite, d'éviter les efforts inutiles si le fournisseur n'est pas nécessaire, d'empêcher les erreurs de base de données (comme par exemple un disque plein), ou d'arrêter le lancement de l'application. De ce fait, faites en sorte de ne jamais appeler getReadableDatabase() ou getWritableDatabase() dans cette méthode.

La meilleure chose à faire, est d'implémenter onOpen(SQLiteDatabase) comme nous avons appris à le faire, pour initialiser la base de données quand elle est ouverte pour la première fois (dès que le fournisseur reçoit une quelconque requête concernant la base).

Par exemple, vous pouvez créer un SQLiteOpenHelper dans onCreate(), mais ne créez les tables que la première fois que vous ouvrez vraiment la base. Rappelez-vous que la première fois que vous appelez getWritableDatabase() on fera automatiquement appel à onCreate() de SQLiteOpenHelper.

N'oubliez pas de retourner true si tout s'est bien déroulé.

1
2
3
4
5
6
7
8
9
public boolean onCreate() {
  // Je crée mon Handler comme nous l'avons vu dans le chapitre sur les bases de données
  mHandler = new DatabaseHandler(getContext(), VERSION);

  // Et si tout s'est bien passé, je retourne true
  return((mHandler == null) ? false : true);

  // Et voilà, on n'a pas ouvert ni touché à la base !
}

Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

Permet d'effectuer des recherches sur la base. Elle doit retourner un Cursor qui contient le résultat de la recherche ou doit lancer une exception en cas de problème. S'il n'y a pas de résultat qui correspond à la recherche, alors il faut renvoyer un Cursor vide, et non null, qui est plutôt réservé aux erreurs.

1
2
3
4
5
6
7
8
9
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
  SQLiteQueryBuilder builder = new SQLiteQueryBuilder();

  builder.setTables(DatabaseHandler.METIER_TABLE_NAME);

  Cursor c = builder.query(mHandler.getReadableDatabase(), projection, selection, selectionArgs, null, null, sort);
  c.setNotificationUri(getContext().getContentResolver(), url);
  return(c);
}

Uri insert(Uri uri, ContentValues values)

On l'utilise pour insérer des données dans le fournisseur. Elle doit retourner l'URI de la nouvelle ligne. Comme vous le savez déjà, ce type d'URI doit être constitué de l'URI qui caractérise la table suivie de l'identifiant de la ligne.

Afin d'alerter les éventuels observateurs qui suivent le fournisseur, on indique que l'ensemble des données a changé avec la méthode void notifyChange(Uri uri, ContentObserver observer), uri indiquant les données qui ont changé et observer valant null.

1
2
3
4
5
6
7
8
9
public Uri insert (Uri url, ContentValues initialValues) {
  long id = mHandler.getWritableDatabase().insert(DatabaseHandler.METIER_TABLE_NAME,    DatabaseHandler.METIER_KEY,    initialValues);
  if (id > -1) {
    Uri uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
    getContext().getContentResolver().notifyChange(uri, null);
    return uri;
  }
  return null;
}

int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)

Met à jour des données dans le fournisseur. Il faut retourner le nombre de lignes modifiées. N'oubliez pas d'alerter les observateurs avec notifyChange() encore une fois.

1
2
3
4
5
public int update (Uri url, ContentValues values, String where, String[] whereArgs) {
  int count = mHandler.getWritableDatabase().update(DatabaseHandler.METIER_TABLE_NAME, values, where, whereArgs);
  getContext().getContentResolver().notifyChange(url, null);
  return count;
}

int delete(Uri uri, String selection, String[] selectionArgs)

Supprime des éléments du fournisseur et doit retourner le nombre de lignes supprimées. Pour alerter les observateurs, utilisez encore une fois void notifyChange(Uri uri, ContentObserver observer).

1
2
3
4
5
public int delete(Uri url, String where, String[] whereArgs) {
  int count = mHandler.getWritableDatabase().delete(DatabaseHandler.METIER_TABLE_NAME, where, whereArgs);
  getContext().getContentResolver().notifyChange(url, null);
  return count;
}

String getType(Uri uri)

Retourne le type MIME des données concernées par uri. Vous connaissez déjà cette méthode par cœur !

1
2
3
4
5
6
7
public String getType(Uri uri) {
  if (membreMatcher.match(uri) == 0) {
    return(TYPE_DIR);
  }

  return(TYPE_ITEM);
}

  • Les fournisseurs de contenu permettent de rendre accessibles les données d'une application sans connaitre son moyen de stockage.
  • Pour accéder à un fournisseur de contenu, vous êtes obligés de passer par un objet client ContentResolver.
  • L'URI pour un fournisseur de contenu est sous la forme suivante : un schéma et l'information :
    • Le schéma d'un fournisseur de contenu est content et s'écrit content://.
    • L'information est un chemin qui devient de plus en plus précis à force de rentrer dans des parties spécifiques. La partie de l'information qui permet de pointer de manière unique vers le bon fournisseur de contenu est l'autorité. Quant à l'affinement de la requête par des "/", cela s'appelle des segments.
  • Il n'est pas rare qu'une application n'offre pas de fournisseur de contenus. Il y a plusieurs raisons pour lesquelles vous pourrez en développer un :
    • Vous voulez permettre à d'autres applications d'accéder à des données complexes ou certains fichiers.
    • Vous voulez permettre à d'autres applications de pouvoir copier des données complexes qui vous appartiennent.
    • Vous voulez peut-être aussi permettre à d'autres applications de faire des recherches sur vos données complexes.