Principes de base des fournisseurs de contenu

Un fournisseur de contenu gère l'accès à un dépôt central de données. Un fournisseur fait partie d'une application Android, qui fournit souvent sa propre UI pour travailler avec les données. Cependant, les fournisseurs de contenu sont principalement utilisés par d'autres applications, qui accèdent au fournisseur à l'aide d'un objet client de fournisseur. Ensemble, les fournisseurs et les clients de fournisseurs offrent une interface standard cohérente aux données, qui gère également la communication interprocessus et l'accès sécurisé aux données.

En règle générale, vous travaillez avec des fournisseurs de contenu dans l'un des deux scénarios suivants: implémenter du code pour accéder à un fournisseur de contenu existant dans une autre application ou créer un fournisseur de contenu dans votre application pour partager des données avec d'autres applications.

Cette page présente les principes de base de la collaboration avec des fournisseurs de contenu existants. Pour découvrir comment implémenter des fournisseurs de contenu dans vos propres applications, consultez la section Créer un fournisseur de contenu.

Cet article décrit les éléments suivants:

  • Fonctionnement des fournisseurs de contenu
  • API que vous utilisez pour récupérer des données auprès d'un fournisseur de contenu.
  • API que vous utilisez pour insérer, mettre à jour ou supprimer des données dans un fournisseur de contenu.
  • Autres fonctionnalités de l'API qui facilitent la collaboration avec les fournisseurs

Présentation

Un fournisseur de contenu présente les données aux applications externes sous la forme d'une ou de plusieurs tables semblables à celles d'une base de données relationnelle. Une ligne représente une instance d'un type de données collectées par le fournisseur, et chaque colonne de la ligne représente une partie de données collectées pour une instance.

Un fournisseur de contenu coordonne l'accès à la couche de stockage des données de votre application pour un certain nombre d'API et de composants. Comme illustré dans la figure 1, ces éléments incluent les suivants:

  • Partager l'accès aux données de votre application avec d'autres applications
  • Envoyer des données à un widget
  • Renvoyer des suggestions de recherche personnalisées pour votre application via le framework de recherche à l'aide de SearchRecentSuggestionsProvider
  • Synchroniser des données d'application avec votre serveur à l'aide d'une implémentation de AbstractThreadedSyncAdapter
  • Charger des données dans votre interface utilisateur à l'aide d'un CursorLoader
Relation entre le fournisseur de contenu et les autres composants.

Figure 1 : Relation entre un fournisseur de contenu et d'autres composants.

Accéder à un fournisseur

Lorsque vous souhaitez accéder aux données d'un fournisseur de contenu, vous utilisez l'objet ContentResolver dans le fichier Context de votre application pour communiquer avec le fournisseur en tant que client. L'objet ContentResolver communique avec l'objet du fournisseur, une instance d'une classe qui implémente ContentProvider.

L'objet du fournisseur reçoit les requêtes de données des clients, effectue l'action demandée et renvoie les résultats. Cet objet comporte des méthodes qui appellent des méthodes portant un nom identique dans l'objet fournisseur, une instance de l'une des sous-classes concrètes de ContentProvider. Les méthodes ContentResolver fournissent les fonctions de base "CRUD" (créer, récupérer, mettre à jour et supprimer) du stockage persistant.

Un modèle courant pour accéder à un ContentProvider à partir de votre UI utilise un CursorLoader pour exécuter une requête asynchrone en arrière-plan. Le Activity ou le Fragment de votre UI appelle un CursorLoader à la requête, qui à son tour obtient le ContentProvider à l'aide du ContentResolver.

L'UI reste ainsi disponible pour l'utilisateur pendant l'exécution de la requête. Ce modèle implique l'interaction d'un certain nombre d'objets différents, ainsi que le mécanisme de stockage sous-jacent, comme illustré dans la figure 2.

Interaction entre ContentProvider, d'autres classes et stockage.

Figure 2. Interaction entre ContentProvider, d'autres classes et le stockage.

Remarque:Pour accéder à un fournisseur, votre application doit généralement demander des autorisations spécifiques dans son fichier manifeste. Ce modèle de développement est décrit plus en détail dans la section Autorisations du fournisseur de contenu.

L'un des fournisseurs intégrés à la plate-forme Android est le fournisseur de dictionnaire utilisateur, qui stocke les mots non standards que l'utilisateur souhaite conserver. Le tableau 1 illustre à quoi peuvent ressembler les données dans le tableau de ce fournisseur:

Tableau 1:exemple de table avec le dictionnaire personnel.

word identifiant d'application de publication locale _ID
mapreduce user1 100 fr_FR 1
precompiler user14 200 fr_FR 2
applet user2 225 fr_CA 3
const utilisateur1 255 pt_BR 4
int user5 100 en_UK 5

Dans le tableau 1, chaque ligne représente une instance d'un mot qui ne figure pas dans un dictionnaire standard. Chaque colonne représente une partie des données pour ce mot, comme la langue dans laquelle il a été rencontré pour la première fois. Les en-têtes de colonne sont des noms de colonne stockés dans le fournisseur. Par exemple, pour faire référence à la langue d'une ligne, vous devez vous reporter à sa colonne locale. Pour ce fournisseur, la colonne _ID sert de colonne de clé primaire qu'il gère automatiquement.

Pour obtenir la liste des mots et de leurs paramètres régionaux à partir du fournisseur de dictionnaire utilisateur, appelez ContentResolver.query(). La méthode query() appelle la méthode ContentProvider.query() définie par le fournisseur de dictionnaire utilisateur. Les lignes de code suivantes montrent un appel ContentResolver.query():

