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.](https://developer.android.com/static/guide/topics/providers/images/content-provider-tech-stack.png?authuser=6&hl=fr)
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.](https://developer.android.com/static/guide/topics/providers/images/content-provider-interaction.png?authuser=6&hl=fr)
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:
- Demandez l'autorisation d'accès en lecture pour le fournisseur.
- 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:
-
Accès par lot: vous pouvez créer un lot d'appels d'accès avec des méthodes de la classe
ContentProviderOperation
, puis les appliquer avecContentResolver.applyBatch()
. -
Requêtes asynchrones: exécutez vos requêtes dans un thread distinct. Vous pouvez utiliser un objet
CursorLoader
. Les exemples du guide sur les chargeurs expliquent comment procéder. - Accès aux données à l'aide d'intents: bien que vous ne puissiez pas envoyer un intent directement à un fournisseur, vous pouvez envoyer un intent à l'application du fournisseur, qui est généralement la mieux adaptée pour modifier les données du fournisseur.
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:
-
Autorisation de lecture:
FLAG_GRANT_READ_URI_PERMISSION
-
Autorisation d'écriture:
FLAG_GRANT_WRITE_URI_PERMISSION
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:
-
Dans votre application, envoyez un intent contenant l'action
ACTION_PICK
et le type MIME "contacts"CONTENT_ITEM_TYPE
à l'aide de la méthodestartActivityForResult()
. - É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.
-
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 ensuitefinish()
pour rendre le contrôle à votre application. -
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. - 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.