Nozioni di base sui fornitori di contenuti

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 lavorare con i dati. Tuttavia, i fornitori di contenuti vengono utilizzati principalmente da altre applicazioni, che accedono al fornitore utilizzando un oggetto client del fornitore. I fornitori e i relativi clienti insieme offrono un'interfaccia standard e coerente ai dati che gestisce anche la comunicazione tra processi e l'accesso sicuro ai dati.

In genere, collabori con i fornitori di contenuti in uno dei due scenari: implementazione di codice per accedere a un fornitore di contenuti esistente in un'altra applicazione o creazione di un nuovo fornitore di contenuti nella tua applicazione per condividere i dati con altre applicazioni.

Questa pagina illustra le nozioni di base relative alla collaborazione con i fornitori di contenuti esistenti. Per scoprire di più sull'implementazione dei fornitori di contenuti nelle tue applicazioni, vedi Creare un fornitore di contenuti.

In questo argomento vengono trattati i seguenti argomenti:

  • 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à delle API che facilitano la collaborazione con i fornitori.

Panoramica

Un fornitore di contenuti presenta i dati alle applicazioni esterne come una o più tabelle simili a quelle presenti in un database relazionale. Una riga rappresenta un'istanza di un determinato tipo di dati raccolti dal provider e ogni colonna della riga rappresenta un singolo dato raccolto per un'istanza.

Un fornitore di contenuti coordina l'accesso al livello di archiviazione dei dati nella tua applicazione per un numero di API e componenti diversi. Come illustrato nella figura 1, sono inclusi:

  • Condivisione dell'accesso ai dati dell'applicazione con altre applicazioni
  • Invio di dati a un widget
  • Restituire suggerimenti di ricerca personalizzati per la tua applicazione tramite il framework di ricerca utilizzando SearchRecentSuggestionsProvider
  • Sincronizzazione dei dati dell'applicazione con il server utilizzando un'implementazione di AbstractThreadedSyncAdapter
  • Caricamento dei dati nell'interfaccia utente utilizzando un CursorLoader
Relazione tra il fornitore di contenuti e altri componenti.

Figura 1. Relazione tra un fornitore di contenuti e altri componenti.

Accedi a un provider

Quando vuoi accedere ai dati di un fornitore di contenuti, utilizzi l'oggetto ContentResolver nel Context della tua applicazione per comunicare con il fornitore come client. L'oggetto ContentResolver comunica con l'oggetto provider, un'istanza di una classe che implementa ContentProvider.

L'oggetto provider riceve le richieste di dati dai client, esegue l'azione richiesta e restituisce i risultati. Questo oggetto ha metodi che chiamano metodi con nomi identici nell'oggetto provider, un'istanza di uno dei sottoclassi concreti 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 dall'interfaccia utente utilizza un CursorLoader per eseguire una query asincrona in background. Il pulsante Activity o Fragment nell'interfaccia utente chiama un CursorLoader per la query, che a sua volta recupera il ContentProvider utilizzando il ContentResolver.

In questo modo, l'interfaccia utente rimane disponibile per l'utente durante l'esecuzione della query. Questo pattern prevede l'interazione di una serie di oggetti diversi, nonché il meccanismo di archiviazione sottostante, come illustrato nella Figura 2.

Interazione tra ContentProvider, altre classi e archiviazione.

Figura 2. Interazione tra ContentProvider, altre classi e archiviazione.

Nota:per accedere a un fornitore, in genere l'applicazione deve richiedere autorizzazioni specifiche nel file manifest. Questo modello di sviluppo è descritto in modo più dettagliato nella sezione Autorizzazioni per i fornitori di contenuti.

Uno dei provider integrati nella piattaforma Android è il provider del dizionario utente, che memorizza le parole non standard che l'utente vuole conservare. La tabella 1 mostra come potrebbero essere i dati nella tabella di questo fornitore:

Tabella 1: tabella di esempio del dizionario utente.

parola id app di pubblicazione impostazioni internazionali _ID
mapreduce utente1 100 it_IT 1
precompiler user14 200 fr_FR 2
applet utente2 225 fr_CA 3
const utente1 255 pt_BR 4
int user5 100 en_UK 5