Kotlin

// Queries the UserDictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

Java

// Queries the UserDictionary and returns results
cursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // The content URI of the words table
    projection,                        // The columns to return for each row
    selectionClause,                   // Selection criteria
    selectionArgs,                     // Selection criteria
    sortOrder);                        // The sort order for the returned rows

Le tableau 2 montre comment les arguments de query(Uri,projection,selection,selectionArgs,sortOrder) correspondent à une instruction SELECT SQL:

Tableau 2:comparaison de query() avec une requête SQL.

Argument query() SELECT mot clé/paramètre Notes
Uri FROM table_name Uri est mappé sur la table du fournisseur nommé table_name.
projection col,col,col,... projection est un tableau de colonnes inclus pour chaque ligne récupérée.
selection WHERE col = value selection spécifie les critères de sélection des lignes.
selectionArgs Aucun équivalent exact. Les arguments de sélection remplacent les espaces réservés ? dans la clause de sélection.
sortOrder ORDER BY col,col,... sortOrder spécifie l'ordre dans lequel les lignes apparaissent dans le Cursor renvoyé.

URI de contenu

Un URI de contenu est un URI qui identifie les données d'un fournisseur. Les URI de contenu incluent le nom symbolique du fournisseur entier (son autorité) et un nom qui pointe vers une table (un chemin d'accès). Lorsque vous appelez une méthode cliente pour accéder à une table dans un fournisseur, l'URI de contenu de la table est l'un des arguments.

Dans les lignes de code précédentes, la constante CONTENT_URI contient l'URI de contenu de la table Words du fournisseur de dictionnaires personnels. L'objet ContentResolver analyse l'autorité de l'URI et l'utilise pour résoudre le fournisseur en comparant l'autorité à une table système de fournisseurs connus. ContentResolver peut ensuite distribuer les arguments de requête au fournisseur approprié.

ContentProvider utilise la partie chemin d'accès de l'URI de contenu pour choisir la table à laquelle accéder. Un fournisseur dispose généralement d'un chemin d'accès pour chaque table qu'il expose.

Dans les lignes de code précédentes, l'URI complet de la table Words est le suivant:

content://user_dictionary/words
  • La chaîne content:// correspond au schéma, qui est toujours présent et l'identifie en tant qu'URI de contenu.
  • La chaîne user_dictionary représente l'autorité du fournisseur.
  • La chaîne words correspond au chemin d'accès à la table.

De nombreux fournisseurs vous permettent d'accéder à une seule ligne d'une table en ajoutant une valeur d'ID à la fin de l'URI. Par exemple, pour récupérer une ligne dont le _ID est 4 à partir du fournisseur de dictionnaire utilisateur, vous pouvez utiliser cet URI de contenu:

Kotlin

val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)

Java

Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);

Vous utilisez souvent des valeurs d'ID lorsque vous récupérez un ensemble de lignes et que vous souhaitez en modifier ou en supprimer une.

Remarque:Les classes Uri et Uri.Builder contiennent des méthodes pratiques pour créer des objets URI valides à partir de chaînes. La classe ContentUris contient des méthodes pratiques pour ajouter des valeurs d'ID à un URI. L'extrait de code précédent utilise withAppendedId() pour ajouter un ID à l'URI de contenu du fournisseur de dictionnaire utilisateur.

Récupérer les données du fournisseur

Cette section explique comment récupérer des données auprès d'un fournisseur, en utilisant le fournisseur de dictionnaire utilisateur comme exemple.

Par souci de clarté, les extraits de code de cette section appellent ContentResolver.query() sur le thread d'UI. Dans le code réel, effectuez toutefois des requêtes de manière asynchrone sur un thread distinct. Vous pouvez utiliser la classe CursorLoader, qui est décrite plus en détail dans le guide des chargeurs. De plus, les lignes de code ne sont que des extraits. Elles n'affichent pas une application complète.

Pour récupérer des données auprès d'un fournisseur, procédez comme suit:

  1. Demandez l'autorisation d'accès en lecture pour le fournisseur.
  2. Définissez le code qui envoie une requête au fournisseur.

Demander l'autorisation d'accès en lecture

Pour récupérer des données auprès d'un fournisseur, votre application doit disposer de l'autorisation d'accès en lecture pour ce fournisseur. Vous ne pouvez pas demander cette autorisation au moment de l'exécution. À la place, vous devez spécifier que vous avez besoin de cette autorisation dans le fichier manifeste, à l'aide de l'élément <uses-permission> et du nom exact de l'autorisation défini par le fournisseur.

Lorsque vous spécifiez cet élément dans votre fichier manifeste, vous demandez cette autorisation pour votre application. Lorsque les utilisateurs installent votre application, ils acceptent implicitement cette demande.

Pour trouver le nom exact de l'autorisation d'accès en lecture du fournisseur que vous utilisez, ainsi que les noms des autres autorisations d'accès utilisées par le fournisseur, consultez la documentation du fournisseur.

Le rôle des autorisations dans l'accès aux fournisseurs est décrit plus en détail dans la section Autorisations du fournisseur de contenu.

Le fournisseur de dictionnaire utilisateur définit l'autorisation android.permission.READ_USER_DICTIONARY dans son fichier manifeste. Par conséquent, une application qui souhaite lire à partir du fournisseur doit demander cette autorisation.

Créer la requête

L'étape suivante consiste à créer une requête pour récupérer des données auprès d'un fournisseur. L'extrait de code suivant définit certaines variables pour accéder au fournisseur de dictionnaire utilisateur:

Kotlin

