Crea un fornitore di contenuti

Un fornitore di contenuti gestisce l'accesso a un repository centrale di dati. Puoi implementare un provider come una o più classi in un'applicazione Android, insieme agli elementi nel file manifest. Una delle tue classi implementa una sottoclasse ContentProvider, che è l'interfaccia tra il tuo provider e altre applicazioni.

Sebbene i fornitori di contenuti siano pensati per rendere disponibili i dati ad altre applicazioni, la tua applicazione può contenere attività che consentono all'utente di eseguire query e modificare i dati gestiti dal provider.

Questa pagina contiene la procedura di base per creare un fornitore di contenuti e un elenco di API da utilizzare.

Prima di iniziare a creare

Prima di iniziare a creare un provider, considera quanto segue:

  • Stabilisci se hai bisogno di un fornitore di contenuti. Devi creare un fornitore di contenuti se vuoi fornire una o più delle seguenti funzionalità:
    • Vuoi offrire dati o file complessi ad altre applicazioni.
    • Vuoi consentire agli utenti di copiare dati complessi dalla tua app ad altre app.
    • Vuoi fornire suggerimenti di ricerca personalizzati utilizzando il framework di ricerca.
    • Vuoi esporre i dati dell'applicazione ai widget.
    • Vuoi implementare le classi AbstractThreadedSyncAdapter, CursorAdapter o CursorLoader.

    Non è necessario che un provider utilizzi database o altri tipi di archiviazione permanente se l'utilizzo avviene interamente all'interno della tua applicazione e non hai bisogno delle funzionalità precedenti elencate. Puoi invece utilizzare uno dei sistemi di archiviazione descritti in Panoramica dell'archiviazione di dati e file.

  • Se non l'hai ancora fatto, consulta le Nozioni di base sui fornitori di contenuti per saperne di più sui provider e sul loro funzionamento.

Quindi, segui questi passaggi per creare il tuo provider:

  1. Progetta l'archiviazione non elaborata per i tuoi dati. Un fornitore di contenuti offre i dati in due modi:
    Dati dei file
    Dati normalmente inseriti nei file, come foto, audio o video. Archivia i file nello spazio privato della tua applicazione. In risposta alla richiesta di un file da un'altra applicazione, il tuo provider può offrire un handle per il file.
    Dati "strutturati"
    Dati che normalmente vengono inseriti in un database, in un array o in una struttura simile. Archivia i dati in un formato compatibile con le tabelle di righe e colonne. Una riga rappresenta un'entità, ad esempio una persona o un elemento nell'inventario. Una colonna rappresenta alcuni dati per l'entità, come il nome di una persona o il prezzo di un articolo. Un modo comune per archiviare questo tipo di dati è in un database SQLite, ma puoi utilizzare qualsiasi tipo di archiviazione permanente. Per scoprire di più sui tipi di archiviazione disponibili per il sistema Android, consulta la sezione Archiviazione dei dati di progettazione.
  2. Definisci un'implementazione concreta della classe ContentProvider e dei relativi metodi richiesti. Questa classe è l'interfaccia tra i tuoi dati e il resto del sistema Android. Per maggiori informazioni su questa classe, consulta la sezione Implementare la classe ContentProvider.
  3. Definisci la stringa dell'autorità del provider, gli URI dei contenuti e i nomi delle colonne. Se vuoi che l'applicazione del provider gestisca gli intent, definisci anche azioni intent, dati extra e flag. Definisci anche le autorizzazioni necessarie per le applicazioni che vogliono accedere ai tuoi dati. Valuta la possibilità di definire tutti questi valori come costanti in una classe di contratto separata. In seguito, puoi esporre questo corso ad altri sviluppatori. Per ulteriori informazioni sugli URI di contenuti, consulta la sezione Progettare gli URI di contenuti. Per maggiori informazioni sugli intent, consulta la sezione Intent e accesso ai dati.
  4. Aggiungi altre parti facoltative, come i dati di esempio o un'implementazione di AbstractThreadedSyncAdapter, in grado di sincronizzare i dati tra il provider e i dati basati su cloud.

Archiviazione dati di progettazione

Un fornitore di contenuti è l'interfaccia per i dati salvati in un formato strutturato. Prima di creare l'interfaccia, decidi come archiviare i dati. Puoi archiviare i dati in qualsiasi forma e poi progettare l'interfaccia in modo da leggere e scrivere i dati in base alle esigenze.