Nella tabella 1, ogni riga rappresenta un'istanza di una parola non trovata in un dizionario standard. Ogni colonna rappresenta un dato per la parola, ad esempio le impostazioni internazionali in cui è stata rilevata per la prima volta. Le intestazioni di colonna sono nomi di colonna memorizzati nel provider. Ad esempio, per fare riferimento alle impostazioni internazionali di una riga, fai riferimento alla colonna locale. Per questo fornitore, la colonna _ID funge da colonna chiave primaria gestita automaticamente dal fornitore.

Per ottenere un elenco di parole e relativi codici paese dal fornitore del dizionario utente, chiama ContentResolver.query(). Il metodo query() chiama il metodo ContentProvider.query() definito dal fornitore del dizionario utente. Le seguenti righe di codice mostrano una chiamata 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 di query(Uri,projection,selection,selectionArgs,sortOrder) corrispondono a un'istruzione SELECT SQL:

Tabella 2: query() rispetto alla query SQL.

argomento query() Parola chiave/parametro SELECT Note
Uri FROM table_name Uri corrisponde 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 le righe vengono visualizzate nel valore Cursor restituito.

URI dei contenuti

Un URI dei contenuti è un URI che identifica i dati in un provider. Gli URI dei contenuti includono il nome simbolico dell'intero provider, ovvero la sua autorità, e un nome che rimanda a una tabella, ovvero un percorso. Quando chiami un metodo client per accedere a una tabella in un provider, l'URI dei contenuti della tabella è uno degli argomenti.

Nelle righe di codice precedenti, la costante CONTENT_URI contiene l'URI dei contenuti della tabella Words dello User Dictionary Provider. L'oggetto ContentResolver analizza l'autorità dell'URI e la utilizza per risolvere il provider confrontandola con una tabella di sistema di fornitori noti. Il ContentResolver può quindi inviare gli argomenti della query al fornitore corretto.

ContentProvider utilizza la parte del percorso dell'URI contenuto per scegliere la tabella a cui accedere. In genere, un provider ha un percorso per ogni tabella che espone.

Nelle righe di codice precedenti, l'URI completo per la tabella Words è:

content://user_dictionary/words
  • La stringa content:// è lo schema, che è sempre presente e identifica l'URI dei contenuti.
  • La stringa user_dictionary è l'autorità del fornitore.
  • La stringa words è il percorso della tabella.

Molti provider ti consentono di accedere a una singola riga di una tabella aggiungendo un valore ID alla fine dell'URI. Ad esempio, per recuperare una riga il cui _ID è 4 dal Provider di dizionario utenti, puoi utilizzare questo URI di contenuto:

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 utilità per la costruzione di oggetti URI ben formattati da stringhe. La classe ContentUris contiene metodi di utilità per aggiungere valori ID a un URI. Lo snippet precedente utilizza withAppendedId() per aggiungere un ID all'URI dei contenuti del fornitore del dizionario utente.

Recuperare i dati dal fornitore

Questa sezione descrive come recuperare i dati da un provider, utilizzando come esempio il provider di dizionari di utenti.

Per chiarezza, gli snippet di codice in questa sezione chiamano ContentResolver.query() nel thread dell'interfaccia utente. Nel codice effettivo, tuttavia, esegui le query in modo asincrono in un thread separato. Puoi utilizzare la classe CursorLoader, descritta più dettagliatamente nella guida Loader. 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:

  1. Richiedi l'autorizzazione di accesso in lettura per il provider.
  2. Definisci il codice che invia una query al provider.

Richiedi autorizzazione di accesso in lettura

Per recuperare i dati da un fornitore, la tua applicazione deve disporre dell'autorizzazione di accesso in lettura per il fornitore. Non puoi richiedere questa autorizzazione in fase di esecuzione. Devi invece specificare che ti serve 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, concedono implicitamente questa richiesta.

