Crea un fornitore di contenuti

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

Sebbene i fornitori di contenuti abbiano lo scopo di rendere disponibili i dati ad altre applicazioni, puoi avere attività nell'applicazione che consentono all'utente di eseguire query e modificare i dati gestiti dal tuo 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 della tua applicazione ai widget.
    • Vuoi implementare le classi AbstractThreadedSyncAdapter, CursorAdapter o CursorLoader.

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

  • Se non lo hai già fatto, leggi le nozioni di base per i fornitori di contenuti per scoprire di più sui fornitori e sul loro funzionamento.

A questo punto, 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
    I dati che normalmente vengono inseriti nei file, ad esempio foto, audio o video. Archivia i file nello spazio privato della tua applicazione. In risposta a una 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 tabelle di righe e colonne. Una riga rappresenta un'entità, ad esempio una persona o un elemento nell'inventario. Una colonna rappresenta alcuni dati relativi all'entità, ad esempio 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 nel sistema Android, consulta la sezione Progettazione dell'archiviazione dei dati.
  2. Definisci un'implementazione concreta della classe ContentProvider e i suoi 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 di autorità del provider, gli URI dei contenuti e i nomi delle colonne. Se vuoi che l'applicazione del provider gestisca gli intent, definisci anche le azioni per intent, i dati aggiuntivi e i 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 il corso ad altri sviluppatori. Per ulteriori informazioni sugli URI dei contenuti, consulta la sezione Progettare gli URI dei contenuti. Per maggiori informazioni sugli intent, consulta la sezione Accesso a intent e dati.
  4. Aggiungi altri elementi facoltativi, ad esempio dati di esempio o un'implementazione di AbstractThreadedSyncAdapter in grado di sincronizzare i dati tra il provider e i dati basati su cloud.

Archiviazione dei dati di progettazione

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

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

  • Se lavori con dati strutturati, valuta un database relazionale come SQLite o un datastore di coppie chiave-valore non relazionale come LevelDB. Se utilizzi dati non strutturati come contenuti multimediali audio, immagini o video, valuta la possibilità di archiviarli come file. È possibile 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 del database SQLite che i provider di Android utilizzano per archiviare i dati orientati alle tabelle. Per creare un database utilizzando questa libreria, crea un'istanza per 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, in modo simile a un database relazionale, ma questo non è un requisito per l'implementazione interna del provider.

  • Per l'archiviazione dei dati dei file, Android dispone di una serie di API orientate ai file. Per saperne di più sull'archiviazione di file, leggi la Panoramica dell'archiviazione di dati e file. Se stai progettando un provider che offre dati relativi ai contenuti multimediali, come musica o video, puoi avere un provider che combina dati e file delle tabelle.
  • In rari casi, potrebbe essere utile implementare più di un fornitore 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 per la condivisione con altre applicazioni.
  • Per lavorare con 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, per poi offrirli sotto forma di tabelle o file.

Nota: se apporti una modifica al repository che non è compatibile con le versioni precedenti, devi contrassegnare il repository 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 causino l'arresto anomalo del sistema quando tenta di reinstallare un'app con un fornitore di contenuti non compatibile.

Considerazioni sulla progettazione dei dati

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

  • I dati della tabella devono avere sempre una colonna "chiave primaria" che il provider mantiene come valore numerico univoco per ogni riga. Puoi utilizzare questo valore per collegare la riga alle righe correlate in altre tabelle (utilizzandolo come "chiave esterna"). Anche se per questa colonna puoi utilizzare qualsiasi nome, l'utilizzo di BaseColumns._ID è la scelta migliore, perché per collegare i risultati di una query del provider a un ListView è necessario che una delle colonne recuperate abbia il nome _ID.
  • Se vuoi fornire immagini bitmap o altri elementi molto grandi di dati orientati al file, archivia i dati in un file e poi forniscili indirettamente invece di archiviarli direttamente in una tabella. In questo caso, devi comunicare agli utenti del tuo provider che devono utilizzare un metodo di file ContentResolver per accedere ai dati.
  • Utilizza il tipo di dati BLOB (binario, oggetto di grandi dimensioni) per archiviare dati di dimensioni diverse o che hanno una struttura variabile. 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. Ciò ti consente di archiviare diversi tipi di riga nella stessa tabella. La tabella "dati" del provider di contatti ContactsContract.Data è un esempio di tabella indipendente dallo schema.

Progetta URI contenuti

Un URI dei contenuti è un URI che identifica i dati in un fornitore. Gli URI dei contenuti includono il nome simbolico dell'intero provider (la sua autorità) e un nome che punta a una tabella o a un file (un percorso). La parte ID facoltativa rimanda a una singola riga in una tabella. Ogni metodo di accesso ai dati di ContentProvider ha un URI dei contenuti come argomento. In questo modo puoi determinare la tabella, la riga o il file a cui accedere.

Per informazioni sugli URI dei contenuti, consulta la pagina Nozioni di base sui fornitori di contenuti.

Progetta un'autorità