// A "projection" defines the columns that are returned for each row
private val mProjection: Array<String> = arrayOf(
        UserDictionary.Words._ID,    // Contract class constant for the _ID column name
        UserDictionary.Words.WORD,   // Contract class constant for the word column name
        UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
)

// Defines a string to contain the selection clause
private var selectionClause: String? = null

// Declares an array to contain selection arguments
private lateinit var selectionArgs: Array<String>

Java

// A "projection" defines the columns that are returned for each row
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};

// Defines a string to contain the selection clause
String selectionClause = null;

// Initializes an array to contain selection arguments
String[] selectionArgs = {""};

L'extrait suivant montre comment utiliser ContentResolver.query(), en prenant comme exemple le fournisseur de dictionnaire personnel. Une requête client du fournisseur est semblable à une requête SQL. Elle contient un ensemble de colonnes à renvoyer, un ensemble de critères de sélection et un ordre de tri.

L'ensemble de colonnes renvoyées par la requête est appelé projection, et la variable est mProjection.

L'expression qui spécifie les lignes à récupérer est divisée en une clause de sélection et en arguments de sélection. La clause de sélection est une combinaison d'expressions logiques et booléennes, de noms de colonnes et de valeurs. La variable est mSelectionClause. Si vous spécifiez le paramètre remplaçable ? au lieu d'une valeur, la méthode de requête récupère la valeur à partir du tableau des arguments de sélection, qui est la variable mSelectionArgs.

Dans l'extrait suivant, si l'utilisateur ne saisit pas de mot, la clause de sélection est définie sur null et la requête renvoie tous les mots du fournisseur. Si l'utilisateur saisit un mot, la clause de sélection est définie sur UserDictionary.Words.WORD + " = ?" et le premier élément du tableau des arguments de sélection est défini sur le mot saisi par l'utilisateur.

Kotlin

/*
 * This declares a String array to contain the selection arguments.
 */
private lateinit var selectionArgs: Array<String>

// Gets a word from the UI
searchString = searchWord.text.toString()

// Insert code here to check for invalid or malicious input

// If the word is the empty string, gets everything
selectionArgs = searchString?.takeIf { it.isNotEmpty() }?.let {
    selectionClause = "${UserDictionary.Words.WORD} = ?"
    arrayOf(it)
} ?: run {
    selectionClause = null
    emptyArray<String>()
}

// Does a query against the table and returns a Cursor object
mCursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI, // The content URI of the words table
        projection,                       // The columns to return for each row
        selectionClause,                  // Either null or the word the user entered
        selectionArgs,                    // Either empty or the string the user entered
        sortOrder                         // The sort order for the returned rows
)

// Some providers return null if an error occurs, others throw an exception
when (mCursor?.count) {
    null -> {
        /*
         * Insert code here to handle the error. Be sure not to use the cursor!
         * You might want to call android.util.Log.e() to log this error.
         */
    }
    0 -> {
        /*
         * Insert code here to notify the user that the search is unsuccessful. This isn't
         * necessarily an error. You might want to offer the user the option to insert a new
         * row, or re-type the search term.
         */
    }
    else -> {
        // Insert code here to do something with the results
    }
}

Java

/*
 * This defines a one-element String array to contain the selection argument.
 */
String[] selectionArgs = {""};

// Gets a word from the UI
searchString = searchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input

// If the word is the empty string, gets everything
if (TextUtils.isEmpty(searchString)) {
    // Setting the selection clause to null returns all words
    selectionClause = null;
    selectionArgs[0] = "";

} else {
    // Constructs a selection clause that matches the word that the user entered
    selectionClause = UserDictionary.Words.WORD + " = ?";

    // Moves the user's input string to the selection arguments
    selectionArgs[0] = searchString;

}

// Does a query against the table and returns a Cursor object
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI, // The content URI of the words table
    projection,                       // The columns to return for each row
    selectionClause,                  // Either null or the word the user entered
    selectionArgs,                    // Either empty or the string the user entered
    sortOrder);                       // The sort order for the returned rows

// Some providers return null if an error occurs, others throw an exception
if (null == mCursor) {
    /*
     * Insert code here to handle the error. Be sure not to use the cursor! You can
     * call android.util.Log.e() to log this error.
     *
     */
// If the Cursor is empty, the provider found no matches
} else if (mCursor.getCount() < 1) {

    /*
     * Insert code here to notify the user that the search is unsuccessful. This isn't necessarily
     * an error. You can offer the user the option to insert a new row, or re-type the
     * search term.
     */

} else {
    // Insert code here to do something with the results

}

Cette requête est semblable à l'instruction SQL suivante:

SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

Dans cette instruction SQL, les noms de colonne réels sont utilisés au lieu des constantes de classe de contrat.

Protéger contre les entrées malveillantes

Si les données gérées par le fournisseur de contenu se trouvent dans une base de données SQL, l'inclusion de données externes non approuvées dans des instructions SQL brutes peut entraîner une injection SQL.

Prenons la clause de sélection suivante:

Kotlin

// Constructs a selection clause by concatenating the user's input to the column name
var selectionClause = "var = $mUserInput"

Java

// Constructs a selection clause by concatenating the user's input to the column name
String selectionClause = "var = " + userInput;

Vous autorisez ainsi l'utilisateur à concatenar du code SQL malveillant à votre instruction SQL. Par exemple, l'utilisateur peut saisir "nothing; DROP TABLE *;" pour mUserInput, ce qui génère la clause de sélection var = nothing; DROP TABLE *;.

La clause de sélection étant traitée comme une instruction SQL, le fournisseur peut effacer toutes les tables de la base de données SQLite sous-jacente, sauf s'il est configuré pour intercepter les tentatives d'injection SQL.

