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 interface utilisateur 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 leurs clients offrent une interface cohérente et standard pour les données, qui gère également la communication inter-processus 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 pour collaborer avec des fournisseurs de contenu existants. Pour découvrir comment mettre en œuvre des fournisseurs de contenu dans vos propres applications, consultez la section Créer un fournisseur de contenu.

Cette rubrique 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 d'API facilitant la collaboration avec les fournisseurs

Présentation

Un fournisseur de contenu présente des données aux applications externes sous la forme d'une ou plusieurs tables semblables à celles d'une base de données relationnelle. Une ligne représente une instance d'un certain type de données collectées par le fournisseur, et chaque colonne de la ligne représente une donnée individuelle collectée 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 l'illustre la figure 1, ces options sont les suivantes:

  • Partager l'accès à vos données d'application avec d'autres applications
  • Envoyer des données vers un widget
  • Renvoi de suggestions de recherche personnalisées pour votre application via le framework de recherche à l'aide de SearchRecentSuggestionsProvider
  • Synchroniser les données d'application avec votre serveur à l'aide de l'implémentation de AbstractThreadedSyncAdapter
  • Charger des données dans votre UI à 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 Context de votre application pour communiquer avec le fournisseur en tant que client. L'objet ContentResolver communique avec l'objet fournisseur, une instance d'une classe qui implémente ContentProvider.

L'objet 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 le même nom dans l'objet fournisseur, une instance de l'une des sous-classes concrètes de ContentProvider. Les méthodes ContentResolver fournissent les fonctions "CRUD" de base (création, récupération, mise à jour et suppression) du stockage persistant.

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

Ainsi, l'interface utilisateur reste 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 du mécanisme de stockage sous-jacent, comme illustré dans la figure 2.

Interaction entre ContentProvider, d'autres classes et le 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 des fournisseurs de contenu.

L'un des fournisseurs intégrés à la plate-forme Android est le fournisseur de dictionnaire d'utilisateurs, qui stocke les mots non standards que l'utilisateur souhaite conserver. Le tableau 1 illustre les données pouvant être présentées dans la table de ce fournisseur:

Tableau 1:exemple de table de dictionnaire utilisateur.

de lettres identifiant d'application de publication paramètres régionaux ID
mapreduce utilisateur1 100 fr_FR 1
precompiler utilisateur14 200 fr_FR 2
applet utilisateur2 225 fr_CA 3
const utilisateur1 255 pt_BR 4
int utilisateur5 100 fr_FR 5

Dans le tableau 1, chaque ligne représente une instance d'un mot introuvable dans un dictionnaire standard. Chaque colonne représente une donnée pour ce mot, par exemple la langue dans laquelle il a été rencontré pour la première fois. Les en-têtes de colonne sont des noms de colonnes stockés dans le fournisseur. Ainsi, pour faire référence aux paramètres régionaux d'une ligne, par exemple, vous devez vous référer à sa colonne locale. Pour ce fournisseur, la colonne _ID sert de colonne de clé primaire qui est automatiquement gérée par le fournisseur.

Pour obtenir la liste des mots et de leurs paramètres régionaux à partir du fournisseur de dictionnaires d'utilisateurs, appelez ContentResolver.query(). La méthode query() appelle la méthode ContentProvider.query() définie par le fournisseur de dictionnaires 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 SQL SELECT:

Tableau 2:query() par rapport à la requête SQL.

Argument query() SÉLECTIONNER un mot clé/paramètre Notes
Uri FROM table_name Uri correspond à 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). Lorsque vous appelez une méthode client pour accéder à une table d'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 dictionnaire utilisateur. 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:// est le schéma, qui est toujours présent et l'identifie en tant qu'URI de contenu.
  • La chaîne user_dictionary constitue l'autorité du fournisseur.
  • La chaîne words est le chemin d'accès de 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 champ _ID est 4 à partir du fournisseur de dictionnaires d'utilisateurs, vous pouvez utiliser l'URI de contenu suivant:

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, puis que vous souhaitez mettre à jour ou supprimer l'une d'elles.

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