Per trovare il nome esatto dell'autorizzazione di accesso in lettura per il provider che utilizzi, nonché i nomi di altre autorizzazioni di accesso utilizzate dal provider, consulta la documentazione del provider.

Il ruolo delle autorizzazioni nell'accesso ai fornitori è descritto in modo più dettagliato nella sezione Autorizzazioni per i fornitori di contenuti.

Il provider di dizionario utenti definisce l'autorizzazione android.permission.READ_USER_DICTIONARY nel proprio file manifest, quindi un'applicazione che vuole leggere dal provider deve richiedere questa autorizzazione.

Costruisci la query

Il passaggio successivo per recuperare i dati da un fornitore consiste nel creare una query. Lo snippet riportato di seguito definisce alcune variabili per accedere al provider di dizionari 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 come esempio il fornitore di dizionari dell'utente. Una query del client del 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 colonne 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.

Nel frammento successivo, se l'utente non inserisce una parola, la clausola di selezione viene impostata su null e la query restituisce tutte le parole del provider. Se l'utente inserisce una parola, la clausola di selezione è 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 al seguente statement 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 anziché le costanti della classe del contratto.

Protezione da input dannosi

Se i dati gestiti dal fornitore di contenuti si trovano in un database SQL, l'inclusione di dati esterni non attendibili nelle istruzioni SQL non elaborate può portare a SQL injection.

Prendi in considerazione 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;

In questo modo, consenti all'utente di concatenare potenzialmente SQL dannoso all'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 del database SQLite sottostante, a meno che non sia configurato per rilevare i tentativi di SQL injection.

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 dell'utente viene associato direttamente alla query anziché essere interpretato come parte di un'istruzione SQL. Poiché non viene trattato come SQL, l'input dell'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 = ?";

Configura l'array di argomenti di selezione nel seguente 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 come segue:

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 argomenti di selezione è il modo migliore per specificare una selezione, anche se il provider non è basato su un database SQL.

Visualizzare i risultati delle query

Il metodo client ContentResolver.query() restituisce sempre un Cursor contenente le colonne specificate dalla proiezione della query per le righe che corrispondono ai criteri di selezione della query. Un oggetto Cursor fornisce l'accesso in lettura casuale alle righe e alle colonne che contiene.

Utilizzando i metodi Cursor, puoi eseguire l'iterazione sulle righe dei risultati, determinare il tipo di dati di ogni colonna, estrarre i dati da una colonna ed esaminare altre proprietà dei risultati.

Alcune implementazioni di Cursor aggiornate automaticamente l'oggetto quando i dati del provider cambiano, attivano metodi in un oggetto osservatore quando Cursor cambia o entrambe le cose.

Nota:un fornitore può limitare l'accesso alle colonne in base alla natura dell'oggetto che esegue la query. Ad esempio, il provider di contatti limita l'accesso di alcune colonne agli adattatori di sincronizzazione, quindi non le restituisce a un'attività o a un servizio.

Se nessuna riga corrisponde ai criteri di selezione, il fornitore restituisce un oggetto Cursor per il quale Cursor.getCount() è equale 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 Exception.

Poiché un Cursor è un elenco di righe, un buon modo per visualizzare i contenuti di un Cursor è collegarlo a un ListView utilizzando un SimpleCursorAdapter.

Lo snippet seguente continua il codice dello snippet precedente. Crea un oggetto SimpleCursorAdapter contenente il Cursor recuperato dalla query e imposta questo oggetto come adattatore per 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);

Nota:per eseguire il backup di un ListView con un Cursor, il cursore deve contenere una colonna denominata _ID. Per questo motivo, la query mostrata in precedenza recupera la colonna _ID per la tabella Words, anche se ListView non la mostra. Questa limitazione spiega anche perché la maggior parte dei fornitori ha una colonna _ID per ciascuna delle sue tabelle.

Ottenere dati dai risultati delle query

Oltre a visualizzare i risultati delle query, puoi utilizzarli per altre attività. Ad esempio, puoi recuperare le ortografie dal fornitore del dizionario utente e poi cercarle in altri fornitori. A questo scopo, devi ripetere l'iterazione delle 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.