Di solito un provider ha un'unica autorità, che funge da nome interno ad Android. Per evitare conflitti con altri provider, utilizza la proprietà del dominio internet (al contrario) come base dell'autorità del provider. Poiché questo suggerimento è 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 provider l'autorità com.example.<appname>.provider.

Progettare la struttura di un percorso

In genere, gli sviluppatori creano URI dei contenuti dall'autorità aggiungendo percorsi che puntano 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 deve essere presente una tabella per ogni livello del percorso.

Gestire gli ID URI dei contenuti

Per convenzione, i fornitori 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 ID alla colonna _ID della tabella ed eseguono l'accesso richiesto sulla riga corrispondente.

Questa convenzione agevola un pattern di progettazione comune per le app che accedono a un fornitore. L'app esegue una query sul provider e visualizza il Cursor risultante in un ListView utilizzando un CursorAdapter. La definizione di CursorAdapter richiede che una delle colonne in Cursor sia _ID

L'utente sceglie quindi una delle righe visualizzate nell'interfaccia utente per esaminare o modificare i dati. L'app recupera la riga corrispondente da Cursor che supporta ListView, ottiene 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 scelta dall'utente.

Pattern URI dei 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 URI dei contenuti a valori interi. Puoi utilizzare i valori interi in un'istruzione switch che sceglie l'azione desiderata per l'URI dei contenuti o gli URI che corrispondono a un determinato pattern.

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

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

Come esempio di progettazione e programmazione della gestione degli URI dei contenuti, considera un provider con l'autorità com.example.app.provider che riconosce i seguenti URI dei contenuti che puntano a 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 dei 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 URI dei contenuti:

content://com.example.app.provider/*
Corrisponde a qualsiasi URI di contenuti nel provider.
content://com.example.app.provider/table2/*
Corrisponde a un URI dei contenuti per le tabelle dataset1 e dataset2, ma non 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 tra 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 metodi pratici 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 mediante la gestione delle richieste di 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 richiesti

La classe astratta ContentProvider definisce sei metodi astratti da implementare come parte della tua sottoclasse concreta. Tutti questi metodi, eccetto onCreate(), vengono richiamati da un'applicazione client che tenta 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. Restituisce i dati come oggetto Cursor.
insert()
Inserisci una nuova riga nel provider. Utilizza gli argomenti per selezionare la tabella di destinazione e per ottenere i valori della colonna da utilizzare. Restituisci un URI dei 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()
Restituisci il tipo MIME corrispondente a un URI dei contenuti. Questo metodo è descritto in modo più dettagliato nella sezione Implementare i tipi MIME del fornitore di contenuti.
onCreate()
Inizializza il tuo provider. Il sistema Android chiama questo metodo subito dopo la creazione del provider. Il provider viene creato solo dopo che un oggetto ContentResolver tenta di accedervi.

Questi metodi hanno la stessa firma dei metodi ContentResolver con nomi identici.

L'implementazione di questi metodi deve tenere conto dei seguenti fattori:

  • Tutti questi metodi, eccetto onCreate(), possono essere chiamati da più thread contemporaneamente, quindi devono essere sicuri per i thread. Per scoprire di più su più thread, consulta la panoramica su processi e thread.
  • Evita di eseguire operazioni lunghe in onCreate(). Rimanda le attività di inizializzazione finché non sono effettivamente necessarie. Questo argomento viene discusso in maggiore dettaglio nella sezione relativa all'implementazione del metodo onCreate().
  • Sebbene sia necessario implementare questi metodi, il codice non deve fare nulla se non 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 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. Restituisci 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, ti conviene 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 crearlo, 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 dallo spazio di 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ò verificare la presenza di righe eliminate e rimuoverle dal server prima di eliminarle dal provider.

Implementare il metodo update()

Il metodo update() prende lo stesso argomento ContentValues utilizzato da insert() e gli stessi argomenti selection e selectionArgs utilizzati 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 con questo metodo e posticipa la creazione del database e il caricamento dei dati finché il provider non riceve effettivamente una richiesta per i dati. Se esegui attività lunghe in onCreate(), l'avvio del provider rallenta. A sua volta, questo rallenta la risposta dal provider ad altre applicazioni.

I due snippet seguenti dimostrano 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 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, restituisce il tipo di dati associati agli URI dei contenuti che corrispondono al pattern.

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

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

  • Digita il pezzo: 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/
  • Componente specifica del fornitore: vnd.<name>.<type>

    Tu fornisci <name> e <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 dell'applicazione. Un'ottima scelta per <type> è una stringa che identifica la tabella associata all'URI.

Ad esempio, se l'autorità di un provider è com.example.app.provider e mostra 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 contenuto. Filtra i tipi MIME offerti in base all'argomento 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 un elemento che è "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, mentre getStreamTypes() restituisce:

{"image/jpeg"}

Se il tuo provider non offre nessuno dei tipi MIME richiesti nella stringa del 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 relativi al provider. La classe stabilisce un contratto tra il provider e altre applicazioni garantendo che il provider sia accessibile correttamente anche in caso di modifiche ai valori effettivi di URI, nomi di colonna e così via.