Di seguito sono riportate alcune delle tecnologie di archiviazione dati disponibili su Android:

  • Se lavori con dati strutturati, prendi in considerazione un database relazionale come SQLite o un datastore di coppie chiave-valore non relazionale come LevelDB. Se lavori con dati non strutturati come file audio, immagini o video, valuta la possibilità di archiviare i dati come file. Puoi combinare diversi tipi di archiviazione ed esporli utilizzando un unico fornitore di contenuti, se necessario.
  • Il sistema Android può interagire con la libreria di persistenza della stanza, che fornisce accesso all'API di database SQLite utilizzata dai provider di Android per archiviare i dati orientati alle tabelle. Per creare un database utilizzando questa libreria, crea un'istanza di una sottoclasse di RoomDatabase, come descritto in Salvare i dati in un database locale utilizzando Room.

    Non è necessario utilizzare un database per implementare il repository. Un provider appare esternamente come un insieme di tabelle, simile a un database relazionale, ma questo non è un requisito per l'implementazione interna del provider.

  • Per archiviare i dati dei file, Android offre diverse API orientate ai file. Per scoprire di più sull'archiviazione dei file, leggi la panoramica sull'archiviazione di dati e file. Se stai progettando un provider che offre dati relativi ai contenuti multimediali, come musica o video, puoi avere un fornitore che combina dati delle tabelle e file.
  • In rari casi, potresti trarre vantaggio dall'implementazione di più fornitori di contenuti per una singola applicazione. Ad esempio, potresti voler condividere alcuni dati con un widget utilizzando un fornitore di contenuti ed esporre un set di dati diverso da condividere con altre applicazioni.
  • Per lavorare con i dati basati sulla rete, utilizza le classi in java.net e android.net. Puoi anche sincronizzare i dati basati sulla rete con un datastore locale, ad esempio un database, e poi offrire i dati sotto forma di tabelle o file.

Nota: se apporti una modifica al repository che non è compatibile con le versioni precedenti, devi contrassegnarlo con un nuovo numero di versione. Devi anche aumentare il numero di versione dell'app che implementa il nuovo fornitore di contenuti. Questa modifica impedisce che i downgrade del sistema provochino l'arresto anomalo del sistema quando tenta di reinstallare un'app con un fornitore di contenuti incompatibile.

Considerazioni sulla progettazione dei dati

Ecco alcuni suggerimenti per progettare la struttura dei dati del tuo provider:

  • I dati della tabella devono avere sempre una colonna "chiave primaria" che il provider conserva come valore numerico univoco per ogni riga. Puoi utilizzare questo valore per collegare la riga alle righe correlate in altre tabelle (utilizzando questo valore come "chiave esterna"). Anche se puoi utilizzare qualsiasi nome per questa colonna, l'utilizzo di BaseColumns._ID è la scelta migliore, perché il collegamento dei risultati di una query del provider a un ListView richiede che una delle colonne recuperate abbia il nome _ID.
  • Se vuoi fornire immagini bitmap o altre parti molto grandi di dati orientati ai file, archivia i dati in un file e quindi forniscili in modo indiretto anziché archiviarli direttamente in una tabella. Se lo fai, devi comunicare agli utenti del tuo provider che devono utilizzare un metodo file ContentResolver per accedere ai dati.
  • Utilizza il tipo di dati BLOB (Binario Grande Oggetto) per archiviare dati che variano di dimensioni o hanno una struttura diversa. Ad esempio, puoi utilizzare una colonna BLOB per archiviare un buffer di protocollo o una struttura JSON.

    Puoi utilizzare un BLOB anche per implementare una tabella indipendente dallo schema. In questo tipo di tabella, definisci una colonna di chiave primaria, una colonna di tipo MIME e una o più colonne generiche come BLOB. Il significato dei dati nelle colonne BLOB è indicato dal valore nella colonna Tipo MIME. In questo modo puoi archiviare diversi tipi di riga nella stessa tabella. La tabella "dati" del provider di contatti ContactsContract.Data è un esempio di tabella indipendente dallo schema.

Progettare gli URI dei 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 o a un file (un percorso). La parte facoltativa dell'ID rimanda a una singola riga in una tabella. Ogni metodo di accesso ai dati di ContentProvider ha un URI di contenuti come argomento. In questo modo puoi determinare la tabella, la riga o il file a cui accedere.

Per informazioni sugli URI di contenuti, consulta Nozioni di base sui provider di contenuti.

Progetta un'autorità

Un provider di solito ha una singola autorità, che funge da nome interno ad Android. Per evitare conflitti con altri provider, utilizza la proprietà del dominio internet (in senso inverso) come base dell'autorità del provider. Poiché questo consiglio è valido anche per i nomi dei pacchetti Android, puoi definire l'autorità del provider come estensione del nome del pacchetto contenente il provider.

