• 20 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

J'ai tout compris !

Mis à jour le 30/04/2018

Exposez vos données avec un ContentProvider

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Maintenant que votre application est fonctionnelle, il serait intéressant de pouvoir exposer le contenu de sa base de données SQLite, de manière sécurisée, à d'autres applications que la vôtre. 

D'accord, mais je ne comprends pas trop pourquoi nous aurions besoin de faire cela ? :euh:

Eh bien parfois, une application Android peut proposer, publiquement et de manière sécurisée, l'accès à certaines de ses données. Par exemple, dans notre cas, imaginez qu'une entreprise comme TripAdvisor ou Booking.com vous contacte et vous propose un partenariat rémunéré pour consulter la liste des choses à faire de chacun de vos utilisateurs, afin de leur proposer des réductions et des bons plans via leur application mobile : vous aurez alors besoin de fournir un accès sécurisé et maîtrisé à votre base de données SQLite ! :)

Cette exposition est possible sur Android grâce à un Content Provider, permettant de partager avec d'autres applications que la vôtre, un contenu que vous aurez préalablement défini.

Aperçu du fonctionnement d'un Content Provider
Aperçu du fonctionnement d'un Content Provider

Dans un précédent chapitre, nous avons déjà vu la manière d'exposer un fichier publiquement grâce à la classe FileProvider, héritant de la classe ContentProvider.

Cette fois-ci, afin d'exposer notre base de données SQLite, nous allons utiliser directement la classe ContentProvider... :) Une fois ce dernier configuré, les applications externes devront, dans le but d'accéder aux données exposées, lui fournir une URI que ce dernier analysera pour retourner les données appropriées.

Ainsi, dans un premier temps, créez le package provider/ dans lequel vous créerez la classe ItemContentProvider.

Classe provider/ItemContentProvider.java :

public class ItemContentProvider extends ContentProvider {

    @Override
    public boolean onCreate() { return true; }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }
}

Explications : Nous avons créé ici une classe, ItemContentProvider, héritant de ContentProvider, dont le but sera d'exposer les données de notre base de données SQLite. Une classe héritant de ContentProvider doit absolument implémenter les 6 méthodes suivantes : 

  • onCreate() : Représente le point d'entrée du Content Provider. Vous pourrez donc y initialiser différentes variables qui vous serviront par la suite.

  • query(): Cette méthode permettra, à partir d'une URI renseignée, de récupérer les données (via un Cursor) depuis la destination de votre choix (dans notre cas, notre base de données SQLite).

  • getType(): Cette méthode permet de retourner le type MIME associé à l'URI permettant d'identifier plus précisément le type des données qui seront retournées.

  • insert(): Cette méthode permettra, à partir d'une URI renseignée, d'insérer des données au format ContentValues dans la destination de notre choix (dans notre cas, notre base de données SQLite).

  • delete(): Cette méthode permettra, à partir d'une URI renseignée, de supprimer des données au format ContentValues de la destination de notre choix (dans notre cas, notre base de données SQLite).

  • update():  Cette méthode permettra, à partir d'une URI renseignée, de mettre à jour des données au format ContentValues dans la destination de notre choix (dans notre cas, notre base de données SQLite).

Pour le moment, ces méthodes sont vides... mais ne vous inquiétez pas, nous les remplirons tout à l'heure ! :) Avant cela, nous devons déclarer notre ContentProvider dans le manifeste de notre application afin de l'activer.

Extrait de AndroidManifest.xml :

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.openclassrooms.savemytrip">

    ...

    <application
        ...>

        ...

        <provider
            android:name=".provider.ItemContentProvider"
            android:authorities="com.openclassrooms.savemytrip.provider"
            android:exported="true"/>
            
    </application>

</manifest>

Explications : Sur le même principe que ce que nous avons fait dans le chapitre précédent concernant le FileProvider, nous devons déclarer le ContentProvider dans le manifeste de notre application afin de le rendre accessible aux autres applications.

Maintenant que notre ContentProvider est correctement déclaré, nous allons pouvoir le configurer pour lui donner accès à notre base de données SQLite, et notamment à notre table Item. Pour cela, nous allons modifier légèrement le DAO gérant notre table Item afin de pouvoir retourner un objet de type Cursor, plus facilement manipulable par le ContentProvider.

Extrait de ItemDao.java :

@Dao
public interface ItemDao {

    @Query("SELECT * FROM Item WHERE userId = :userId")
    Cursor getItemsWithCursor(long userId);

    ...
}

Puis, nous allons modifier notre classe de modèle Item afin de créer une méthode publique, lui permettant de transformer un objet de type ContentValues en un objet Item.

Extrait de Item.java :

public class Item {

    ...
    
    // --- UTILS ---
    public static Item fromContentValues(ContentValues values) {
        final Item item = new Item();
        if (values.containsKey("text")) item.setText(values.getAsString("text"));
        if (values.containsKey("category")) item.setCategory(values.getAsInteger("category"));
        if (values.containsKey("isSelected")) item.setSelected(values.getAsBoolean("isSelected"));
        if (values.containsKey("userId")) item.setUserId(values.getAsLong("userId"));
        return item;
    }
}

Explications : La classe ContentValues est en réalité un dictionnaire assez basique qui nous permet de récupérer une valeur à partir d'une clé... :) Nous placerons ensuite cette valeur dans la propriété correspondante de l'objet Item.

Modifions maintenant notre ContentProvider afin d'exposer notre table Item.

Classe ItemContentProvider.java : 

public class ItemContentProvider extends ContentProvider {

    // FOR DATA
    public static final String AUTHORITY = "com.openclassrooms.savemytrip.provider";
    public static final String TABLE_NAME = Item.class.getSimpleName();
    public static final Uri URI_ITEM = Uri.parse("content://" + AUTHORITY + "/" + TABLE_NAME);