Rilascia le risorse dei risultati della query

Gli oggetti Cursor devono essere chiusi se non sono più necessari, in modo che le risorse associate vengano rilasciate prima. Questo può essere fatto chiamando close() o utilizzando un'istruzione try-with-resources nel linguaggio di programmazione Java o la funzione use() nel linguaggio di programmazione Kotlin.

Autorizzazioni del fornitore di contenuti

L'applicazione di un fornitore può specificare le autorizzazioni che altre applicazioni devono avere per accedere ai dati del fornitore. Queste autorizzazioni consentono all'utente di sapere a quali dati un'applicazione tenta di accedere. In base ai requisiti del fornitore, altre applicazioni richiedono le autorizzazioni necessarie per accedere al fornitore. 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 in lettura e scrittura completo, indipendentemente dalle autorizzazioni specificate.

Il fornitore del dizionario utente richiede l'autorizzazione android.permission.READ_USER_DICTIONARY per recuperare 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 le richiede con un elemento <uses-permission> nel file manifest. Quando Android Package Manager installa l'applicazione, l'utente deve approvare tutte le autorizzazioni richieste dall'applicazione. Se l'utente li approva, Package Manager continua l'installazione. Se l'utente non li approva, il gestore di pacchetti interrompe l'installazione.

Il seguente elemento di esempio <uses-permission> richiede l'accesso in lettura al Provider di dizionario utente:

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

L'impatto delle autorizzazioni sull'accesso dei fornitori è spiegato più dettagliatamente in Suggerimenti per la sicurezza.

Inserire, aggiornare ed eliminare i dati

Nello stesso modo in cui recuperi i dati da un fornitore, utilizzi anche l'interazione tra un client del fornitore e il ContentProvider del fornitore per modificarli. Chiami un metodo di ContentResolver con argomenti che vengono passati al metodo corrispondente di ContentProvider. Il provider e il client del provider gestiscono automaticamente le comunicazioni di sicurezza e di inter-elaborazione.

Inserisci i dati

Per inserire dati in un fornitore, chiami il metodo ContentResolver.insert(). Questo metodo inserisce una nuova riga nel provider e restituisce un URI dei contenuti per quella riga. Il seguente snippet mostra come inserire una nuova parola nel fornitore del 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 di una riga. Le colonne di questo oggetto non devono avere lo stesso tipo di dati e, se non vuoi specificare alcun valore, puoi impostare una colonna su null utilizzando ContentValues.putNull().

Lo snippet precedente non aggiunge la colonna _ID perché questa colonna viene mantenuta automaticamente. Il provider assegna un valore univoco di _ID a ogni riga aggiunta. I fornitori di solito 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> sono i contenuti di _ID per la nuova riga. La maggior parte dei provider è in grado di rilevare automaticamente questa forma di URI dei contenuti ed eseguire l'operazione richiesta su quella determinata riga.

Per ottenere il valore di _ID dal valore restituito di Uri, chiama ContentUris.parseId().

Aggiornare i dati

Per aggiornare una riga, utilizzi un oggetto ContentValues con i valori aggiornati, come faresti con un'inserzione, 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.

Il seguente snippet modifica tutte le righe la cui impostazioni internazionali hanno la lingua "en" in modo che abbiano le impostazioni internazionali 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
);

Esegui la convalida dell'input dell'utente quando chiami ContentResolver.update(). Per saperne di più, consulta la sezione Proteggersi da input dannosi.

Elimina i dati

L'eliminazione delle righe è simile al recupero dei dati delle righe. Specifica i criteri di selezione per le righe da eliminare e il metodo client restituisce il numero di righe eliminate. Il seguente snippet 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
);

Esegui la convalida dell'input dell'utente quando chiami ContentResolver.delete(). Per saperne di più su questo argomento, consulta la sezione Proteggere 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 offrire anche i seguenti formati:

  • Numero intero
  • Numero intero lungo (long)
  • virgola mobile
  • Punto floating lungo (doppio)