Pour éviter ce problème, utilisez une clause de sélection qui utilise ? comme paramètre remplaçable et un tableau distinct d'arguments de sélection. De cette manière, l'entrée utilisateur est directement liée à la requête plutôt que d'être interprétée dans une instruction SQL. Comme elle n'est pas traitée comme du code SQL, l'entrée utilisateur ne peut pas injecter du code SQL malveillant. Au lieu d'utiliser la concaténation pour inclure la saisie de l'utilisateur, utilisez cette clause de sélection:

Kotlin

// Constructs a selection clause with a replaceable parameter
var selectionClause = "var = ?"

Java

// Constructs a selection clause with a replaceable parameter
String selectionClause =  "var = ?";

Configurez le tableau d'arguments de sélection comme suit:

Kotlin

// Defines a mutable list to contain the selection arguments
var selectionArgs: MutableList<String> = mutableListOf()

Java

// Defines an array to contain the selection arguments
String[] selectionArgs = {""};

Insérez une valeur dans le tableau des arguments de sélection comme suit:

Kotlin

// Adds the user's input to the selection argument
selectionArgs += userInput

Java

// Sets the selection argument to the user's input
selectionArgs[0] = userInput;

Une clause de sélection qui utilise ? comme paramètre remplaçable et un tableau d'arguments de sélection est le moyen privilégié de spécifier une sélection, même si le fournisseur n'est pas basé sur une base de données SQL.

Afficher les résultats de la requête

La méthode cliente ContentResolver.query() renvoie toujours un Cursor contenant les colonnes spécifiées par la projection de la requête pour les lignes correspondant aux critères de sélection de la requête. Un objet Cursor fournit un accès en lecture aléatoire aux lignes et aux colonnes qu'il contient.

À l'aide des méthodes Cursor, vous pouvez itérer les lignes des résultats, déterminer le type de données de chaque colonne, extraire les données d'une colonne et examiner les autres propriétés des résultats.

Certaines implémentations de Cursor mettent à jour automatiquement l'objet lorsque les données du fournisseur changent, déclenchent des méthodes dans un objet observateur lorsque Cursor change, ou les deux.

Remarque:Un fournisseur peut limiter l'accès aux colonnes en fonction de la nature de l'objet qui effectue la requête. Par exemple, le fournisseur de contacts limite l'accès de certaines colonnes aux adaptateurs de synchronisation. Il ne les renvoie donc pas vers une activité ou un service.

Si aucune ligne ne correspond aux critères de sélection, le fournisseur renvoie un objet Cursor pour lequel Cursor.getCount() est défini sur 0, c'est-à-dire un curseur vide.

Si une erreur interne se produit, les résultats de la requête dépendent du fournisseur concerné. Il peut renvoyer null ou générer une exception Exception.

Étant donné qu'un Cursor est une liste de lignes, un bon moyen d'afficher le contenu d'un Cursor consiste à l'associer à un ListView à l'aide d'un SimpleCursorAdapter.

L'extrait de code suivant poursuit le code de l'extrait précédent. Elle crée un objet SimpleCursorAdapter contenant le Cursor récupéré par la requête et le définit en tant qu'adaptateur pour une ListView.

Kotlin

// Defines a list of columns to retrieve from the Cursor and load into an output row
val wordListColumns : Array<String> = arrayOf(
        UserDictionary.Words.WORD,      // Contract class constant containing the word column name
        UserDictionary.Words.LOCALE     // Contract class constant containing the locale column name
)

// Defines a list of View IDs that receive the Cursor columns for each row
val wordListItems = intArrayOf(R.id.dictWord, R.id.locale)

// Creates a new SimpleCursorAdapter
cursorAdapter = SimpleCursorAdapter(
        applicationContext,             // The application's Context object
        R.layout.wordlistrow,           // A layout in XML for one row in the ListView
        mCursor,                        // The result from the query
        wordListColumns,                // A string array of column names in the cursor
        wordListItems,                  // An integer array of view IDs in the row layout
        0                               // Flags (usually none are needed)
)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter)

Java

// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] wordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};

// Defines a list of View IDs that receive the Cursor columns for each row
int[] wordListItems = { R.id.dictWord, R.id.locale};

// Creates a new SimpleCursorAdapter
cursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // The application's Context object
    R.layout.wordlistrow,                  // A layout in XML for one row in the ListView
    mCursor,                               // The result from the query
    wordListColumns,                       // A string array of column names in the cursor
    wordListItems,                         // An integer array of view IDs in the row layout
    0);                                    // Flags (usually none are needed)

// Sets the adapter for the ListView
wordList.setAdapter(cursorAdapter);

Remarque:Pour remplacer un ListView par un Cursor, le curseur doit contenir une colonne nommée _ID. Par conséquent, la requête affichée précédemment récupère la colonne _ID pour la table Words, même si ListView ne l'affiche pas. Cette restriction explique également pourquoi la plupart des fournisseurs disposent d'une colonne _ID pour chacune de leurs tables.

Obtenir des données à partir des résultats de requête

En plus d'afficher les résultats de requête, vous pouvez les utiliser pour d'autres tâches. Par exemple, vous pouvez récupérer des orthographes auprès du fournisseur de dictionnaire utilisateur, puis les rechercher dans d'autres fournisseurs. Pour ce faire, itérez sur les lignes de Cursor, comme illustré dans l'exemple suivant:

Kotlin