    @Override
    public boolean onCreate() { return true; }

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

        if (getContext() != null){
            long userId = ContentUris.parseId(uri);
            final Cursor cursor = SaveMyTripDatabase.getInstance(getContext()).itemDao().getItemsWithCursor(userId);
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;
        }

        throw new IllegalArgumentException("Failed to query row for uri " + uri);
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return "vnd.android.cursor.item/" + AUTHORITY + "." + TABLE_NAME;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {

        if (getContext() != null){
            final long id = SaveMyTripDatabase.getInstance(getContext()).itemDao().insertItem(Item.fromContentValues(contentValues));
            if (id != 0){
                getContext().getContentResolver().notifyChange(uri, null);
                return ContentUris.withAppendedId(uri, id);
            }
        }

        throw new IllegalArgumentException("Failed to insert row into " + uri);
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        if (getContext() != null){
            final int count = SaveMyTripDatabase.getInstance(getContext()).itemDao().deleteItem(ContentUris.parseId(uri));
            getContext().getContentResolver().notifyChange(uri, null);
            return count;
        }
        throw new IllegalArgumentException("Failed to delete row into " + uri);
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
        if (getContext() != null){
            final int count = SaveMyTripDatabase.getInstance(getContext()).itemDao().updateItem(Item.fromContentValues(contentValues));
            getContext().getContentResolver().notifyChange(uri, null);
            return count;
        }
        throw new IllegalArgumentException("Failed to update row into " + uri);
    }
}

Explications : Nous avons ici rempli toutes les méthodes de notre ContentProvider. Avant cela, nous avons créé des variables de classe statiques publiques. Elles nous serviront à identifier l'autorité définissant notre ContentProvider, le nom de la table que nous interrogerons, ainsi que l'URI de base qu'il faudra renseigner pour communiquer avec ce dernier.

De manière générale, vous remarquerez que nous suivons une certaine logique pour chacune des méthodes de notre ContentProvider :

  1. Dans un premier temps, nous effectuons une opération (création, récupération, mise à jour et suppression) sur notre base de données SQLite, et plus particulièrement sur la table Item, grâce à notre objet SaveMyTripDatabase qui appellera le DAO ItemDao.

  2. Puis dans un second temps, nous renvoyons ou mettons à jour l'URI de la ressource manipulée afin d'informer l'utilisateur que l'opération s'est bien déroulée.

Pour les méthodes  update  ou  delete , nous renvoyons le nombre de lignes qui ont été impactées par l'opération en question.

A la fin de chacune de nos méthodes, nous renvoyons une exception  IllegalArgumentException  si jamais le ContentProvider ne peut effectuer l'opération jusqu'au bout.

D'accord ! En revanche, je me demande comment on va faire pour tester notre ContentProvider... Il faut que je crée une seconde application, c'est ça ? :)

En fait, il y a un moyen un peu plus simple de réaliser cela... notamment grâce aux tests instrumentalisés !:D Ainsi, j'ai créé pour vous une classe de test, ItemContentProviderTest, qui nous permettra facilement de vérifier si notre ContentProvider fonctionne bien.

Classe ItemContentProviderTest.java :

@RunWith(AndroidJUnit4.class)
public class ItemContentProviderTest {

    // FOR DATA
    private ContentResolver mContentResolver;

    // DATA SET FOR TEST
    private static long USER_ID = 1;

    @Before
    public void setUp() {
        Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getContext(),
                SaveMyTripDatabase.class)
                .allowMainThreadQueries()
                .build();
        mContentResolver = InstrumentationRegistry.getContext().getContentResolver();
    }

    @Test
    public void getItemsWhenNoItemInserted() {
        final Cursor cursor = mContentResolver.query(ContentUris.withAppendedId(ItemContentProvider.URI_ITEM, USER_ID), null, null, null, null);
        assertThat(cursor, notNullValue());
        assertThat(cursor.getCount(), is(0));
        cursor.close();
    }

    @Test
    public void insertAndGetItem() {
        // BEFORE : Adding demo item
        final Uri userUri = mContentResolver.insert(ItemContentProvider.URI_ITEM, generateItem());
        // TEST
        final Cursor cursor = mContentResolver.query(ContentUris.withAppendedId(ItemContentProvider.URI_ITEM, USER_ID), null, null, null, null);
        assertThat(cursor, notNullValue());
        assertThat(cursor.getCount(), is(1));
        assertThat(cursor.moveToFirst(), is(true));
        assertThat(cursor.getString(cursor.getColumnIndexOrThrow("text")), is("Visite cet endroit de rêve !"));
    }

    // ---

    private ContentValues generateItem(){
        final ContentValues values = new ContentValues();
        values.put("text", "Visite cet endroit de rêve !");
        values.put("category", "0");
        values.put("isSelected", "false");
        values.put("userId", "1");
        return values;
    }
}

Explications : Nous avons créé un test instrumentalisé qui jouera le rôle de l'application externe souhaitant accéder aux données de notre application. Pour cela, nous avons utilisé la classe ContentResolver, permettant à partir d'une URI, de communiquer avec notre ContentProvider en appelant ses différentes méthodes.

Exécutez maintenant ce test. Une fois ce dernier terminé, si vous lancez votre application, vous devriez voir qu'une nouvelle "chose à faire" a été ajoutée dans votre liste... par votre test... via votre ContentProvider ! :)

Plutôt sympa non ? Notre test a joué le rôle de l'application externe et a ajouté des vraies données à l'intérieur de notre base de données SQLite !

Exemple de certificat de réussite
Exemple de certificat de réussite