Ad esempio, se il nome del pacchetto Android è com.example.<appname>, assegna al tuo provider l'autorità com.example.<appname>.provider.

Progettare la struttura di un percorso

In genere gli sviluppatori creano URI di contenuti forniti dall'autorità aggiungendo percorsi che rimandano a singole tabelle. Ad esempio, se hai due tabelle, table1 e table2, puoi combinarle con l'autorità dell'esempio precedente per ottenere gli URI dei contenuti com.example.<appname>.provider/table1 e com.example.<appname>.provider/table2. I percorsi non sono limitati a un singolo segmento e non è necessario che esista una tabella per ogni livello del percorso.

Gestire gli ID URI dei contenuti

Per convenzione, i provider offrono l'accesso a una singola riga in una tabella accettando un URI dei contenuti con un valore ID per la riga alla fine dell'URI. Sempre per convenzione, i provider associano il valore dell'ID alla colonna _ID della tabella ed eseguono l'accesso richiesto in base alla riga corrispondente.

Questa convenzione facilita uno schema di progettazione comune per le app che accedono a un provider. L'app esegue una query sul provider e mostra il valore Cursor risultante in un ListView utilizzando un CursorAdapter. La definizione di CursorAdapter richiede che una delle colonne in Cursor sia _ID

L'utente sceglie poi una delle righe visualizzate dall'interfaccia utente per esaminare o modificare i dati. L'app recupera la riga corrispondente da Cursor a supporto di ListView, riceve il valore _ID per questa riga, lo aggiunge all'URI del contenuto e invia la richiesta di accesso al provider. Il provider può quindi eseguire la query o la modifica in base alla riga esatta selezionata dall'utente.

Pattern URI contenuti

Per aiutarti a scegliere quale azione eseguire per un URI dei contenuti in entrata, l'API del provider include la classe di convenienza UriMatcher, che mappa i pattern degli URI dei contenuti a valori interi. Puoi utilizzare i valori interi in un'istruzione switch che scelga l'azione desiderata per gli URI o gli URI dei contenuti che corrispondono a un determinato pattern.

Un pattern dell'URI dei contenuti corrisponde agli URI dei contenuti utilizzando caratteri jolly:

  • * corrisponde a una stringa di caratteri validi di qualsiasi lunghezza.
  • # corrisponde a una stringa di caratteri numerici di qualsiasi lunghezza.

Come esempio di progettazione e codifica della gestione dell'URI di contenuti, considera un provider con l'autorità com.example.app.provider che riconosce i seguenti URI di contenuti che puntano alle tabelle:

  • content://com.example.app.provider/table1: una tabella denominata table1.
  • content://com.example.app.provider/table2/dataset1: una tabella denominata dataset1.
  • content://com.example.app.provider/table2/dataset2: una tabella denominata dataset2.
  • content://com.example.app.provider/table3: una tabella denominata table3.

Il provider riconosce anche questi URI di contenuti se hanno un ID riga aggiunto, ad esempio content://com.example.app.provider/table3/1 per la riga identificata da 1 in table3.

Sono possibili i seguenti pattern degli URI dei contenuti:

content://com.example.app.provider/*
Corrisponde a qualsiasi URI di contenuti del provider.
content://com.example.app.provider/table2/*
Corrisponde a un URI di contenuti per le tabelle dataset1 e dataset2, ma non corrisponde agli URI dei contenuti per table1 o table3.
content://com.example.app.provider/table3/#
Corrisponde a un URI dei contenuti per le singole righe in table3, ad esempio content://com.example.app.provider/table3/6 per la riga identificata da 6.

Il seguente snippet di codice mostra come funzionano i metodi in UriMatcher. Questo codice gestisce gli URI di un'intera tabella in modo diverso dagli URI di una singola riga utilizzando il pattern URI dei contenuti content://<authority>/<path> per le tabelle e content://<authority>/<path>/<id> per le righe singole.

Il metodo addURI() mappa un'autorità e un percorso a un valore intero. Il metodo match() restituisce il valore intero per un URI. Un'istruzione switch sceglie se eseguire query sull'intera tabella e su un singolo record.