Un altro tipo di dati utilizzato spesso dai provider è un BLOB (binario di grandi dimensioni) implementato come array di byte da 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 è generalmente elencato nella relativa documentazione. I tipi di dati per il fornitore di dizionari utente sono elencati nella documentazione di riferimento per la relativa classe del 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 gestiscono anche le informazioni sul tipo di dati MIME per ogni URI dei contenuti che definiscono. Puoi utilizzare le informazioni sul tipo MIME per scoprire se la tua applicazione può gestire i dati offerti dal fornitore o per scegliere un tipo di gestione in base al tipo MIME. Di solito hai bisogno del tipo MIME quando collabori con un fornitore che contiene strutture o file di dati complessi.

Ad esempio, la tabella ContactsContract.Data nel provider di contatti utilizza i tipi MIME per etichettare il tipo di dati di contatto memorizzati in ogni riga. Per ottenere il tipo MIME corrispondente a un URI dei contenuti, chiama ContentResolver.getType().

La sezione Riferimento ai tipi MIME descrive la sintassi dei tipi MIME standard e personalizzati.

Forme alternative di accesso del fornitore

Nello sviluppo di applicazioni sono importanti tre forme alternative di accesso del provider:

L'accesso e la modifica collettivi 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 un insieme di operazioni oltre i confini dei processi come una transazione, chiamata operazione atomica.

Per accedere a un provider in modalità batch, crea un array di oggetti ContentProviderOperation, quindi inviali a un fornitore di contenuti con ContentResolver.applyBatch(). A questo metodo devi passare l'autorità del fornitore di contenuti, anziché un determinato URI dei contenuti.

In questo modo, ogni oggetto ContentProviderOperation nell'array può lavorare con una tabella diversa. Una chiamata a ContentResolver.applyBatch() restituisce un array di risultati.

La descrizione della classe del contratto ContactsContract.RawContacts include uno snippet di codice che mostra l'inserimento collettivo.

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 l'applicazione non dispone delle autorizzazioni di accesso recuperando un intent di risultato da un'applicazione che dispone delle autorizzazioni o attivando un'applicazione con autorizzazioni e consentendo all'utente di eseguire operazioni al suo interno.

Ottenere l'accesso con autorizzazioni temporanee

Puoi accedere ai dati in un provider di contenuti anche se non disponi delle autorizzazioni di accesso appropriate inviando un'intent a un'applicazione che dispone delle autorizzazioni e ricevendo un'intent di risultato contenente le autorizzazioni URI. Si tratta di autorizzazioni per un URI dei contenuti specifico che rimangono valide fino al termine dell'attività che le riceve. L'applicazione che dispone di autorizzazioni permanenti concede autorizzazioni temporary impostando un flag nell'intent del risultato:

Nota:questi flag non forniscono accesso in lettura o scrittura generale al fornitore whose authority è contenuta nell'URI dei contenuti. L'accesso è solo per l'URI stesso.

Quando invii URI dei contenuti a un'altra app, includi almeno uno di questi parametri. 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 sui dati rappresentati dall'URI dei contenuti, a seconda del flag incluso nell'intent.
  • Ottieni visibilità dei pacchetti nell'app con il fornitore di contenuti corrispondente all'autorità URI. L'app che invia l'intent e l'app 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 in modo più dettagliato nella guida Autorizzazioni su Android.

Ad esempio, puoi recuperare i dati di un contatto nel provider di contatti anche se non disponi dell'autorizzazione READ_CONTACTS. Potresti eseguire questa operazione in un'applicazione che invia a un contatto un'email di auguri per il suo compleanno. Anziché richiedere READ_CONTACTS, che ti consente di accedere a tutti i contatti e a tutte le informazioni dell'utente, lascia che sia l'utente a controllare quali contatti vengono utilizzati dalla tua applicazione. Per farlo, segui la procedura riportata di seguito:

  1. Nella tua applicazione, invia un intent contenente l'azione ACTION_PICK e il tipo MIME "contatti" CONTENT_ITEM_TYPE, utilizzando il metodo startActivityForResult().
  2. Poiché questo intent corrisponde al filtro per intent per l'attività "selezione" dell'app Contatti, l'attività viene messa in primo piano.
  3. 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'intenzione da restituire alla tua applicazione. L'intent contiene l'URI contenuto del contatto selezionato dall'utente e i flag "extra" FLAG_GRANT_READ_URI_PERMISSION. Questi flag concedono all'app l'autorizzazione URI per leggere i dati del contatto a cui rimanda l'URI dei contenuti. L'attività di selezione chiama quindi finish() per restituire il controllo all'applicazione.
  4. L'attività torna in primo piano e il sistema chiama il metodo onActivityResult() dell'attività. Questo metodo riceve l'intent del risultato creato dall'attività di selezione nell'app Persone.
  5. 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 manifest. Puoi quindi recuperare le informazioni sul compleanno o sull'indirizzo email del contatto e inviare l'e-greeting.

