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.
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()
: permet, à 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()
: 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()
: permet, à 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()
: permet, à 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()
: permet, à 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 {
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 && contentValues != 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 && contentValues != 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 rempli ici 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 :
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.
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.
À 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.getInstrumentation().getContext()
,
SaveMyTripDatabase.class)
.allowMainThreadQueries()
.build();
mContentResolver = InstrumentationRegistry.getInstrumentation().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é de vraies données à l'intérieur de notre base de données SQLite !
En résumé
Le ContentProvider reprend les mêmes principes que le FileProvider. Il permet d’exposer, dans notre cas, une base de données.
Nous pouvons ainsi récupérer, insérer, supprimer ou modifier notre base depuis l’extérieur de notre application.
Pour communiquer avec notre ContentProvider, on utilise un ContentResolver qui prend en paramètre une URI correspondant au provider cible et aux paramètres à envoyer pour exécuter une requête.
Parfait ! Vous savez maintenant comment gérer vos données localement pour avoir une application 100% hors-ligne et partager vos données aux autres applications de façon sécurisée !
Dans le prochain chapitre, nous allons voir comment améliorer la qualité de notre code avec l’intégration continue. Vous allez voir, c’est passionnant ! 🤓