/*
* Only executes if the cursor is valid. The User Dictionary Provider returns null if
* an internal error occurs. Other providers might throw an Exception instead of returning null.
*/
mCursor?.apply {
    // Determine the column index of the column named "word"
    val index: Int = getColumnIndex(UserDictionary.Words.WORD)

    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you get an
     * exception.
     */
    while (moveToNext()) {
        // Gets the value from the column
        newWord = getString(index)

        // Insert code here to process the retrieved word
        ...
        // End of while loop
    }
}

Java

// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/*
 * Only executes if the cursor is valid. The User Dictionary Provider returns null if
 * an internal error occurs. Other providers might throw an Exception instead of returning null.
 */

if (mCursor != null) {
    /*
     * Moves to the next row in the cursor. Before the first movement in the cursor, the
     * "row pointer" is -1, and if you try to retrieve data at that position you get an
     * exception.
     */
    while (mCursor.moveToNext()) {

        // Gets the value from the column
        newWord = mCursor.getString(index);

        // Insert code here to process the retrieved word
        ...
        // End of while loop
    }
} else {

    // Insert code here to report an error if the cursor is null or the provider threw an exception
}

Les implémentations Cursor contiennent plusieurs méthodes "get" pour récupérer différents types de données à partir de l'objet. Par exemple, l'extrait précédent utilise getString(). Ils disposent également d'une méthode getType() qui renvoie une valeur indiquant le type de données de la colonne.

Libérer les ressources des résultats de requête

Les objets Cursor doivent être fermés s'ils ne sont plus nécessaires, afin que les ressources qui leur sont associées soient libérées plus tôt. Pour ce faire, appelez close() ou utilisez une instruction try-with-resources dans le langage de programmation Java ou la fonction use() dans le langage de programmation Kotlin.

Autorisations du fournisseur de contenu

L'application d'un fournisseur peut spécifier les autorisations dont d'autres applications doivent disposer pour accéder aux données du fournisseur. Ces autorisations permettent à l'utilisateur de savoir à quelles données une application tente d'accéder. Selon les exigences du fournisseur, d'autres applications demandent les autorisations nécessaires pour accéder au fournisseur. Les utilisateurs finaux voient les autorisations demandées lorsqu'ils installent l'application.

Si l'application d'un fournisseur ne spécifie aucune autorisation, les autres applications n'ont pas accès aux données du fournisseur, sauf si le fournisseur est exporté. De plus, les composants de l'application du fournisseur disposent toujours d'un accès complet en lecture et en écriture, quelles que soient les autorisations spécifiées.

Le fournisseur de dictionnaires utilisateur a besoin de l'autorisation android.permission.READ_USER_DICTIONARY pour en extraire des données. Le fournisseur dispose d'une autorisation android.permission.WRITE_USER_DICTIONARY distincte pour insérer, mettre à jour ou supprimer des données.

Pour obtenir les autorisations nécessaires pour accéder à un fournisseur, une application les demande à l'aide d'un élément <uses-permission> dans son fichier manifeste. Lorsque le Gestionnaire de paquets Android installe l'application, l'utilisateur doit approuver toutes les autorisations demandées par l'application. Si l'utilisateur les approuve, le gestionnaire de paquets poursuit l'installation. Si l'utilisateur ne les approuve pas, le gestionnaire de paquets arrête l'installation.

L'exemple d'élément <uses-permission> suivant demande un accès en lecture au fournisseur de dictionnaire utilisateur:

<uses-permission android:name="android.permission.READ_USER_DICTIONARY">

L'impact des autorisations sur l'accès du fournisseur est expliqué plus en détail dans les conseils de sécurité.

Insérer, mettre à jour et supprimer des données

De la même manière que vous récupérez des données auprès d'un fournisseur, vous utilisez également l'interaction entre un client du fournisseur et le ContentProvider du fournisseur pour modifier les données. Vous appelez une méthode de ContentResolver avec des arguments transmis à la méthode correspondante de ContentProvider. Le fournisseur et le client du fournisseur gèrent automatiquement la sécurité et la communication inter-processus.

Insérer des données

Pour insérer des données dans un fournisseur, appelez la méthode ContentResolver.insert(). Cette méthode insère une nouvelle ligne dans le fournisseur et renvoie un URI de contenu pour cette ligne. L'extrait de code suivant montre comment insérer un nouveau mot dans le fournisseur de dictionnaire personnel:

Kotlin

// Defines a new Uri object that receives the result of the insertion
lateinit var newUri: Uri
...
// Defines an object to contain the new values to insert
val newValues = ContentValues().apply {
    /*
     * Sets the values of each column and inserts the word. The arguments to the "put"
     * method are "column name" and "value".
     */
    put(UserDictionary.Words.APP_ID, "example.user")
    put(UserDictionary.Words.LOCALE, "en_US")
    put(UserDictionary.Words.WORD, "insert")
    put(UserDictionary.Words.FREQUENCY, "100")

}

newUri = contentResolver.insert(
        UserDictionary.Words.CONTENT_URI,   // The UserDictionary content URI
        newValues                           // The values to insert
)

Java

// Defines a new Uri object that receives the result of the insertion
Uri newUri;
...
// Defines an object to contain the new values to insert
ContentValues newValues = new ContentValues();

/*
 * Sets the values of each column and inserts the word. The arguments to the "put"
 * method are "column name" and "value".
 */
newValues.put(UserDictionary.Words.APP_ID, "example.user");
newValues.put(UserDictionary.Words.LOCALE, "en_US");
newValues.put(UserDictionary.Words.WORD, "insert");
newValues.put(UserDictionary.Words.FREQUENCY, "100");