Kotlin

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * The calls to addURI() go here for all the content URI patterns that the provider
     * recognizes. For this snippet, only the calls for table 3 are shown.
     */

    /*
     * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
     * in the path.
     */
    addURI("com.example.app.provider", "table3", 1)

    /*
     * Sets the code for a single row to 2. In this case, the # wildcard is
     * used. content://com.example.app.provider/table3/3 matches, but
     * content://com.example.app.provider/table3 doesn't.
     */
    addURI("com.example.app.provider", "table3/#", 2)
}
...
class ExampleProvider : ContentProvider() {
    ...
    // Implements ContentProvider.query()
    override fun query(
            uri: Uri?,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
    ): Cursor? {
        var localSortOrder: String = sortOrder ?: ""
        var localSelection: String = selection ?: ""
        when (sUriMatcher.match(uri)) {
            1 -> { // If the incoming URI was for all of table3
                if (localSortOrder.isEmpty()) {
                    localSortOrder = "_ID ASC"
                }
            }
            2 -> {  // If the incoming URI was for a single row
                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                localSelection += "_ID ${uri?.lastPathSegment}"
            }
            else -> { // If the URI isn't recognized,
                // do some error handling here
            }
        }

        // Call the code to actually do the query
    }
}

Java

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here for all the content URI patterns that the provider
         * recognizes. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to one. No wildcard is used
         * in the path.
         */
        uriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the # wildcard is
         * used. content://com.example.app.provider/table3/3 matches, but
         * content://com.example.app.provider/table3 doesn't.
         */
        uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (uriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query.
                 */
                selection = selection + "_ID = " + uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI isn't recognized, do some error handling here
        }
        // Call the code to actually do the query
    }

Un'altra classe, ContentUris, fornisce dei metodi di convenienza per lavorare con la parte id degli URI dei contenuti. Le classi Uri e Uri.Builder includono metodi pratici per analizzare gli oggetti Uri esistenti e crearne di nuovi.

Implementare la classe ContentProvider

L'istanza ContentProvider gestisce l'accesso a un set strutturato di dati gestendo le richieste da altre applicazioni. Tutte le forme di accesso alla fine chiamano ContentResolver, che a sua volta chiama un metodo concreto di ContentProvider per ottenere l'accesso.

Metodi obbligatori

La classe astratta ContentProvider definisce sei metodi astratti da implementare come parte della sottoclasse concreta. Tutti questi metodi, tranne onCreate(), vengono chiamati da un'applicazione client che sta tentando di accedere al tuo fornitore di contenuti.

query()
Recupera i dati dal tuo provider. Utilizza gli argomenti per selezionare la tabella su cui eseguire la query, le righe e le colonne da restituire e l'ordinamento del risultato. Restituisci i dati come oggetto Cursor.
insert()
Inserisci una nuova riga nel provider. Utilizza gli argomenti per selezionare la tabella di destinazione e ottenere i valori della colonna da utilizzare. Restituisci un URI di contenuti per la riga appena inserita.
update()
Aggiorna le righe esistenti nel provider. Utilizza gli argomenti per selezionare la tabella e le righe da aggiornare e per ottenere i valori della colonna aggiornati. Restituisce il numero di righe aggiornate.
delete()
Elimina le righe dal provider. Utilizza gli argomenti per selezionare la tabella e le righe da eliminare. Restituisce il numero di righe eliminate.
getType()
Restituisce il tipo MIME corrispondente a un URI di contenuto. Questo metodo è descritto più dettagliatamente nella sezione Implementare i tipi MIME dei provider di contenuti.
onCreate()
Inizializza il provider. Il sistema Android chiama questo metodo subito dopo aver creato il provider. Il provider non viene creato finché un oggetto ContentResolver non tenta di accedervi.

Questi metodi hanno la stessa firma dei metodi ContentResolver con lo stesso nome.

L'implementazione di questi metodi deve tenere conto di quanto segue:

  • Tutti questi metodi tranne onCreate() possono essere chiamati da più thread contemporaneamente, quindi devono essere adatti ai thread. Per scoprire di più su più thread, consulta la panoramica dei processi e dei thread.
  • Evita di eseguire operazioni lunghe in onCreate(). Rimanda le attività di inizializzazione fino a quando non sono effettivamente necessarie. La sezione sull'implementazione del metodo onCreate() illustra questo aspetto in modo più dettagliato.
  • Anche se devi implementare questi metodi, il codice non deve fare altro che restituire il tipo di dati previsto. Ad esempio, puoi impedire ad altre applicazioni di inserire dati in alcune tabelle ignorando la chiamata a insert() e restituendo 0.

Implementare il metodo query()

Il metodo ContentProvider.query() deve restituire un oggetto Cursor o, in caso di errore, generare un Exception. Se utilizzi un database SQLite come archiviazione dei dati, puoi restituire il valore Cursor restituito da uno dei metodi query() della classe SQLiteDatabase.