Récupérer les données auprès du fournisseur

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

Par souci de clarté, les extraits de code de cette section appellent ContentResolver.query() sur le thread UI. Toutefois, en code réel, effectuez les 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 ne présentent 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 d'une autorisation d'accès en lecture pour celui-ci. 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 votre 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 le fichier manifeste, vous demandez cette autorisation pour votre application. Lorsque les utilisateurs installent votre application, ils accordent implicitement cette requête.

Pour connaître 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 qu'il utilise, consultez sa documentation.

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

Le fournisseur de dictionnaires d'utilisateurs définit l'autorisation android.permission.READ_USER_DICTIONARY dans son fichier manifeste. Une application qui souhaite lire à partir du fournisseur doit donc demander cette autorisation.

Construire la requête

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

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 l'exemple du fournisseur de dictionnaires d'utilisateurs. 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é 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 colonne 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 du tableau d'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 d'arguments de sélection est défini sur le mot saisi.

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 analogue à 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 à la place des constantes de classe de contrat.

Se 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 fiables dans des instructions SQL brutes peut entraîner une injection SQL.

Considérez 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;

En procédant ainsi, vous laissez l'utilisateur concaténer potentiellement des requêtes SQL malveillantes dans 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 *;.

Étant donné que la clause de sélection est 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 façon, l'entrée utilisateur est directement liée à la requête au lieu d'être interprétée dans le cadre d'une instruction SQL. Étant donné qu'elle n'est pas traitée comme SQL, l'entrée utilisateur ne peut pas injecter du code SQL malveillant. Au lieu d'utiliser la concaténation pour inclure l'entrée 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 = {""};

Placez une valeur dans le tableau d'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 de tableaux d'arguments de sélection est le moyen privilégié pour 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 client 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 dans les résultats, déterminer le type de données de chaque colonne, extraire les données d'une colonne et examiner d'autres propriétés des résultats.

Certaines implémentations de Cursor mettent automatiquement à jour 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 à certaines colonnes aux adaptateurs de synchronisation afin de ne pas les renvoyer à 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 0, c'est-à-dire un curseur vide.

En cas d'erreur interne, les résultats de la requête dépendent du fournisseur concerné. Elle peut renvoyer null ou générer une erreur 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 suivant poursuit le code de l'extrait précédent. Elle crée un objet SimpleCursorAdapter contenant la Cursor récupérée par la requête et définit cet objet comme adaptateur pour un 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 sauvegarder une ListView avec un Cursor, le curseur doit contenir une colonne nommée _ID. De ce fait, 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 de résultats de requête

En plus d'afficher les résultats de la requête, vous pouvez les utiliser pour d'autres tâches. Par exemple, vous pouvez récupérer les orthographes du fournisseur de dictionnaires d'utilisateurs, puis les rechercher auprès d'autres fournisseurs. Pour ce faire, itérez 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.

Autorisations des fournisseurs de contenu

L'application d'un fournisseur peut spécifier les autorisations dont d'autres applications doivent disposer pour accéder à ses données. Ces autorisations permettent à l'utilisateur de savoir à quelles données une application tente d'accéder. En fonction des exigences du fournisseur, d'autres applications demandent les autorisations dont elles ont besoin 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 à ses données, sauf si celui-ci 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 récupérer ses 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 avec un élément <uses-permission> dans son fichier manifeste. Lorsque le gestionnaire de packages Android installe l'application, l'utilisateur doit approuver toutes les autorisations demandées par l'application. Si l'utilisateur les approuve, le gestionnaire de packages poursuit l'installation. Si l'utilisateur ne les approuve pas, le gestionnaire de packages interrompt l'installation.