newUri = getContentResolver().insert(
    UserDictionary.Words.CONTENT_URI,   // The UserDictionary content URI
    newValues                           // The values to insert
);

Les données de la nouvelle ligne sont placées dans un seul objet ContentValues, qui ressemble à un curseur à une ligne. Les colonnes de cet objet n'ont pas besoin d'avoir le même type de données. Si vous ne souhaitez pas du tout spécifier de valeur, vous pouvez définir une colonne sur null à l'aide de ContentValues.putNull().

L'extrait précédent n'ajoute pas la colonne _ID, car celle-ci est gérée automatiquement. Le fournisseur attribue une valeur unique de _ID à chaque ligne ajoutée. Les fournisseurs utilisent généralement cette valeur comme clé primaire de la table.

L'URI de contenu renvoyé dans newUri identifie la ligne nouvellement ajoutée au format suivant:

content://user_dictionary/words/<id_value>

<id_value> correspond au contenu de _ID pour la nouvelle ligne. La plupart des fournisseurs peuvent détecter automatiquement cette forme d'URI de contenu, puis effectuer l'opération demandée sur cette ligne particulière.

Pour obtenir la valeur de _ID à partir de l'élément Uri renvoyé, appelez ContentUris.parseId().

Mettre à jour des données

Pour mettre à jour une ligne, vous utilisez un objet ContentValues avec les valeurs mises à jour, comme vous le feriez pour une insertion, et des critères de sélection, comme vous le feriez pour une requête. La méthode cliente que vous utilisez est ContentResolver.update(). Vous n'avez besoin d'ajouter des valeurs à l'objet ContentValues que pour les colonnes que vous mettez à jour. Si vous souhaitez effacer le contenu d'une colonne, définissez la valeur sur null.

L'extrait de code suivant remplace toutes les lignes dont la langue est "en" par une langue null. La valeur renvoyée correspond au nombre de lignes mises à jour.

Kotlin

// Defines an object to contain the updated values
val updateValues = ContentValues().apply {
    /*
     * Sets the updated value and updates the selected words.
     */
    putNull(UserDictionary.Words.LOCALE)
}

// Defines selection criteria for the rows you want to update
val selectionClause: String = UserDictionary.Words.LOCALE + "LIKE ?"
val selectionArgs: Array<String> = arrayOf("en_%")

// Defines a variable to contain the number of updated rows
var rowsUpdated: Int = 0
...
rowsUpdated = contentResolver.update(
        UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
        updateValues,                      // The columns to update
        selectionClause,                   // The column to select on
        selectionArgs                      // The value to compare to
)

Java

// Defines an object to contain the updated values
ContentValues updateValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String selectionClause = UserDictionary.Words.LOCALE +  " LIKE ?";
String[] selectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int rowsUpdated = 0;
...
/*
 * Sets the updated value and updates the selected words.
 */
updateValues.putNull(UserDictionary.Words.LOCALE);

rowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
    updateValues,                      // The columns to update
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

Nettoyez les entrées utilisateur lorsque vous appelez ContentResolver.update(). Pour en savoir plus, consultez la section Protéger contre les entrées malveillantes.

Supprimer les données

La suppression de lignes est semblable à la récupération des données de ligne. Vous spécifiez des critères de sélection pour les lignes que vous souhaitez supprimer, et la méthode cliente renvoie le nombre de lignes supprimées. L'extrait de code suivant supprime les lignes dont l'ID d'application correspond à "user". La méthode renvoie le nombre de lignes supprimées.

Kotlin

// Defines selection criteria for the rows you want to delete
val selectionClause = "${UserDictionary.Words.APP_ID} LIKE ?"
val selectionArgs: Array<String> = arrayOf("user")

// Defines a variable to contain the number of rows deleted
var rowsDeleted: Int = 0
...
// Deletes the words that match the selection criteria
rowsDeleted = contentResolver.delete(
        UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
        selectionClause,                   // The column to select on
        selectionArgs                      // The value to compare to
)

Java

// Defines selection criteria for the rows you want to delete
String selectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] selectionArgs = {"user"};

// Defines a variable to contain the number of rows deleted
int rowsDeleted = 0;
...
// Deletes the words that match the selection criteria
rowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,  // The UserDictionary content URI
    selectionClause,                   // The column to select on
    selectionArgs                      // The value to compare to
);

Nettoyez les entrées utilisateur lorsque vous appelez ContentResolver.delete(). Pour en savoir plus, consultez la section Protéger contre les entrées malveillantes.

Types de données du fournisseur

Les fournisseurs de contenu peuvent proposer de nombreux types de données différents. Le fournisseur de dictionnaire utilisateur ne propose que du texte, mais les fournisseurs peuvent également proposer les formats suivants:

  • Nombre entier
  • Entier long (long)
  • virgule flottante
  • virgule flottante longue (double)

Les fournisseurs utilisent souvent un autre type de données : un objet binaire volumineux (BLOB) implémenté sous la forme d'un tableau d'octets de 64 ko. Vous pouvez consulter les types de données disponibles en examinant les méthodes "get" de la classe Cursor.

Le type de données de chaque colonne d'un fournisseur est généralement indiqué dans sa documentation. Les types de données du fournisseur de dictionnaire utilisateur sont listés dans la documentation de référence de sa classe de contrat, UserDictionary.Words. Les catégories de contrats sont décrites dans la section Catégories de contrats. Vous pouvez également déterminer le type de données en appelant Cursor.getType().