Se la query non corrisponde ad alcuna riga, restituisci un'istanza Cursor il cui metodo getCount() restituisce 0. Restituisce null solo se si è verificato un errore interno durante il processo di query.

Se non utilizzi un database SQLite come archiviazione dei dati, usa una delle sottoclassi concrete di Cursor. Ad esempio, la classe MatrixCursor implementa un cursore in cui ogni riga è un array di istanze Object. Con questo corso, utilizza addRow() per aggiungere una nuova riga.

Il sistema Android deve essere in grado di comunicare Exception oltre i confini dei processi. Android può farlo per le seguenti eccezioni, utili per gestire gli errori delle query:

Implementare il metodo insert()

Il metodo insert() aggiunge una nuova riga alla tabella appropriata, utilizzando i valori nell'argomento ContentValues. Se il nome di una colonna non è nell'argomento ContentValues, puoi fornire un valore predefinito nel codice del provider o nello schema del database.

Questo metodo restituisce l'URI dei contenuti per la nuova riga. Per creare questa operazione, aggiungi la chiave primaria della nuova riga, in genere il valore _ID, all'URI dei contenuti della tabella utilizzando withAppendedId().

Implementare il metodo delete()

Il metodo delete() non deve eliminare righe dall'archiviazione dei dati. Se utilizzi un adattatore di sincronizzazione con il tuo provider, valuta la possibilità di contrassegnare una riga eliminata con un flag "delete" anziché rimuoverla completamente. L'adattatore di sincronizzazione può controllare la presenza di righe eliminate e rimuoverle dal server prima di eliminarle dal provider.

Implementare il metodo update()

Il metodo update() accetta lo stesso argomento ContentValues usato da insert() e gli stessi argomenti selection e selectionArgs usati da delete() e ContentProvider.query(). Ciò potrebbe consentirti di riutilizzare il codice tra questi metodi.

Implementare il metodo onCreate()

Il sistema Android chiama onCreate() all'avvio del provider. Esegui solo attività di inizializzazione a esecuzione rapida in questo metodo e posticipa la creazione del database e il caricamento dei dati fino a quando il provider non riceve effettivamente una richiesta per i dati. Se esegui attività lunghe in onCreate(), rallenti l'avvio del provider. A sua volta, questo rallenta la risposta del provider ad altre applicazioni.

I due snippet seguenti mostrano l'interazione tra ContentProvider.onCreate() e Room.databaseBuilder(). Il primo snippet mostra l'implementazione di ContentProvider.onCreate(), in cui viene creato l'oggetto di database e gestisce gli oggetti di accesso ai dati:

Kotlin

// Defines the database name
private const val DBNAME = "mydb"
...
class ExampleProvider : ContentProvider() {

    // Defines a handle to the Room database
    private lateinit var appDatabase: AppDatabase

    // Defines a Data Access Object to perform the database operations
    private var userDao: UserDao? = null

    override fun onCreate(): Boolean {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(context, AppDatabase::class.java, DBNAME).build()

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.userDao

        return true
    }
    ...
    // Implements the provider's insert method
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Java

public class ExampleProvider extends ContentProvider

    // Defines a handle to the Room database
    private AppDatabase appDatabase;

    // Defines a Data Access Object to perform the database operations
    private UserDao userDao;

    // Defines the database name
    private static final String DBNAME = "mydb";

    public boolean onCreate() {

        // Creates a new database object
        appDatabase = Room.databaseBuilder(getContext(), AppDatabase.class, DBNAME).build();

        // Gets a Data Access Object to perform the database operations
        userDao = appDatabase.getUserDao();

        return true;
    }
    ...
    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which DAO to use when inserting data, handle error conditions, etc.
    }
}

Implementare i tipi MIME ContentProvider

La classe ContentProvider prevede due metodi per restituire i tipi MIME:

getType()
Uno dei metodi obbligatori che implementi per qualsiasi provider.
getStreamTypes()
Un metodo che dovresti implementare se il tuo provider offre file.

Tipi MIME per le tabelle

Il metodo getType() restituisce un String in formato MIME che descrive il tipo di dati restituiti dall'argomento URI del contenuto. L'argomento Uri può essere un pattern anziché un URI specifico. In questo caso, restituisci il tipo di dati associati agli URI di contenuti che corrispondono al pattern.

Per tipi di dati comuni come testo, HTML o JPEG, getType() restituisce il tipo MIME standard per quei dati. Un elenco completo di questi tipi di standard è disponibile sul sito web di IANA MIME Media Tipi.