L'exemple d'élément <uses-permission> suivant demande un accès en lecture au fournisseur de dictionnaires 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 la section 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 le client d'un fournisseur et son ContentProvider pour modifier des 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 incluses dans un seul objet ContentValues, dont la forme est semblable à celle d'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 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 cette colonne 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 du tableau.

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

content://user_dictionary/words/<id_value>

<id_value> correspond au contenu du fichier _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 du Uri renvoyé, appelez ContentUris.parseId().

Mettre à jour des données

Pour mettre à jour une ligne, utilisez un objet ContentValues avec les valeurs mises à jour, tout comme vous le feriez avec une insertion, et des critères de sélection, comme vous le feriez avec une requête. La méthode client que vous utilisez est ContentResolver.update(). Il vous suffit d'ajouter des valeurs à l'objet ContentValues 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 les paramètres régionaux sont "en" par null. La valeur renvoyée correspond au nombre de lignes qui ont été 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
);

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

Supprimer vos 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
);

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

Types de données des fournisseurs

Les fournisseurs de contenu peuvent proposer de nombreux types de données différents. Le fournisseur de dictionnaires personnels 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)

Un autre type de données fréquemment utilisé par les fournisseurs est le BLOB (Binary Large Object) implémenté sous la forme d'un tableau d'octets de 64 Ko. Vous pouvez afficher les types de données disponibles en consultant 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 dictionnaires utilisateur sont listés dans la documentation de référence de sa classe de contrat, UserDictionary.Words. Les classes 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 les informations de type de données MIME pour chaque URI de contenu qu'ils définissent. Vous pouvez utiliser les informations du type MIME pour savoir si votre application peut gérer les données proposées par le fournisseur ou pour choisir un type de traitement basé sur le type MIME. Vous avez généralement besoin du type MIME lorsque vous travaillez avec un fournisseur contenant des structures de données ou des fichiers complexes.

Par exemple, la table ContactsContract.Data du fournisseur de contacts utilise des types MIME pour appliquer un libellé au 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 le type MIME décrit la syntaxe des types MIME standards et personnalisés.

Autres formes d'accès au fournisseur

Trois autres formes d'accès de fournisseur sont importantes pour 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 lors du même appel de méthode et, en général, pour effectuer un ensemble d'opérations au-delà des limites du 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 distribuez-les à un fournisseur de contenu avec ContentResolver.applyBatch(). Vous transmettez à cette méthode l'autorité du fournisseur de contenu, plutôt qu'un URI de contenu particulier.

Chaque objet ContentProviderOperation du tableau peut ainsi fonctionner avec 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 lot.

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, soit en obtenant un intent de résultat d'une application disposant d'autorisations, soit en activant une application disposant d'autorisations et en laissant l'utilisateur travailler dessus.

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 en dispose et en recevant en retour un intent de résultat contenant des autorisations d'URI. Il s'agit d'autorisations pour un URI de contenu spécifique qui sont valables jusqu'à la fin de l'activité qui les reçoit. L'application disposant d'autorisations permanentes accorde des autorisations temporaires en définissant une option dans l'intent de résultat:

