Un fornitore di contenuti gestisce l'accesso a un repository centrale di dati. Un provider fa parte di un'applicazione Android, che spesso fornisce la propria UI per l'elaborazione dei dati. Tuttavia, i fornitori di contenuti sono utilizzati principalmente da altre applicazioni, che accedono al provider utilizzando un oggetto client provider. Insieme, provider e client provider offrono un'interfaccia coerente e standard per i dati che gestisce anche la comunicazione tra processi e l'accesso sicuro ai dati.
In genere si collabora con i fornitori di contenuti in uno di questi due scenari: implementare il codice per accedere a un fornitore di contenuti esistente in un'altra applicazione o creare un nuovo fornitore di contenuti nella propria applicazione per condividere dati con altre applicazioni.
In questa pagina vengono spiegate le nozioni di base relative alla collaborazione con i fornitori di contenuti esistenti. Per scoprire come implementare i fornitori di contenuti nelle tue applicazioni, consulta Creare un fornitore di contenuti.
Questo argomento descrive quanto segue:
- Come funzionano i fornitori di contenuti.
- L'API che utilizzi per recuperare i dati da un fornitore di contenuti.
- L'API che utilizzi per inserire, aggiornare o eliminare i dati in un fornitore di contenuti.
- Altre funzionalità dell'API che facilitano la collaborazione con i fornitori.
Panoramica
Un fornitore di contenuti presenta i dati alle applicazioni esterne sotto forma di una o più tabelle simili alle tabelle presenti in un database relazionale. Una riga rappresenta un'istanza di un tipo di dati raccolti dal provider e ogni colonna nella riga rappresenta un singolo dato raccolto per un'istanza.
Un fornitore di contenuti coordina l'accesso al livello di archiviazione dei dati nell'applicazione per una serie di API e componenti diversi. Come illustrato nella Figura 1, questi includono quanto segue:
- Condivisione dell'accesso ai dati delle tue applicazioni con altre applicazioni
- Invio di dati a un widget
- Restituzione di suggerimenti di ricerca personalizzati per la tua applicazione tramite il framework di ricerca utilizzando
SearchRecentSuggestionsProvider
- Sincronizzazione dei dati dell'applicazione con il tuo server utilizzando un'implementazione di
AbstractThreadedSyncAdapter
- Caricamento dei dati nell'interfaccia utente tramite
CursorLoader
Accedi a un fornitore
Quando vuoi accedere ai dati di un fornitore di contenuti, devi utilizzare l'oggetto ContentResolver
nell'elemento Context
dell'applicazione per comunicare con il provider come client. L'oggetto ContentResolver
comunica con l'oggetto provider, un'istanza di una classe che implementa ContentProvider
.
L'oggetto provider riceve richieste di dati dai client, esegue l'azione richiesta e restituisce i risultati. Questo oggetto include metodi che chiamano metodi con nomi identici nell'oggetto provider,
un'istanza di una delle sottoclassi concrete di ContentProvider
. I metodi ContentResolver
forniscono le funzioni "CRUD" di base (creazione, recupero, aggiornamento ed eliminazione) dell'archiviazione permanente.
Un pattern comune per accedere a un ContentProvider
dalla UI utilizza un CursorLoader
per eseguire una query asincrona in background. Activity
o Fragment
nella tua UI chiama un CursorLoader
alla query, che a sua volta riceve ContentProvider
utilizzando ContentResolver
.
In questo modo l'interfaccia utente continuerà a essere disponibile per l'utente mentre la query è in esecuzione. Questo pattern comporta l'interazione di un certo numero di oggetti diversi, nonché il meccanismo di archiviazione sottostante, come illustrato nella Figura 2.
Nota: per accedere a un provider, l'applicazione di solito deve richiedere autorizzazioni specifiche nel file manifest. Questo pattern di sviluppo è descritto più dettagliatamente nella sezione Autorizzazioni del fornitore di contenuti.
Uno dei provider integrati nella piattaforma Android è User Dictionary Provider, che archivia le parole non standard che l'utente vuole conservare. La tabella 1 illustra l'aspetto dei dati nella tabella di questo provider:
parola | id app | di pubblicazione | impostazioni internazionali | ID |
---|---|---|---|---|
mapreduce |
utente1 | 100 | it_IT | 1 |
precompiler |
utente14 | 200 | fr_FR | 2 |
applet |
utente2 | 225 | fr_CA | 3 |
const |
utente1 | 255 | pt_BR | 4 |
int |
utente5 | 100 | it_IT | 5 |
Nella tabella 1, ogni riga rappresenta un'istanza di una parola non trovata in un dizionario standard. Ogni colonna rappresenta un dato per quella parola, ad esempio l'impostazione internazionale in cui è stata rilevata per la prima volta. Le intestazioni di colonna sono nomi di colonna archiviati nel provider. Per fare riferimento alle impostazioni internazionali di una riga, ad esempio, fai riferimento alla relativa colonna locale
. Per questo provider, la colonna _ID
funge da colonna chiave primaria gestita automaticamente dal provider.
Per ottenere un elenco delle parole e delle relative impostazioni internazionali dal fornitore del dizionario utente, chiama ContentResolver.query()
.
Il metodo query()
chiama il
metodo ContentProvider.query()
definito dal
provider del dizionario utente. Le seguenti righe di codice mostrano una
chiamata a 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
La tabella 2 mostra come gli argomenti per
query(Uri,projection,selection,selectionArgs,sortOrder)
corrispondono a un'istruzione SQL SELECT:
query() argomento |
SELECT parola chiave/parametro | Notes |
---|---|---|
Uri |
FROM table_name |
Uri viene mappato alla tabella nel provider denominato table_name. |
projection |
col,col,col,... |
projection è un array di colonne incluso per ogni riga recuperata.
|
selection |
WHERE col = value |
selection specifica i criteri per la selezione delle righe. |
selectionArgs |
Nessun equivalente esatto. Gli argomenti di selezione sostituiscono i segnaposto ? nella
clausola di selezione.
|
|
sortOrder |
ORDER BY col,col,... |
sortOrder specifica l'ordine in cui vengono visualizzate le righe nell'elemento
Cursor restituito.
|
URI contenuti
Un URI di contenuto è un URI che identifica i dati in un provider. Gli URI dei contenuti includono il nome simbolico dell'intero provider (la sua autorità) e un nome che rimanda a una tabella: un path. Quando chiami un metodo client per accedere a una tabella in un provider, l'URI contenuto della tabella è uno degli argomenti.
Nelle righe di codice precedenti, la costante CONTENT_URI
contiene l'URI del contenuto della tabella Words
del fornitore del dizionario utente. L'oggetto ContentResolver
analizza l'autorità dell'URI e la utilizza per risolvere il provider confrontando l'autorità con una tabella di sistema di provider noti. ContentResolver
può quindi inviare gli argomenti della query al provider corretto.
ContentProvider
utilizza la parte del percorso dell'URI dei contenuti per scegliere la tabella a cui accedere. Un provider di solito ha un percorso per ogni tabella che espone.
Nelle righe di codice precedenti, l'URI completo della tabella Words
è:
content://user_dictionary/words
- La stringa
content://
è lo schema, che è sempre presente e lo identifica come URI di contenuto. - La stringa
user_dictionary
è l'autorità del provider. - La stringa
words
è il percorso della tabella.
Molti provider consentono di accedere a una singola riga in una tabella aggiungendo un valore ID alla fine dell'URI. Ad esempio, per recuperare una riga il cui _ID
è
4
dal provider del dizionario utente, puoi utilizzare questo URI di contenuti:
Kotlin
val singleUri: Uri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4)
Java
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
Spesso utilizzi i valori ID quando recuperi un insieme di righe e poi vuoi aggiornarne o eliminarne una.
Nota: le classi Uri
e Uri.Builder
contengono metodi di convenienza per creare oggetti URI formati a partire dalle stringhe. La
classe ContentUris
contiene metodi pratici per aggiungere valori ID a
un URI. Lo snippet precedente utilizza withAppendedId()
per aggiungere un ID all'URI dei contenuti del provider del dizionario utente.
Recupera i dati dal provider
Questa sezione descrive come recuperare i dati da un provider, utilizzando il Provider di dizionario utente come esempio.
Per maggiore chiarezza, gli snippet di codice in questa sezione chiamano
ContentResolver.query()
nel thread dell'interfaccia utente. Nel
codice effettivo, tuttavia, esegui query in modo asincrono su un thread separato. Puoi utilizzare la classe CursorLoader
, descritta più dettagliatamente nella guida
Caricatori. Inoltre, le righe di codice sono solo snippet. Non mostrano un'applicazione completa.
Per recuperare i dati da un fornitore, segui questi passaggi di base:
- Richiedi l'autorizzazione di accesso in lettura per il provider.
- Definisci il codice che invia una query al provider.
Richiedi autorizzazione di accesso in lettura
Per recuperare i dati da un provider, la tua applicazione deve disporre dell'autorizzazione di accesso in lettura
al provider. Non puoi richiedere questa autorizzazione in fase di runtime. Devi invece specificare questa autorizzazione nel file manifest, utilizzando l'elemento <uses-permission>
e il nome esatto dell'autorizzazione definito dal provider.
Quando specifichi questo elemento nel file manifest, richiedi questa autorizzazione per la tua applicazione. Quando gli utenti installano la tua applicazione, accettano implicitamente questa richiesta.
Per trovare il nome esatto dell'autorizzazione di accesso in lettura per il provider che stai utilizzando, nonché i nomi di altre autorizzazioni di accesso utilizzate dal provider, consulta la documentazione del provider.
Il ruolo delle autorizzazioni nell'accesso ai provider è descritto più dettagliatamente nella sezione Autorizzazioni dei fornitori di contenuti.
Il provider del dizionario utente definisce l'autorizzazione android.permission.READ_USER_DICTIONARY
nel file manifest, perciò un'applicazione che vuole leggere dal provider deve richiedere questa autorizzazione.
Costruire la query
Il passaggio successivo per il recupero dei dati da un provider è creare una query. Lo snippet seguente definisce alcune variabili per l'accesso al provider del dizionario utente:
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 = {""};
Lo snippet successivo mostra come utilizzare ContentResolver.query()
, utilizzando il fornitore del dizionario utente come esempio. Una query del client provider è simile a una query SQL e contiene un insieme di colonne da restituire, un insieme di criteri di selezione e un ordinamento.
L'insieme di colonne restituito dalla query è chiamato proiezione e la variabile è mProjection
.
L'espressione che specifica le righe da recuperare è suddivisa in una clausola di selezione e argomenti di selezione. La clausola di selezione è una combinazione di espressioni logiche e booleane, nomi di colonna e valori. La variabile è mSelectionClause
. Se specifichi il
parametro sostituibile ?
anziché un valore, il metodo di query recupera il valore
dall'array degli argomenti di selezione, ovvero la variabile mSelectionArgs
.
Nello snippet successivo, se l'utente non inserisce una parola, la clausola di selezione viene impostata su null
e la query restituisce tutte le parole del fornitore. Se l'utente inserisce una parola, la clausola di selezione viene impostata su UserDictionary.Words.WORD + " = ?"
e il primo elemento dell'array di argomenti di selezione viene impostato sulla parola inserita dall'utente.
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 }
Questa query è analoga alla seguente istruzione SQL:
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;
In questa istruzione SQL, vengono utilizzati i nomi effettivi delle colonne al posto delle costanti delle classi di contratto.
Proteggiti da input dannosi
Se i dati gestiti dal fornitore di contenuti si trovano in un database SQL, l'inclusione di dati esterni non attendibili in istruzioni SQL non elaborati può portare a SQL injection.
Considera la seguente clausola di selezione:
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;
Se lo fai, consenti all'utente di concatenare potenzialmente le query SQL dannose nella tua istruzione SQL.
Ad esempio, l'utente può inserire "nothing; DROP TABLE *;" per mUserInput
, che genera la clausola di selezione var = nothing; DROP TABLE *;
.
Poiché la clausola di selezione viene trattata come un'istruzione SQL, il provider potrebbe cancellare tutte le tabelle nel database SQLite sottostante, a meno che il provider non sia configurato per rilevare i tentativi di iniezione SQL.
Per evitare questo problema, utilizza una clausola di selezione che utilizzi ?
come parametro sostituibile e un array separato di argomenti di selezione. In questo modo, l'input utente viene associato direttamente alla query anziché interpretato come parte di un'istruzione SQL.
Poiché non viene considerato SQL, l'input utente non può inserire codice SQL dannoso. Anziché utilizzare la concatenazione per includere l'input utente, utilizza questa clausola di selezione:
Kotlin
// Constructs a selection clause with a replaceable parameter var selectionClause = "var = ?"
Java
// Constructs a selection clause with a replaceable parameter String selectionClause = "var = ?";
Imposta l'array di argomenti di selezione in questo modo:
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 = {""};
Inserisci un valore nell'array degli argomenti di selezione in questo modo:
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;
Una clausola di selezione che utilizza ?
come parametro sostituibile e un array di array di argomenti di selezione è il modo migliore per specificare una selezione, anche se il provider non si basa su un database SQL.
Visualizza risultati della query
Il metodo client ContentResolver.query()
restituisce sempre un elemento Cursor
contenente le colonne specificate dalla proiezione della query per le righe che corrispondono ai criteri di selezione della query. Un
oggetto Cursor
fornisce accesso in lettura casuale alle righe e alle colonne che
contiene.
Con i metodi Cursor
, puoi eseguire l'iterazione delle righe nei risultati, determinare il tipo di dati di ogni colonna, estrarre i dati da una colonna ed esaminare altre proprietà dei risultati.
Alcune implementazioni Cursor
aggiornano automaticamente l'oggetto quando i dati del provider cambiano, attivano i metodi in un oggetto osservatore quando cambia Cursor
oppure entrambi i casi.
Nota: un provider può limitare l'accesso alle colonne in base alla natura dell'oggetto che effettua la query. Ad esempio, il provider di contatti limita l'accesso ad alcune colonne per sincronizzare gli adattatori, in modo da non restituirli a un'attività o a un servizio.
Se nessuna riga corrisponde ai criteri di selezione, il provider restituisce un oggetto Cursor
per cui Cursor.getCount()
è pari a 0, ovvero un cursore vuoto.
Se si verifica un errore interno, i risultati della query dipendono dal provider specifico. Potrebbe
restituire null
o generare un Exception
.
Poiché Cursor
è un elenco di righe, un buon modo per visualizzare i contenuti di un elemento Cursor
è collegarlo a un ListView
utilizzando un SimpleCursorAdapter
.
Lo snippet seguente continua il codice dello snippet precedente. Crea un oggetto
SimpleCursorAdapter
contenente l'elemento Cursor
recuperato dalla query e imposta questo oggetto come adattatore per
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);
Nota: per eseguire il backup di ListView
con Cursor
, il cursore deve contenere una colonna denominata _ID
.
Per questo motivo, in precedenza la query mostrata recupera la colonna _ID
per la tabella Words
, anche se ListView
non la visualizza.
Questa limitazione spiega anche perché la maggior parte dei provider ha una colonna _ID
per ciascuna
tabella.
Recuperare i dati dai risultati della query
Oltre a visualizzare i risultati della query, puoi utilizzarli per altre attività. Ad esempio, puoi recuperare l'ortografia dal provider di dizionario utente e quindi cercarla in altri provider. A questo scopo, esegui l'iterazione sulle righe in Cursor
, come mostrato nell'esempio seguente:
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 }
Le implementazioni Cursor
contengono diversi metodi "get" per recuperare diversi tipi di dati dall'oggetto. Ad esempio, lo snippet precedente
utilizza getString()
. Hanno anche un
metodo getType()
che restituisce un valore che indica
il tipo di dati della colonna.
Autorizzazioni del fornitore di contenuti
L'applicazione di un provider può specificare le autorizzazioni che altre applicazioni devono disporre per accedere ai dati del provider. Queste autorizzazioni consentono all'utente di sapere a quali dati tenta di accedere un'applicazione. In base ai requisiti del provider, altre applicazioni richiedono le autorizzazioni necessarie per accedere al provider. Gli utenti finali vedono le autorizzazioni richieste quando installano l'applicazione.
Se l'applicazione di un provider non specifica alcuna autorizzazione, le altre applicazioni non avranno accesso ai dati del provider, a meno che il provider non venga esportato. Inoltre, i componenti dell'applicazione del provider hanno sempre accesso completo in lettura e scrittura, indipendentemente dalle autorizzazioni specificate.
Il provider di dizionario utente richiede l'autorizzazione android.permission.READ_USER_DICTIONARY
per recuperarne i dati.
Il provider ha un'autorizzazione android.permission.WRITE_USER_DICTIONARY
separata per l'inserimento, l'aggiornamento o l'eliminazione dei dati.
Per ottenere le autorizzazioni necessarie per accedere a un provider, un'applicazione richiede loro un elemento <uses-permission>
nel proprio file manifest. Quando il gestore di pacchetti Android installa l'applicazione, l'utente deve approvare tutte le autorizzazioni richieste dall'applicazione. Se l'utente le approva,
Gestione pacchetti continua l'installazione. Se l'utente non li approva, Gestione pacchetti
interrompe l'installazione.
Il seguente elemento
<uses-permission>
di esempio richiede l'accesso in lettura al provider del dizionario utente:
<uses-permission android:name="android.permission.READ_USER_DICTIONARY">
L'impatto delle autorizzazioni sull'accesso del provider è spiegato più dettagliatamente nei suggerimenti per la sicurezza.
Inserisci, aggiorna ed elimina dati
Allo stesso modo in cui recuperi i dati da un provider, utilizzi anche l'interazione tra
un client del provider e ContentProvider
del provider per modificare i dati.
Chiama un metodo di ContentResolver
con argomenti che vengono passati al metodo corrispondente di ContentProvider
. Il provider e il client del provider gestiscono automaticamente la sicurezza e le comunicazioni tra processi.
Inserisci dati
Per inserire dati in un provider, chiami il metodo
ContentResolver.insert()
. Questo metodo inserisce una nuova riga nel provider e restituisce un URI di contenuto per tale riga.
Lo snippet seguente mostra come inserire una nuova parola nel provider dizionario utente:
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 );
I dati della nuova riga vengono inseriti in un singolo oggetto ContentValues
, che ha una forma simile a un cursore a una riga. Le colonne in questo oggetto non devono necessariamente avere lo stesso tipo di dati e, se non vuoi specificare un valore, puoi impostare una colonna su null
utilizzando ContentValues.putNull()
.
Lo snippet precedente non aggiunge la colonna _ID
, perché questa viene gestita
automaticamente. Il provider assegna un valore univoco _ID
a ogni riga aggiunta. I provider in genere utilizzano questo valore come chiave primaria della tabella.
L'URI dei contenuti restituito in newUri
identifica la riga appena aggiunta con il seguente formato:
content://user_dictionary/words/<id_value>
<id_value>
corrisponde al contenuto di _ID
per la nuova riga.
La maggior parte dei provider è in grado di rilevare automaticamente questo formato dell'URI dei contenuti e quindi di eseguire l'operazione richiesta su quella riga specifica.
Per ottenere il valore di _ID
dal valore Uri
restituito, chiama
ContentUris.parseId()
.
Aggiorna dati
Per aggiornare una riga, utilizza un oggetto ContentValues
con i valori aggiornati, proprio come fai con un inserimento, e i criteri di selezione, come faresti con una query.
Il metodo client che utilizzi è ContentResolver.update()
. Devi solo aggiungere
valori all'oggetto ContentValues
per le colonne che stai aggiornando. Se vuoi cancellare i contenuti di una colonna, imposta il valore su null
.
Lo snippet seguente modifica tutte le righe le cui impostazioni internazionali hanno la lingua "en"
in
un'impostazione internazionale pari a null
. Il valore restituito è il numero di righe che sono state aggiornate.
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 );
Elimina l'input utente quando chiami
ContentResolver.update()
. Per saperne di più, leggi la sezione Protezione da input dannosi.
Elimina i dati
L'eliminazione delle righe è simile al recupero dei dati delle righe. Sei tu a specificare i criteri di selezione per le righe da eliminare e il metodo client restituisce il numero di righe eliminate.
Lo snippet seguente elimina le righe il cui ID app corrisponde a "user"
. Il metodo restituisce il numero di righe eliminate.
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 );
Elimina l'input utente quando chiami
ContentResolver.delete()
. Per saperne di più, leggi la sezione Protezione da input dannosi.
Tipi di dati del fornitore
I fornitori di contenuti possono offrire molti tipi di dati diversi. Il fornitore del dizionario utente offre solo testo, ma i fornitori possono anche offrire i seguenti formati:
- Numero intero
- numero intero lungo (lungo)
- rappresentazione in virgola mobile
- rappresentazione in virgola mobile lunga (doppia)
Un altro tipo di dati utilizzato spesso dai provider è un Blob (Binary Large Object) implementato come array di 64 kB. Puoi visualizzare i tipi di dati disponibili esaminando i metodi "get" della classe Cursor
.
Il tipo di dati per ogni colonna di un provider è solitamente elencato nella relativa documentazione.
I tipi di dati relativi al fornitore del dizionario utente sono elencati nella documentazione di riferimento relativa alla classe di contratto UserDictionary.Words
. Le classi di contratto sono descritte nella sezione Classi di contratto.
Puoi anche determinare il tipo di dati chiamando Cursor.getType()
.
I provider mantengono inoltre le informazioni sul tipo di dati MIME per ogni URI di contenuto che definiscono. Puoi utilizzare le informazioni sul tipo MIME per scoprire se la tua applicazione può gestire i dati offerti dal provider o per scegliere un tipo di gestione in base al tipo MIME. Di solito è necessario il tipo MIME quando lavori con un provider che contiene strutture di dati o file complessi.
Ad esempio, la tabella ContactsContract.Data
nel provider di contatti utilizza i tipi MIME per etichettare il tipo di dati di contatto archiviati in ogni riga. Per ottenere il tipo MIME corrispondente a un URI di contenuti, chiama
ContentResolver.getType()
.
La sezione Riferimento tipo MIME descrive la sintassi dei tipi MIME standard e personalizzati.
Forme alternative di accesso del provider
Tre forme alternative di accesso al provider sono importanti nello sviluppo delle applicazioni:
-
Accesso in batch: puoi creare un batch di chiamate di accesso con i metodi della
classe
ContentProviderOperation
e quindi applicarli conContentResolver.applyBatch()
. -
Query asincrone: esegui le query in un thread separato. Puoi
utilizzare un oggetto
CursorLoader
. Gli esempi nella guida Caricatori mostrano come eseguire questa operazione. - Accesso ai dati tramite intent: anche se non è possibile inviare un intent direttamente a un provider, è possibile inviare un intent all'applicazione del provider, che di solito è la migliore per modificare i dati del provider.
L'accesso in batch e la modifica mediante intent sono descritti nelle sezioni seguenti.
Accesso in batch
L'accesso in batch a un provider è utile per inserire un numero elevato di righe, per inserire righe in più tabelle nella stessa chiamata al metodo e, in generale, per eseguire come transazione una serie di operazioni oltre i confini dei processi, chiamata operazione atomica.
Per accedere a un provider in modalità batch, crea un array di oggetti ContentProviderOperation
e inviali a un provider di contenuti con ContentResolver.applyBatch()
. Devi passare
l'autorità del fornitore di contenuti a questo metodo, anziché a un determinato URI di contenuti.
In questo modo ogni oggetto ContentProviderOperation
dell'array può funzionare su una tabella diversa. Una chiamata a ContentResolver.applyBatch()
restituisce un array di risultati.
La descrizione della classe di contratto ContactsContract.RawContacts
include uno snippet di codice che dimostra l'inserimento in gruppo.
Accesso ai dati tramite intent
Gli intent possono fornire accesso indiretto a un fornitore di contenuti. Puoi consentire all'utente di accedere ai dati in un provider anche se la tua applicazione non dispone delle autorizzazioni di accesso ottenendo un intent di risultato da un'applicazione che dispone delle autorizzazioni o attivando un'applicazione che dispone di autorizzazioni e consentendo all'utente di utilizzarla.
Ottenere l'accesso con autorizzazioni temporanee
Puoi accedere ai dati in un fornitore di contenuti, anche se non disponi delle autorizzazioni di accesso appropriate, inviando un intent a un'applicazione che dispone delle autorizzazioni e ricevendo di nuovo un intent di risultato contenente le autorizzazioni URI. Queste sono le autorizzazioni per un URI di contenuti specifico che durano fino al termine dell'attività che le riceve. L'applicazione con autorizzazioni permanenti concede autorizzazioni temporanee impostando un flag nell'intent del risultato:
-
Autorizzazione di lettura:
FLAG_GRANT_READ_URI_PERMISSION
-
Autorizzazione di scrittura:
FLAG_GRANT_WRITE_URI_PERMISSION
Nota: questi flag non concedono un accesso generale in lettura o scrittura al provider la cui autorità è contenuta nell'URI dei contenuti. L'accesso riguarda solo l'URI stesso.
Quando invii gli URI dei contenuti a un'altra app, includi almeno uno di questi flag. I flag forniscono le seguenti funzionalità a qualsiasi app che riceve un intent e ha come target Android 11 (livello API 30) o versioni successive:
- Leggi o scrivi ai dati rappresentati dall'URI dei contenuti, a seconda del flag incluso nell'intent.
- Ottieni visibilità del pacchetto sull'app contenente il fornitore di contenuti che corrisponde all'autorità URI. L'app che invia l'intent e quella che contiene il fornitore di contenuti potrebbero essere due app diverse.
Un provider definisce le autorizzazioni URI per gli URI dei contenuti nel file manifest, utilizzando
l'attributo android:grantUriPermissions
dell'elemento
<provider>
e l'elemento secondario
<grant-uri-permission>
dell'elemento
<provider>
. Il meccanismo delle autorizzazioni URI è spiegato più dettagliatamente nella guida Autorizzazioni su Android.
Ad esempio, puoi recuperare i dati per un contatto nel provider di contatti, anche se non disponi dell'autorizzazione READ_CONTACTS
. Potresti voler eseguire questa operazione in un'applicazione che invia messaggi di benvenuto a un contatto il giorno del suo compleanno. Anziché richiedere READ_CONTACTS
, che ti consente di accedere a tutti i contatti dell'utente e a tutte le sue informazioni, consenti all'utente di controllare quali contatti utilizza la tua applicazione. Per eseguire questa operazione, procedi nel seguente modo:
-
Nell'applicazione, invia un intent contenente l'azione
ACTION_PICK
e il tipo MIME "contacts"CONTENT_ITEM_TYPE
, utilizzando il metodostartActivityForResult()
. - Poiché questo intent corrisponde al filtro per intent per l'attività di "selezione" dell'app Persone, l'attività viene messa in primo piano.
-
Nell'attività di selezione, l'utente seleziona un contatto da aggiornare. In questo caso, l'attività di selezione chiama
setResult(resultcode, intent)
per configurare un intent da restituire alla tua applicazione. L'intent contiene l'URI dei contenuti del contatto selezionato dall'utente e i flag "extra"FLAG_GRANT_READ_URI_PERMISSION
. Questi flag concedono all'app l'autorizzazione URI per la lettura dei dati relativi al contatto a cui rimanda l'URI dei contenuti. L'attività di selezione chiama quindifinish()
per restituire il controllo all'applicazione. -
La tua attività torna in primo piano e il sistema chiama il relativo metodo
onActivityResult()
. Questo metodo riceve l'intent risultato creato dall'attività di selezione nell'app Persone. - Con l'URI dei contenuti dell'intent del risultato, puoi leggere i dati del contatto dal provider di contatti anche se non hai richiesto l'autorizzazione di accesso in lettura permanente al provider nel file manifest. Puoi quindi ottenere le informazioni sulla data di nascita o l'indirizzo email del contatto e inviare il messaggio di saluto.
Utilizza un'altra applicazione
Un altro modo per consentire all'utente di modificare i dati per i quali non disponi delle autorizzazioni di accesso è attivare un'applicazione che dispone delle autorizzazioni e consentire all'utente di lavorare lì.
Ad esempio, l'applicazione Calendar accetta un intent ACTION_INSERT
che consente di attivare la UI di inserimento dell'applicazione. In questo intent puoi passare dati "extra", che l'applicazione
utilizza per precompilare l'interfaccia utente. Poiché gli eventi ricorrenti hanno una sintassi complessa, il modo
preferito per inserire eventi nel provider di calendari è attivare l'app Calendar con un
ACTION_INSERT
e poi consentire all'utente di inserire l'evento lì.
Visualizzare i dati tramite un'app helper
Se la tua applicazione dispone delle autorizzazioni di accesso, potresti comunque utilizzare un intent per visualizzare i dati in un'altra applicazione. Ad esempio, l'applicazione Calendar accetta un intent ACTION_VIEW
che mostra una data o un evento specifici.
In questo modo puoi visualizzare le informazioni del calendario senza dover creare una tua interfaccia utente.
Per saperne di più su questa funzionalità, consulta la panoramica del provider di calendario.
L'applicazione a cui invii l'intent non deve essere necessariamente quella associata al provider. Ad esempio, puoi recuperare un contatto dal
provider di contatti e poi inviare un intent ACTION_VIEW
contenente l'URI di contenuti per l'immagine del contatto a un visualizzatore di immagini.
Classi di contratto
Una classe di contratto definisce le costanti che consentono alle applicazioni di utilizzare gli URI dei contenuti, i nomi delle colonne, le azioni intento e altre funzionalità di un fornitore di contenuti. Le classi di contratto non sono incluse automaticamente con un provider. Lo sviluppatore del provider deve definirli e quindi renderli disponibili ad altri sviluppatori. Molti dei provider inclusi nella piattaforma Android
hanno classi di contratto corrispondenti nel pacchetto android.provider
.
Ad esempio, il fornitore del dizionario utente ha una classe di contratto UserDictionary
contenente l'URI del contenuto e le costanti dei nomi di colonna. L'URI dei contenuti per la tabella Words
è definito nella costante UserDictionary.Words.CONTENT_URI
.
La classe UserDictionary.Words
contiene anche le costanti dei nomi di colonna,
utilizzate negli snippet di esempio in questa guida. Ad esempio, una proiezione delle query può essere definita come segue:
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 };
Un'altra classe di contratto è ContactsContract
per il fornitore di contatti.
La documentazione di riferimento per questa classe include snippet di codice di esempio. Una delle sue
sottoclassi, ContactsContract.Intents.Insert
, è una classe di
contratto che contiene le costanti per i dati sugli intent e sugli intent.
Riferimento tipo MIME
I fornitori di contenuti possono restituire tipi multimediali MIME standard, stringhe di tipo MIME personalizzate o entrambi.
I tipi MIME hanno il formato seguente:
type/subtype
Ad esempio, il noto tipo MIME text/html
ha il tipo text
e
il sottotipo html
. Se il provider restituisce questo tipo per un URI, significa che una query che utilizza tale URI restituisce il testo contenente tag HTML.
Le stringhe di tipo MIME personalizzato, chiamate anche tipi MIME specifici del fornitore, hanno valori type e subtype più complessi. Per più righe, il valore del tipo è sempre il seguente:
vnd.android.cursor.dir
Per una singola riga, il valore del tipo è sempre il seguente:
vnd.android.cursor.item
subtype è specifico del provider. I provider con tecnologia Android integrata di solito hanno un sottotipo semplice. Ad esempio, quando l'applicazione Contatti crea una riga per un numero di telefono, imposta il seguente tipo MIME nella riga:
vnd.android.cursor.item/phone_v2
Il valore del sottotipo è phone_v2
.
Gli sviluppatori di altri provider possono creare il proprio pattern di sottotipi in base all'autorità e ai nomi tabella del provider. Prendiamo come esempio un fornitore che include gli orari dei treni.
L'autorità del provider è com.example.trains
e contiene le tabelle Line1, Line2 e Line3. In risposta al seguente URI di contenuti per la tabella Line1:
content://com.example.trains/Line1
il provider restituisce il seguente tipo MIME:
vnd.android.cursor.dir/vnd.example.line1
In risposta al seguente URI di contenuti per la riga 5 della tabella Line2:
content://com.example.trains/Line2/5
il provider restituisce il seguente tipo MIME:
vnd.android.cursor.item/vnd.example.line2
La maggior parte dei fornitori di contenuti definisce le costanti delle classi di contratto per i tipi MIME utilizzati. La
classe di contratto del fornitore di contatti ContactsContract.RawContacts
,
ad esempio, definisce la costante
CONTENT_ITEM_TYPE
per il tipo MIME di
una singola riga di contatto non elaborata.
Gli URI contenuti per le singole righe sono descritti nella sezione URI contenuti.