Una classe contratto è utile anche agli sviluppatori perché di solito include nomi mnemonici per le costanti, quindi è 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 dalla classe del contratto e visualizzare Javadoc per le costanti.

Gli sviluppatori non possono accedere al file della classe del contratto dalla tua 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 contratto.

Implementazione delle 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 dell'archiviazione di dati e file descrive inoltre la sicurezza e le autorizzazioni in vigore per i 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.
  • SQLiteDatabase database che crei sono privati per la tua applicazione e il tuo provider.
  • Per impostazione predefinita, i file di dati salvati nella memoria esterna sono pubblici e leggibili in tutto il mondo. Non puoi utilizzare un fornitore di contenuti per limitare l'accesso ai file nello spazio di archiviazione esterno, perché altre applicazioni possono utilizzare altre chiamate API per leggerli e scriverli.
  • Le chiamate al metodo per l'apertura o la creazione di file o database SQLite nella memoria interna del dispositivo possono potenzialmente concedere l'accesso in lettura e scrittura a tutte le altre applicazioni. Se utilizzi un file o un database interno come repository del provider e gli concedi l'accesso "leggibile in tutto il mondo" o "scrivibile a livello mondiale", le autorizzazioni che imposti per il provider nel relativo file manifest non proteggono i tuoi dati. L'accesso predefinito per i file e i 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 dati, archiviali in file interni, database SQLite o nel cloud, ad esempio su un server remoto, e mantieni privati i file e i database per la tua applicazione.

Implementare le autorizzazioni

Per impostazione predefinita, tutte le applicazioni possono leggere o scrivere sul provider, anche se i dati sottostanti sono privati, perché per impostazione predefinita il provider non ha autorizzazioni impostate. Per cambiare 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 determinati record 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 provider, utilizza la definizione dell'ambito in stile Java per l'attributo android:name. Ad esempio, assegna il nome com.example.app.provider.permission.READ_PROVIDER dell'autorizzazione di lettura.

Il seguente elenco descrive l'ambito delle autorizzazioni del provider, partendo da quelle che si applicano all'intero provider per poi diventare più granulare. 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 in scrittura all'intero provider, specificata con l'attributo android:permission dell'elemento <provider>.
Autorizzazioni separate a livello di provider per lettura e scrittura
Un'autorizzazione di lettura e un'autorizzazione di scrittura per l'intero provider. Le specifichi 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 dei contenuti specificato, puoi specificare un'autorizzazione di lettura/scrittura, un'autorizzazione di lettura, un'autorizzazione di scrittura o tutte e tre le autorizzazioni. Le autorizzazioni di lettura e scrittura hanno la precedenza sull'autorizzazione di lettura/scrittura. Inoltre, l'autorizzazione a livello di percorso ha la precedenza su quelle a livello di provider.
Autorizzazione temporanea
Un livello di autorizzazione che concede accesso temporaneo a un'applicazione, anche se l'applicazione 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 provider sono quelle che accedono continuamente a tutti i tuoi dati.

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

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

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

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

Se questo flag è impostato su "false", aggiungi elementi secondari <grant-uri-permission> all'elemento <provider>. Ogni elemento figlio specifica l'URI o gli URI dei contenuti per cui 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, nel file manifest della relativa applicazione viene definita una sottoclasse di ContentProvider, utilizzando l'elemento <provider>. Il sistema Android riceve 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 in modo più dettagliato nella sezione Implementare la classe ContentProvider.
Autorizzazioni
Attributi che specificano le autorizzazioni necessarie per le altre applicazioni per accedere ai dati del provider:

Le autorizzazioni e gli attributi corrispondenti sono descritti in maggiore dettaglio nella sezione Implementare le autorizzazioni del fornitore 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 viene avviato questo provider, rispetto ad altri provider nella stessa procedura
  • android:multiProcess: flag che consente al sistema di avviare il provider nella stessa procedura 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 completamente documentati nella guida all'elemento <provider>.

Attributi informativi
Un'icona ed etichetta facoltative per il provider:
  • android:icon: una risorsa drawable contenente un'icona per il provider. L'icona viene visualizzata accanto all'etichetta del provider nell'elenco di 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 completamente documentati nella guida all'elemento <provider>.

Nota: se scegli come target Android 11 o versioni successive, consulta la documentazione sulla visibilità dei pacchetti per ulteriori esigenze di configurazione.

Accesso a intent e 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à della destinazione si occupa del recupero e della visualizzazione dei dati nella sua 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 "extra" visualizzati dall'attività della destinazione 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 migliorare 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, consentire ad altre applicazioni di modificare direttamente i tuoi dati può causare dati non validi.

Se vuoi che gli sviluppatori utilizzino l'accesso per intent, assicurati di documentarlo accuratamente. Spiega perché l'accesso per intent utilizzando l'interfaccia utente della tua applicazione è migliore che provare a modificare i dati con il 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, leggi la pagina Intent e filtri per intent.

Per ulteriori informazioni correlate, consulta la panoramica del fornitore di Calendar.