Les fournisseurs gèrent également des informations sur le type de données MIME pour chaque URI de contenu qu'ils définissent. Vous pouvez utiliser les informations sur le type MIME pour savoir si votre application peut gérer les données proposées par le fournisseur ou pour choisir un type de gestion en fonction du type MIME. Vous avez généralement besoin du type MIME lorsque vous travaillez avec un fournisseur qui contient des structures de données ou des fichiers complexes.

Par exemple, la table ContactsContract.Data du fournisseur de contacts utilise des types MIME pour étiqueter le type de données de contact stockées dans chaque ligne. Pour obtenir le type MIME correspondant à un URI de contenu, appelez ContentResolver.getType().

La documentation de référence sur les types MIME décrit la syntaxe des types MIME standards et personnalisés.

Autres formes d'accès des fournisseurs

Trois autres formes d'accès au fournisseur sont importantes dans le développement d'applications:

L'accès et la modification par lot à l'aide d'intents sont décrits dans les sections suivantes.

Accès par lot

L'accès par lot à un fournisseur est utile pour insérer un grand nombre de lignes, pour insérer des lignes dans plusieurs tables dans le même appel de méthode et, en général, pour effectuer un ensemble d'opérations au-delà des limites de processus en tant que transaction, appelée opération atomique.

Pour accéder à un fournisseur en mode de traitement par lot, créez un tableau d'objets ContentProviderOperation, puis envoyez-les à un fournisseur de contenu avec ContentResolver.applyBatch(). Vous transmettez l'autorité du fournisseur de contenu à cette méthode, plutôt qu'un URI de contenu particulier.

Cela permet à chaque objet ContentProviderOperation du tableau de fonctionner sur une table différente. Un appel à ContentResolver.applyBatch() renvoie un tableau de résultats.

La description de la classe de contrat ContactsContract.RawContacts inclut un extrait de code qui illustre l'insertion par lots.

Accès aux données à l'aide d'intents

Les intents peuvent fournir un accès indirect à un fournisseur de contenu. Vous pouvez autoriser l'utilisateur à accéder aux données d'un fournisseur, même si votre application ne dispose pas d'autorisations d'accès, en récupérant un intent de résultat à partir d'une application disposant d'autorisations ou en activant une application disposant d'autorisations et en permettant à l'utilisateur de travailler dans celle-ci.

Obtenir un accès avec des autorisations temporaires

Vous pouvez accéder aux données d'un fournisseur de contenu, même si vous ne disposez pas des autorisations d'accès appropriées, en envoyant un intent à une application qui dispose des autorisations et en recevant un intent de résultat contenant des autorisations URI. Il s'agit d'autorisations pour un URI de contenu spécifique qui restent valables jusqu'à la fin de l'activité qui les reçoit. L'application disposant d'autorisations permanentes accorde des autorisations temporaires en définissant un indicateur dans l'intent de résultat:

Remarque:Ces indicateurs n'accordent pas d'accès en lecture ou en écriture général au fournisseur dont l'autorité est contenue dans l'URI de contenu. L'accès ne concerne que l'URI lui-même.

Lorsque vous envoyez des URI de contenu à une autre application, incluez au moins l'un de ces indicateurs. Les indicateurs fournissent les fonctionnalités suivantes à toute application qui reçoit un intent et qui cible Android 11 (niveau d'API 30) ou une version ultérieure:

  • Lisez ou écrivez dans les données représentées par l'URI de contenu, en fonction de l'option incluse dans l'intent.
  • Bénéficiez d'une visibilité sur le package dans l'application dont le fournisseur de contenu correspond à l'autorité d'URI. L'application qui envoie l'intent et l'application qui contient le fournisseur de contenu peuvent être deux applications différentes.

Un fournisseur définit les autorisations d'URI pour les URI de contenu dans son fichier manifeste à l'aide de l'attribut android:grantUriPermissions de l'élément <provider>, ainsi que de l'élément enfant <grant-uri-permission> de l'élément <provider>. Le mécanisme des autorisations d'URI est expliqué plus en détail dans le guide Autorisations sur Android.

Par exemple, vous pouvez récupérer les données d'un contact dans Contacts Provider, même si vous ne disposez pas de l'autorisation READ_CONTACTS. Vous pouvez le faire dans une application qui envoie des messages d'anniversaire à un contact. Au lieu de demander READ_CONTACTS, qui vous donne accès à tous les contacts et à toutes les informations de l'utilisateur, laissez l'utilisateur contrôler les contacts que votre application utilise. Pour ce faire, procédez comme suit:

  1. Dans votre application, envoyez un intent contenant l'action ACTION_PICK et le type MIME des "contacts" CONTENT_ITEM_TYPE, à l'aide de la méthode startActivityForResult().
  2. Étant donné que cet intent correspond au filtre d'intent de l'activité "sélection" de l'application People, l'activité passe au premier plan.
  3. Dans l'activité de sélection, l'utilisateur sélectionne un contact à mettre à jour. Dans ce cas, l'activité de sélection appelle setResult(resultcode, intent) pour configurer un intent à renvoyer à votre application. L'intent contient l'URI de contenu du contact sélectionné par l'utilisateur et les indicateurs "suppléments" FLAG_GRANT_READ_URI_PERMISSION. Ces indicateurs accordent à votre application une autorisation d'URI pour lire les données du contact pointé par l'URI de contenu. L'activité de sélection appelle ensuite finish() pour transférer le contrôle à votre application.
  4. Votre activité revient au premier plan, et le système appelle la méthode onActivityResult() de votre activité. Cette méthode reçoit l'intent de résultat créé par l'activité de sélection dans l'application Contacts.
  5. Avec l'URI de contenu de l'intent de résultat, vous pouvez lire les données du contact à partir du fournisseur Contacts, même si vous n'avez pas demandé au fournisseur une autorisation d'accès en lecture permanente dans votre fichier manifeste. Vous pouvez ensuite obtenir les informations d'anniversaire ou l'adresse e-mail du contact, puis envoyer la carte électronique.