Per gli URI di contenuti che rimandano a una o più righe di dati di una tabella, getType() restituisce un tipo MIME nel formato MIME specifico del fornitore di Android:

  • Digita la parte: vnd
  • Sottotipo di parte:
    • Se il pattern URI è per una singola riga: android.cursor.item/
    • Se il pattern URI è per più di una riga: android.cursor.dir/
  • Parte specifica del fornitore: vnd.<name>.<type>

    Tu fornisci i <name> e le <type>. Il valore <name> è univoco a livello globale, mentre il valore <type> è univoco per il pattern URI corrispondente. Una buona scelta per <name> è il nome della tua azienda o una parte del nome del pacchetto Android della tua applicazione. Una buona scelta per <type> è una stringa che identifica la tabella associata all'URI.

Ad esempio, se l'autorità di un provider è com.example.app.provider ed espone una tabella denominata table1, il tipo MIME per più righe in table1 è:

vnd.android.cursor.dir/vnd.com.example.provider.table1

Per una singola riga di table1, il tipo MIME è:

vnd.android.cursor.item/vnd.com.example.provider.table1

Tipi MIME per i file

Se il tuo provider offre file, implementa getStreamTypes(). Il metodo restituisce un array String di tipi MIME per i file che il provider può restituire per un determinato URI di contenuti. Filtra i tipi MIME offerti in base all'argomento del filtro del tipo MIME, in modo da restituire solo i tipi MIME che il client vuole gestire.

Ad esempio, prendi in considerazione un fornitore che offre immagini fotografiche come file in formato JPG, PNG e GIF. Se un'applicazione chiama ContentResolver.getStreamTypes() con la stringa di filtro image/*, per qualcosa che è un'"immagine", il metodo ContentProvider.getStreamTypes() restituisce l'array:

{ "image/jpeg", "image/png", "image/gif"}

Se l'app è interessata solo ai file JPG, può chiamare ContentResolver.getStreamTypes() con la stringa di filtro *\/jpeg e getStreamTypes() restituisce:

{"image/jpeg"}

Se il tuo provider non offre nessuno dei tipi MIME richiesti nella stringa di filtro, getStreamTypes() restituisce null.

Implementare una classe di contratto

Una classe di contratto è una classe public final che contiene definizioni costanti di URI, nomi di colonna, tipi MIME e altri metadati di pertinenza del provider. La classe stabilisce un contratto tra il provider e altre applicazioni garantendo l'accesso corretto al provider anche in caso di modifiche ai valori effettivi degli URI, dei nomi delle colonne e così via.

Una classe contratto è utile anche agli sviluppatori perché in genere utilizza nomi mnemonici per le costanti, pertanto è meno probabile che gli sviluppatori utilizzino valori errati per i nomi di colonna o gli URI. Poiché è una classe, può contenere documentazione Javadoc. Gli ambienti di sviluppo integrati come Android Studio possono completare automaticamente i nomi delle costanti della classe del contratto e visualizzare Javadoc per le costanti.

Gli sviluppatori non possono accedere al file della classe della classe di contratto dall'applicazione, ma possono compilarlo in modo statico nell'applicazione da un file JAR fornito da te.

La classe ContactsContract e le sue classi nidificate sono esempi di classi di contratto.

Implementare le autorizzazioni del fornitore di contenuti

Le autorizzazioni e l'accesso per tutti gli aspetti del sistema Android sono descritti in dettaglio in Suggerimenti per la sicurezza. La panoramica sull'archiviazione di dati e file descrive anche la sicurezza e le autorizzazioni attive per vari tipi di archiviazione. In breve, i punti importanti sono i seguenti:

  • Per impostazione predefinita, i file di dati archiviati nella memoria interna del dispositivo sono privati per l'applicazione e il provider.
  • I database SQLiteDatabase che crei sono privati per la tua applicazione e il tuo provider.
  • Per impostazione predefinita, i file di dati che salvi nella memoria esterna sono pubblici e leggibili in tutto il mondo. Non puoi utilizzare un fornitore di contenuti per limitare l'accesso ai file nell'unità di archiviazione esterna, perché altre applicazioni possono utilizzare altre chiamate API per la lettura e la scrittura.
  • Il metodo richiede l'apertura o la creazione di file o database SQLite nella memoria interna del dispositivo può concedere potenzialmente l'accesso in lettura e scrittura a tutte le altre applicazioni. Se utilizzi un file o un database interno come repository del provider e concedi l'accesso "leggibile in tutto il mondo" o "scrivibile in tutto il mondo", le autorizzazioni che imposti per il provider nel relativo file manifest non proteggono i tuoi dati. L'accesso predefinito per file e database nella memoria interna è "privato"; non modificare questa impostazione per il repository del provider.