Usa 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 lasciare che sia l'utente a eseguire il lavoro lì.

Ad esempio, l'applicazione Calendar accetta un intento ACTION_INSERT che ti consente di attivare l'interfaccia utente di inserimento dell'applicazione. In questo intento 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 gli eventi nel provider di calendario è attivare l'app Calendar con un ACTION_INSERT e consentire all'utente di inserire l'evento lì.

Visualizzare i dati utilizzando un'app di supporto

Se la tua applicazione dispone di autorizzazioni di accesso, potresti comunque utilizzare un intent per visualizzare i dati in un'altra applicazione. Ad esempio, l'applicazione Calendar accetta un intento ACTION_VIEW che mostra una determinata data o un evento. In questo modo puoi visualizzare le informazioni del calendario senza dover creare la tua interfaccia utente. Per scoprire di più su questa funzionalità, consulta la panoramica dei fornitori di calendario.

L'applicazione a cui invii l'intent non deve necessariamente essere quella associata al fornitore. Ad esempio, puoi recuperare un contatto dal fornitore di servizi di contatto, quindi inviare un'intenzione ACTION_VIEW contenente l'URI dei contenuti per l'immagine del contatto a un visualizzatore di immagini.

Classi di contratto

Una classe di contratto definisce costanti che aiutano le applicazioni a lavorare con gli URI dei contenuti, i nomi delle colonne, le azioni di intent e altre funzionalità di un fornitore di contenuti. Le classi contrattuali non sono incluse automaticamente con un provider. Lo sviluppatore del provider deve definirli e poi metterli a disposizione di altri sviluppatori. Molti dei fornitori 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 costanti URI dei contenuti e 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 costanti dei nomi delle colonne, che vengono utilizzate negli snippet di esempio di questa guida. Ad esempio, una proiezione di 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 provider di contatti. La documentazione di riferimento di questa classe include snippet di codice di esempio. Uno dei suoi sottoclassi, ContactsContract.Intents.Insert, è una classe di contratto che contiene costanti per intent e dati degli intent.

Riferimento ai tipi MIME

I fornitori di contenuti possono restituire tipi di contenuti MIME standard, stringhe di tipo MIME personalizzate o entrambi.

I tipi MIME hanno il seguente formato:

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 questo URI restituisce testo contenente tag HTML.

Le stringhe di tipo MIME personalizzate, 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

L'elemento subtype è specifico del fornitore. I provider integrati di Android hanno in genere 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 altri sviluppatori di fornitori possono creare il proprio pattern di sottotipi in base all'autorità e ai nomi delle tabelle del fornitore. Ad esempio, prendiamo in considerazione un fornitore che contiene gli orari dei treni. L'autorità del provider è com.example.trains e contiene le tabelle Line1, Line2 e Line3. In risposta al seguente URI dei contenuti per la tabella Line1:

content://com.example.trains/Line1

Il fornitore restituisce il seguente tipo MIME:

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

In risposta al seguente URI dei contenuti per la riga 5 della tabella Line2:

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

Il fornitore restituisce il seguente tipo MIME:

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

La maggior parte dei fornitori di contenuti definisce le costanti delle classi contrattuali per i tipi MIME utilizzati. La classe del 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 dei contenuti per le singole righe sono descritti nella sezione URI dei contenuti.