Utiliser une autre application

Une autre façon de permettre à l'utilisateur de modifier des données auxquelles vous n'avez pas d'autorisation d'accès consiste à activer une application qui dispose de ces autorisations et à laisser l'utilisateur effectuer la tâche.

Par exemple, l'application Agenda accepte un intent ACTION_INSERT qui vous permet d'activer l'UI d'insertion de l'application. Vous pouvez transmettre des données "supplémentaires" dans cet intent, que l'application utilise pour préremplir l'UI. La syntaxe des événements périodiques étant complexe, la méthode privilégiée pour insérer des événements dans le fournisseur d'agenda consiste à activer l'application Agenda avec un ACTION_INSERT, puis à laisser l'utilisateur insérer l'événement à cet emplacement.

Afficher des données à l'aide d'une application d'assistance

Si votre application dispose d'autorisations d'accès, vous pouvez toujours utiliser un intent pour afficher des données dans une autre application. Par exemple, l'application Agenda accepte un intent ACTION_VIEW qui affiche une date ou un événement spécifique. Cela vous permet d'afficher des informations d'agenda sans avoir à créer votre propre interface utilisateur. Pour en savoir plus sur cette fonctionnalité, consultez la présentation du fournisseur d'agenda.

L'application à laquelle vous envoyez l'intent ne doit pas nécessairement être l'application associée au fournisseur. Par exemple, vous pouvez récupérer un contact auprès du fournisseur de contacts, puis envoyer un intent ACTION_VIEW contenant l'URI de contenu de l'image du contact à un visionneuse d'images.

Classes de contrats

Une classe de contrat définit des constantes qui aident les applications à utiliser les URI de contenu, les noms de colonnes, les actions d'intent et d'autres fonctionnalités d'un fournisseur de contenu. Les classes de contrat ne sont pas automatiquement incluses avec un fournisseur. Le développeur du fournisseur doit les définir, puis les mettre à la disposition d'autres développeurs. De nombreux fournisseurs inclus avec la plate-forme Android disposent de classes de contrat correspondantes dans le package android.provider.

Par exemple, le fournisseur de dictionnaire utilisateur possède une classe de contrat UserDictionary contenant des constantes d'URI de contenu et de nom de colonne. L'URI de contenu de la table Words est défini dans la constante UserDictionary.Words.CONTENT_URI. La classe UserDictionary.Words contient également des constantes de nom de colonne, qui sont utilisées dans les exemples d'extraits de ce guide. Par exemple, une projection de requête peut être définie comme suit:

Kotlin

val projection : Array<String> = arrayOf(
        UserDictionary.Words._ID,
        UserDictionary.Words.WORD,
        UserDictionary.Words.LOCALE
)

Java

String[] projection =
{
    UserDictionary.Words._ID,
    UserDictionary.Words.WORD,
    UserDictionary.Words.LOCALE
};

Une autre classe de contrat est ContactsContract pour le fournisseur de contacts. La documentation de référence de cette classe inclut des exemples de code. L'une de ses sous-classes, ContactsContract.Intents.Insert, est une classe de contrat qui contient des constantes pour les intents et les données d'intent.

Référence du type MIME

Les fournisseurs de contenu peuvent renvoyer des types multimédias MIME standards, des chaînes de type MIME personnalisées, ou les deux.

Les types MIME sont au format suivant:

type/subtype

Par exemple, le type MIME text/html bien connu a le type text et le sous-type html. Si le fournisseur renvoie ce type pour un URI, cela signifie qu'une requête utilisant cet URI renvoie du texte contenant des balises HTML.

Les chaînes de type MIME personnalisées, également appelées types MIME spécifiques au fournisseur, ont des valeurs type et subtype plus complexes. Dans le cas de plusieurs lignes, la valeur du type est toujours la suivante:

vnd.android.cursor.dir

Pour une seule ligne, la valeur de type est toujours la suivante:

vnd.android.cursor.item

subtype est propre au fournisseur. Les fournisseurs intégrés Android ont généralement un sous-type simple. Par exemple, lorsque l'application Contacts crée une ligne pour un numéro de téléphone, elle définit le type MIME suivant dans la ligne:

vnd.android.cursor.item/phone_v2

La valeur du sous-type est phone_v2.

Les autres développeurs de fournisseurs peuvent créer leur propre modèle de sous-types en fonction de l'autorité et des noms de table du fournisseur. Prenons l'exemple d'un fournisseur qui contient des horaires de train. L'autorité du fournisseur est com.example.trains, et elle contient les tables Line1, Line2 et Line3. En réponse à l'URI de contenu suivant pour la table Line1:

content://com.example.trains/Line1

le fournisseur renvoie le type MIME suivant:

vnd.android.cursor.dir/vnd.example.line1

En réponse à l'URI de contenu suivant pour la ligne 5 du tableau Line2:

content://com.example.trains/Line2/5

Le fournisseur renvoie le type MIME suivant:

vnd.android.cursor.item/vnd.example.line2

La plupart des fournisseurs de contenu définissent des constantes de classe de contrat pour les types MIME qu'ils utilisent. Par exemple, la classe de contrat ContactsContract.RawContacts du fournisseur de contacts définit la constante CONTENT_ITEM_TYPE pour le type MIME d'une seule ligne de contact brute.

Les URI de contenu pour des lignes uniques sont décrits dans la section URI de contenu.