Se vuoi utilizzare le autorizzazioni del fornitore di contenuti per controllare l'accesso ai tuoi dati, archiviali in file interni, database SQLite o nel cloud, ad esempio su un server remoto, e mantieni privati file e database per la tua applicazione.

Implementa le autorizzazioni

Per impostazione predefinita, tutte le applicazioni possono leggere o scrivere sul tuo provider, anche se i dati sottostanti sono privati, poiché per impostazione predefinita il tuo provider non ha autorizzazioni impostate. Per modificare questa impostazione, imposta le autorizzazioni per il provider nel file manifest, utilizzando gli attributi o gli elementi secondari dell'elemento <provider>. Puoi impostare autorizzazioni che si applicano all'intero provider, a determinate tabelle, a record specifici o a tutti e tre.

Puoi definire le autorizzazioni per il provider con uno o più elementi <permission> nel file manifest. Per rendere l'autorizzazione univoca per il tuo provider, utilizza l'ambito in stile Java per l'attributo android:name. Ad esempio, assegna all'autorizzazione di lettura il nome com.example.app.provider.permission.READ_PROVIDER.

L'elenco seguente descrive l'ambito delle autorizzazioni del provider, partendo da quelle applicabili all'intero provider per passare poi a una maggiore granularità. Le autorizzazioni più granulari hanno la precedenza su quelle con ambito più ampio.

Singola autorizzazione a livello di provider di lettura-scrittura
Un'autorizzazione che controlla l'accesso in lettura e scrittura all'intero provider, specificato con l'attributo android:permission dell'elemento <provider>.
Autorizzazioni di lettura e scrittura separate a livello di provider
Un'autorizzazione di lettura e un'autorizzazione di scrittura per l'intero provider. Puoi specificare questi valori con gli attributi android:readPermission e android:writePermission dell'elemento <provider>. Hanno la precedenza sull'autorizzazione richiesta da android:permission.
Autorizzazione a livello di percorso
Autorizzazione di lettura, scrittura o lettura/scrittura per un URI dei contenuti nel tuo provider. Devi specificare ogni URI che vuoi controllare con un elemento secondario <path-permission> dell'elemento <provider>. Per ogni URI di contenuto specificato, puoi specificare un'autorizzazione di lettura/scrittura, un'autorizzazione di lettura, un'autorizzazione di scrittura o tutte e tre. Le autorizzazioni di lettura e scrittura hanno la precedenza su quelle di lettura/scrittura. Inoltre, le autorizzazioni a livello di percorso hanno la precedenza sulle autorizzazioni a livello di provider.
Autorizzazione temporanea
Un livello di autorizzazione che concede accesso temporaneo a un'applicazione, anche se quest'ultima non dispone delle autorizzazioni normalmente richieste. La funzionalità di accesso temporaneo riduce il numero di autorizzazioni che un'applicazione deve richiedere nel file manifest. Quando attivi le autorizzazioni temporanee, le uniche applicazioni che richiedono autorizzazioni permanenti per il tuo provider sono quelle che accedono continuamente a tutti i tuoi dati.

Ad esempio, considera le autorizzazioni di cui hai bisogno se implementi un provider email e un'app e vuoi consentire a un'applicazione di visualizzazione di immagini esterna di mostrare gli allegati fotografici del tuo provider. Per concedere al visualizzatore di immagini l'accesso necessario senza richiedere le autorizzazioni, puoi configurare autorizzazioni temporanee per gli URI dei contenuti delle foto.

Progetta la tua app email in modo che, quando l'utente vuole mostrare una foto, l'app invii un intent contenente l'URI dei contenuti della foto e i flag di autorizzazione al visualizzatore di immagini. Il visualizzatore di immagini può quindi chiedere al tuo provider email di recuperare la foto, anche se non dispone della normale autorizzazione di lettura per il provider.

Per attivare le autorizzazioni temporanee, imposta l'attributo android:grantUriPermissions dell'elemento <provider> oppure aggiungi uno o più elementi secondari <grant-uri-permission> all'elemento <provider>. Chiama Context.revokeUriPermission() ogni volta che rimuovi il supporto per un URI dei contenuti associato a un'autorizzazione temporanea del tuo provider.

Il valore dell'attributo determina in che misura il tuo provider viene reso accessibile. Se l'attributo è impostato su "true", il sistema concede l'autorizzazione temporanea all'intero provider, sostituendo eventuali altre autorizzazioni richieste dalle autorizzazioni a livello di provider o percorso.