Remarque:Ces indicateurs n'accordent pas d'accès général en lecture ou en écriture 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 offrent les fonctionnalités suivantes à toute application qui reçoit un intent et cible Android 11 (niveau d'API 30) ou version ultérieure:

  • Lire ou écrire dans les données représentées par l'URI de contenu, en fonction de l'indicateur inclus dans l'intent.
  • Obtenez la visibilité du package dans l'application contenant le fournisseur de contenu qui correspond à l'autorité d'URI. L'application qui envoie l'intent et celle qui contient le fournisseur de contenu peuvent être deux applications différentes.

Un fournisseur définit des 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 d'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. Pour ce faire, vous pouvez utiliser une application qui envoie des messages d'accueil à un contact le jour de son anniversaire. Au lieu de demander l'autorisation READ_CONTACTS, qui vous donne accès à tous les contacts de l'utilisateur et à toutes leurs informations, laissez-le contrôler les contacts utilisés par votre application. Pour ce faire, procédez comme suit:

  1. Dans votre application, envoyez un intent contenant l'action ACTION_PICK et le type MIME "contacts" CONTENT_ITEM_TYPE à l'aide de la méthode startActivityForResult().
  2. Étant donné que cet intent correspond au filtre d'intent pour l'activité de "sélection" de l'application People, l'activité s'affiche au premier plan.
  3. Dans l'activité de sélection, l'utilisateur sélectionne un contact à mettre à jour. Lorsque cela se produit, l'activité de sélection appelle setResult(resultcode, intent) pour configurer un intent à restituer à votre application. L'intent contient l'URI de contenu du contact sélectionné par l'utilisateur et les indicateurs "extras" FLAG_GRANT_READ_URI_PERMISSION. Ces indicateurs autorisent l'URI à votre application à lire les données du contact vers lequel pointe l'URI de contenu. L'activité de sélection appelle ensuite finish() pour rendre 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 de contacts, même si vous n'avez pas demandé au fournisseur d'accès en lecture permanent dans votre fichier manifeste. Vous pouvez ensuite obtenir la date de naissance ou l'adresse e-mail du contact, puis lui envoyer le message d'accueil.

Utiliser une autre application

Une autre façon de permettre à l'utilisateur de modifier des données pour lesquelles vous ne disposez pas des autorisations d'accès consiste à activer une application disposant d'autorisations et à laisser l'utilisateur s'en charger.

Par exemple, l'application Agenda accepte un intent ACTION_INSERT qui vous permet d'activer l'interface utilisateur d'insertion de l'application. Vous pouvez transmettre des données "extras" à cet intent, que l'application utilise pour préremplir l'interface utilisateur. Étant donné que les événements récurrents ont une syntaxe complexe, la méthode privilégiée pour insérer des événements dans le fournisseur d'agendas consiste à activer l'application Agenda avec un ACTION_INSERT, puis à laisser l'utilisateur insérer l'événement à cet endroit.

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

Si votre application possède des 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 particulier. Vous pouvez ainsi afficher les informations de votre 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 à partir du fournisseur de contacts, puis envoyer un intent ACTION_VIEW contenant l'URI de contenu de l'image du contact à un lecteur d'images.

Classes de contrats

Une classe de contrat définit des constantes qui aident les applications à fonctionner avec les URI de contenu, les noms de colonne, les actions d'intent et d'autres fonctionnalités d'un fournisseur de contenu. Les classes de contrat ne sont pas incluses automatiquement 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 dans la plate-forme Android ont des classes de contrat correspondantes dans le package android.provider.

Par exemple, le fournisseur de dictionnaires d'utilisateurs possède une classe de contrat UserDictionary contenant l'URI de contenu et les constantes des noms 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 comprend des exemples d'extraits 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.

Documentation de référence sur le type MIME

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

Les types MIME ont le format suivant:

type/subtype

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

Les chaînes de types MIME personnalisées, également appelées types MIME propres au fournisseur, ont des valeurs type et subtype plus complexes. Pour plusieurs lignes, la valeur de 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 spécifique au fournisseur. Les fournisseurs Android intégrés 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 y définit le type MIME suivant:

vnd.android.cursor.item/phone_v2

La valeur du sous-type est phone_v2.

Les développeurs d'autres fournisseurs peuvent créer leur propre modèle de sous-types en fonction de l'autorité du fournisseur et des noms de table. Prenons l'exemple d'un fournisseur qui contient les 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 ligne 1 de la table:

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 de la ligne 2 du tableau:

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 du fournisseur de contacts ContactsContract.RawContacts définit la constante CONTENT_ITEM_TYPE pour le type MIME d'une seule ligne de contact brute.

Les URI de contenu de lignes individuelles sont décrits dans la section URI de contenu.