Se questo flag è impostato su "false", aggiungi elementi secondari <grant-uri-permission> all'elemento <provider>. Ogni elemento secondario specifica l'URI dei contenuti o gli URI per i quali viene concesso l'accesso temporaneo.

Per delegare l'accesso temporaneo a un'applicazione, un intent deve contenere il flag FLAG_GRANT_READ_URI_PERMISSION, il flag FLAG_GRANT_WRITE_URI_PERMISSION o entrambi. Questi valori vengono impostati con il metodo setFlags().

Se l'attributo android:grantUriPermissions non è presente, si presume che sia "false".

L'elemento <provider>

Come per i componenti Activity e Service, una sottoclasse di ContentProvider viene definita nel file manifest per la relativa applicazione, utilizzando l'elemento <provider>. Il sistema Android recupera le seguenti informazioni dall'elemento:

Autorità (android:authorities)
Nomi simbolici che identificano l'intero provider all'interno del sistema. Questo attributo è descritto più dettagliatamente nella sezione Progettare gli URI dei contenuti.
Nome classe provider (android:name)
La classe che implementa ContentProvider. Questa classe è descritta più dettagliatamente nella sezione Implementare la classe ContentProvider.
Autorizzazioni
Attributi che specificano le autorizzazioni che altre applicazioni devono avere per accedere ai dati del provider:

Le autorizzazioni e gli attributi corrispondenti sono descritti in maggiore dettaglio nella sezione Implementare le autorizzazioni del provider di contenuti.

Attributi di avvio e controllo
Questi attributi determinano come e quando il sistema Android avvia il provider, le caratteristiche di processo del provider e altre impostazioni di runtime:
  • android:enabled: flag che consente al sistema di avviare il provider
  • android:exported: flag che consente ad altre applicazioni di utilizzare questo provider
  • android:initOrder: l'ordine in cui il fornitore viene avviato, rispetto ad altri provider nella stessa procedura
  • android:multiProcess: flag che consente al sistema di avviare il provider nello stesso processo del client chiamante
  • android:process: il nome del processo in cui viene eseguito il provider
  • android:syncable: flag che indica che i dati del provider devono essere sincronizzati con i dati su un server

Questi attributi sono descritti in modo esaustivo nella guida all'elemento <provider>.

Attributi informativi
Un'icona ed etichetta facoltative per il fornitore:
  • android:icon: una risorsa disegnabile contenente un'icona per il provider. L'icona viene visualizzata accanto all'etichetta del fornitore nell'elenco delle app in Impostazioni > App > Tutte.
  • android:label: un'etichetta informativa che descrive il fornitore, i suoi dati o entrambi. L'etichetta viene visualizzata nell'elenco delle app in Impostazioni > App > Tutte.

Questi attributi sono descritti in modo esaustivo nella guida all'elemento <provider>.

Intent e accesso ai dati

Le applicazioni possono accedere indirettamente a un fornitore di contenuti con un Intent. L'applicazione non chiama nessuno dei metodi di ContentResolver o ContentProvider. Invia invece un intent che avvia un'attività, che spesso fa parte dell'applicazione del provider. L'attività di destinazione ha il compito di recuperare e visualizzare i dati nella propria UI.

A seconda dell'azione nell'intent, l'attività di destinazione può anche richiedere all'utente di apportare modifiche ai dati del provider. Un intent potrebbe anche contenere dati "aggiuntivi" che l'attività di destinazione mostra nella UI. L'utente ha quindi la possibilità di modificare questi dati prima di utilizzarli per modificare i dati nel provider.

Puoi utilizzare l'accesso per intent per favorire l'integrità dei dati. Il provider potrebbe dipendere dall'inserimento, dall'aggiornamento e dall'eliminazione dei dati in base a una logica di business rigorosamente definita. In questo caso, se consenti ad altre applicazioni di modificare direttamente i tuoi dati, potresti riscontrare dati non validi.

Se vuoi che gli sviluppatori utilizzino l'accesso per intent, assicurati di documentarlo attentamente. Spiega perché l'accesso per intent utilizzando l'interfaccia utente dell'applicazione è meglio che provare a modificare i dati con il loro codice.

La gestione di un intent in entrata che vuole modificare i dati del tuo provider non è diversa dalla gestione di altri intent. Per scoprire di più sull'utilizzo degli intent, consulta la pagina Intent e filtri di intent.

Per ulteriori informazioni correlate, consulta la panoramica del provider di calendario.