Criação de um provedor de conteúdo

O provedor de conteúdo gerencia o acesso a um repositório central de dados. Implementa-se um provedor como uma ou mais classes em um aplicativo do Android, em conjunto com elementos no arquivo de manifesto. Uma das classes implementa uma subclasse ContentProvider, que é a interface entre o provedor e outros aplicativos. Embora provedores de conteúdo se destinem a disponibilizar dados a outros aplicativos, naturalmente é possível ter atividades no aplicativo que permitam ao usuário consultar e modificar os dados gerenciados pelo provedor.

O restante deste tópico é uma lista básica de etapas para a criação de um provedor de conteúdo e uma lista de APIs para usar.

Antes de começar a criar

Antes de começar a criar um provedor, considere o seguinte:

  1. Avalie se você precisa de um provedor de conteúdo. É necessário criar um provedor de conteúdo para fornecer um ou mais dos recursos a seguir:
    • Oferecer dados ou arquivos complexos a outros aplicativos.
    • Permitir que usuários copiem dados complexos do seu aplicativo para outros aplicativos.
    • Fornecer sugestões de pesquisa personalizada usando a biblioteca de pesquisa.
    • Expor os dados do aplicativo a widgets.
    • Implementar as classes AbstractThreadedSyncAdapter, CursorAdapter ou CursorLoader.

    Provedores não são necessários para usar bancos de dados ou outros tipos de armazenamento persistente, caso o uso seja inteiramente dentro do aplicativo e você não precise de nenhum dos recursos listados acima. Em vez disso, é possível usar um dos sistemas de armazenamento descritos na página Como salvar dados do aplicativo.

  2. Se você ainda não tiver feito isso, leia o tópico Fundamentos do provedor de conteúdo para saber mais sobre provedores.

A seguir, siga estas etapas para criar seu provedor:

  1. Projete o armazenamento bruto dos dados. Os provedores de conteúdo oferecem dados de duas maneiras:
    Dados de arquivo
    Dados que normalmente transitam em arquivos, como fotos, áudio ou vídeos. Armazene os arquivos no espaço privado do seu aplicativo. Em resposta a uma solicitação de um arquivo por outro aplicativo, o provedor pode oferecer um identificador para o arquivo.
    Dados “estruturados”
    Dados que normalmente transitam em um banco de dados, uma matriz ou estrutura similar. Armazene os dados de forma compatível com tabelas de linhas e colunas. Cada linha representa uma entidade, como uma pessoa ou um item em um inventário. Cada coluna representa alguns dados da entidade, como o nome da pessoa ou o preço do item. Um modo comum de armazenar esse tipo de dados é em um banco de dados SQLite, mas é possível usar qualquer tipo de armazenamento persistente. Para saber mais sobre os tipos de armazenamento disponíveis no sistema Android, consulte a seção Projeto de armazenamento de dados.
  2. Defina uma implementação sólida da classe ContentProvider e seus métodos obrigatórios. Essa classe é a interface entre seus dados e o restante do sistema Android. Para mais informações sobre essa classe, consulte a seção Implementação da classe ContentProvider.
  3. Defina a string de autoridade do provedor, os URIs de conteúdo e os nomes de coluna. Se você quiser que o aplicativo do provedor processe intents, defina também ações de intent, dados extras e sinalizadores. Também defina as permissões necessárias para aplicativos que queiram acessar seus dados. É importante considerar definir todos esses valores como constantes em uma classe de contrato separada. Posteriormente, será possível expor essa classe a outros desenvolvedores. Para mais informações sobre URIs de conteúdo, consulte a seção Projeto de URIs de conteúdo. Para mais informações sobre intents, consulte a seção Intents e acesso a dados.
  4. Adicione outras partes opcionais, como dados de exemplo ou uma implementação de AbstractThreadedSyncAdapter que possa sincronizar dados entre o provedor e os dados com base em nuvem.

Projeto de armazenamento de dados

Os provedores de conteúdo são a interface para dados salvos em um formato estruturado. Antes de criar a interface, é preciso decidir como armazenar os dados. É possível armazená-los como quiser e, em seguida, projetar a interface para ler e gravar os dados conforme necessário.

Estas são algumas das tecnologias de armazenamento de dados disponíveis no Android:

  • Se estiver trabalhando com dados estruturados, considere um banco de dados relacional como o SQLite ou um armazenamento de dados de chave-valor não relacional como o LevelDB. Caso esteja trabalhando com dados não estruturados, como áudio, imagem ou vídeo, considere armazenar os dados como arquivos. Você pode misturar e corresponder vários tipos de armazenamento e expô-los usando um provedor de conteúdo único, se for necessário.
  • O sistema Android pode interagir com a biblioteca de persistência Room, que fornece acesso à API do banco de dados SQLite, usada pelos próprios provedores do Android para armazenar dados orientados por tabela. Para criar um banco de dados usando essa biblioteca, instancie uma subclasse da RoomDatabase, conforme descrito no guia da biblioteca de persistência Room.

    Lembre-se de que não é necessário usar nenhum banco de dados para implementar o repositório. Um provedor aparece externamente como um conjunto de tabelas, semelhante a um banco de dados relacional, mas isso não é um requisito para a implementação interna do provedor.

  • Para armazenar dados de arquivos, o Android tem diversas APIs orientadas a arquivo. Para saber mais sobre armazenamento de arquivos, leia o tópico Armazenamento de dados. Se você estiver projetando um provedor que oferece dados relacionados a mídia como música ou vídeos, é possível ter um provedor que combine dados de tabela e de arquivos.
  • Em casos raros, você pode se beneficiar da implementação de mais de um provedor de conteúdo para um aplicativo único. Por exemplo, você pode precisar compartilhar dados com um widget usando um provedor de conteúdo e expor um conjunto de dados diferente para compartilhamento com outros aplicativos.
  • Para trabalhar com dados em rede, use classes em java.net e em android.net. É possível, também, sincronizar dados em rede com um armazenamento local de dados, como um banco de dados, e, em seguida, fornecer os dados na forma de tabelas ou arquivos. O exemplo de aplicativo Exemplo de adaptador de sincronização demonstra o tipo de sincronização.

Observação: se você fizer uma alteração ao repositório que não seja compatível retroativamente, será necessário marcar o repositório com um número de versão novo. Também é necessário aumentar o número da versão do aplicativo que implementa o novo provedor de conteúdo. Essa alteração evita que downgrades do sistema causem falhas quando há tentativas de reinstalação de um aplicativo que tenha um provedor de conteúdo incompatível.

Considerações para projetar dados

Veja algumas dicas para projetar a estrutura de dados do seu provedor:

  • Os dados de tabela sempre precisam ter uma coluna de “chave principal” que o provedor mantém como um valor numérico exclusivo para cada linha. É possível usar esse valor para vincular a linha a linhas relacionadas em outras tabelas (usando-o como uma “chave externa”). Embora seja possível usar qualquer nome para essa coluna, é melhor usar BaseColumns._ID, porque a vinculação dos resultados de uma consulta de provedor com uma ListView requer que uma das colunas tenha o nome _ID.
  • Se você quiser fornecer imagens bitmap ou outras partes muito grandes de dados orientados a arquivo, armazene os dados em um arquivo e, em seguida, forneça-o indiretamente em vez de armazená-lo diretamente em uma tabela. Ao fazer isso, será necessário informar aos usuários do provedor que eles precisam usar um método de arquivo ContentResolver para acessar os dados.
  • Use objeto grande binário (BLOB) como tipo de dados para armazenar dados que variam em tamanho ou que tenham estrutura variável. Por exemplo, é possível usar uma coluna de BLOB para armazenar um buffer de protocolo ou uma estrutura JSON.

    Pode-se usar um BLOB para implementar uma tabela independente de esquema. Nesse tipo de tabela, define-se uma coluna de chave principal, uma coluna de tipo MIME e uma ou mais colunas genéricas como BLOB. O significado dos dados nas colunas BLOB é indicado pelo valor na coluna de tipo MIME. Isso permite o armazenamento de diferentes tipos de linha na mesma tabela. A tabela de “dados” ContactsContract.Data do Provedor de contatos é um exemplo de uma tabela independente de esquema.

Projeto de URIs de conteúdo

URI de conteúdo é um URI que identifica dados em um provedor. URIs de conteúdo contêm o nome simbólico de todo o provedor (a autoridade) e um nome que aponta para uma tabela ou arquivo (um caminho). Parte do código opcional aponta para uma linha individual em uma tabela. Cada método de acesso aos dados ContentProvider tem um URI de conteúdo como um argumento. Isso permite determinar a tabela, a linha ou o arquivo a acessar.

Os conceitos básicos de URIs de conteúdo são descritos no tópico Fundamentos do provedor de conteúdo.

Projeto de uma autoridade

Os provedores normalmente têm uma única autoridade, que serve como seu nome interno do Android. Para evitar conflitos com outros provedores, use a propriedade de domínio da Internet (ao contrário) como a base da autoridade do provedor. Em virtude de essa recomendação ser verdadeira para nomes de pacote do Android, é possível definir a autoridade do provedor como uma extensão do nome do pacote que contém o provedor. Por exemplo, se o nome do pacote do Android for com.example.<appname>, é necessário atribuir ao provedor a autoridade com.example.<appname>.provider.

Projeto de uma estrutura de caminho

Desenvolvedores normalmente criam URIs de conteúdo a partir da autoridade anexando caminhos que apontam para tabelas individuais. Por exemplo, se você tiver duas tabelas (table1 e table2), combine a autoridade do exemplo anterior para produzir os URIs de conteúdo com.example.<appname>.provider/table1 e com.example.<appname>.provider/table2. Caminhos não se limitam a um único segmento e não precisam ter uma tabela para cada nível do caminho.

Identificação de códigos de URIs de conteúdo

Por convenção, provedores fornecem acesso a uma linha exclusiva em uma tabela aceitando um URI de conteúdo com um valor de código para a linha no fim do URI. Também por convenção, provedores combinam o valor do código com a coluna _ID da tabela e realizam o acesso solicitado na linha correspondente.

Essa convenção facilita um padrão comum de projeto para aplicativos que acessam um provedor. O aplicativo realiza uma consulta no provedor e exibe o Cursor resultante em uma ListView usando um CursorAdapter. A definição de CursorAdapter requer que uma das colunas no Cursor seja _ID

Em seguida, o usuário seleciona uma das linhas exibidas na IU para visualizar ou modificar os dados. O aplicativo tem a linha correspondente do Cursor que apoia a ListView, recebe o valor _ID para essa linha, anexa-o ao URI de conteúdo e envia a solicitação de acesso ao provedor. O provedor, então, pode realizar a consulta ou modificação na exata linha que o usuário selecionou.

Padrões de URI de conteúdo

Para ajudar a escolher que ação tomar para um URI de conteúdo recebido, a API do provedor contém a classe de conveniência UriMatcher, que mapeia “padrões” de URI de conteúdo como valores de número inteiro. É possível usar os valores de número inteiro em uma declaração switch que escolha a ação desejada para o URI de conteúdo ou URIs que atendam ao padrão determinado.

Um padrão de URI de conteúdo corresponde a URIs de conteúdo que usam caracteres curinga:

  • *: Corresponde a uma string de qualquer caractere válido de qualquer comprimento.
  • #: corresponde a uma string de caracteres numéricos de qualquer comprimento.

Como um exemplo de projeto e codificação da identificação de URIs de conteúdo, considere um provedor com a autoridade com.example.app.provider que reconheça os URIs de conteúdo que apontam para tabelas:

  • content://com.example.app.provider/table1: Uma tabela chamada table1.
  • content://com.example.app.provider/table2/dataset1: é uma tabela chamada dataset1.
  • content://com.example.app.provider/table2/dataset2: é uma tabela chamada dataset2.
  • content://com.example.app.provider/table3: é uma tabela chamada table3.

O provedor também reconhecerá esses URIs de conteúdo se eles tiverem um código de linha anexado, como content://com.example.app.provider/table3/1 para a linha identificada por 1 em table3.

Os seguintes padrões de URI de conteúdo seriam possíveis:

content://com.example.app.provider/*
Corresponde a qualquer URI de conteúdo no provedor.
content://com.example.app.provider/table2/*:
corresponde a um URI de conteúdo das tabelas dataset1 e dataset2, mas não a URIs de conteúdo de table1 nem table3.
content://com.example.app.provider/table3/#: corresponde a um URI de conteúdo para linhas exclusivas em table3, como content://com.example.app.provider/table3/6 da linha identificada por 6.

O snippet de código a seguir mostra como funcionam os métodos em UriMatcher. Esse código identifica URIs para uma tabela inteira, diferentemente dos URIs para uma linha exclusiva, usando o padrão de URI de conteúdo content://<authority>/<path> para tabelas e content://<authority>/<path>/<id> para linhas exclusivas.

O método addURI() mapeia uma autoridade e um caminho como um valor de número inteiro. O método match() retorna o valor inteiro de um URI. Uma declaração switch escolhe entre consultar a tabela inteira e consultar um único registro:

Kotlin

private val sUriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
    /*
     * The calls to addURI() go here, for all of the content URI patterns that the provider
     * should recognize. 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 is not recognized
                // You should 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 of the content URI patterns that the provider
         * should recognize. 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
         */
        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 is not recognized, you should do some error handling here.
        }
        // call the code to actually do the query
    }

Outra classe, ContentUris, fornece métodos convenientes para trabalhar com a parte id dos URIs de conteúdo. As classes Uri e Uri.Builder contêm métodos convenientes para analisar objetos Uri existentes e criar novos.

Implementação da classe ContentProvider

A instância ContentProvider gerencia o acesso a um conjunto de dados estruturado lidando com solicitações de outros aplicativos. Todas as formas de acesso ocasionalmente chamam ContentResolver que, em seguida, chama um método concreto de ContentProvider para ter acesso.

Métodos obrigatórios

A classe abstrata ContentProvider define seis métodos abstratos que precisam ser implementados como parte das subclasses concretas. Todos esses métodos, exceto onCreate(), são chamados por um aplicativo cliente que está tentando acessar seu provedor de conteúdo:

query()
Recupere dados do provedor. Use os argumentos para selecionar a tabela para consultar as linhas e colunas a retornar e a ordem de classificação do resultado. Retorne os dados como um objeto Cursor.
insert()
Insira uma nova linha no provedor. Use os argumentos para selecionar a tabela de destino e receber os valores de coluna a usar. Retorne um URI de conteúdo para a linha recentemente inserida.
update()
Atualize as linhas existentes no provedor. Use os argumentos para selecionar a tabela e linhas para atualizar e receber os valores de coluna atualizados. Retorne o número de linhas atualizadas.
delete()
Exclua linhas do provedor. Use os argumentos para selecionar a tabela e as linhas a excluir. Retorne o número de linhas excluídas.
getType()
Retorne o tipo MIME correspondente a um URI de conteúdo. Esse método é descrito com mais detalhes na seção Implementação de tipos MIME do Provedor de conteúdo.
onCreate()
Inicialize o provedor. O sistema Android chama esse método imediatamente após criar o provedor. O provedor não é criado até que um objeto ContentResolver tente acessá-lo.

Esses métodos têm a mesma assinatura dos métodos ContentResolver de mesmo nome.

A implementação desses métodos levará em conta o seguinte:

  • Todos esses métodos, exceto onCreate(), podem ser chamados por diversos encadeamentos simultaneamente, portanto, precisam ter encadeamento seguro. Para saber mais sobre vários threads, consulte o tópico Processos e threads.
  • Evite realizar longas ações em onCreate() Adie tarefas de inicialização até que sejam realmente necessárias. A seção Implementação do método onCreate() aborda o assunto mais detalhadamente.
  • Embora seja preciso implementar esses métodos, o código não precisa fazer nada além de retornar o tipo de dados esperado. Por exemplo, pode ser necessário evitar que outros aplicativos insiram dados em algumas tabelas. Para fazer isso, você pode ignorar a chamada de insert() e retornar 0.

Implementação do método query()

O método ContentProvider.query() precisa retornar um objeto Cursor ou, caso haja falha, lançar uma Exception. Se você estiver usando um banco de dados SQLite como armazenamento de dados, pode simplesmente retornar o Cursor retornado por um dos métodos query() da classe SQLiteDatabase. Se a consulta não corresponder a nenhuma linha, será necessário retornar uma instância de Cursor cujo método getCount() retorne 0. Retorne null somente se ocorrer um erro interno durante o processo de consulta.

Se você não estiver usando um banco de dados SQLite como modo de armazenamento de dados, use uma das subclasses concretas de Cursor. Por exemplo, a classe MatrixCursor implementa um cursor em que cada linha é uma matriz de Object. Com essa classe, use addRow() para adicionar uma nova linha.

Lembre-se de que o sistema Android precisa ser capaz de se comunicar com Exception entre limites de processo. O Android pode fazer isso para as exceções a seguir, que podem ser úteis para processar erros de consulta:

Implementação do método insert()

O método insert() adiciona uma nova linha à tabela apropriada usando os valores no argumento ContentValues. Se um nome de coluna não estiver no argumento ContentValues, talvez seja necessário fornecer um valor padrão para ele tanto no código do provedor quanto no esquema do banco de dados.

Esse método retornará o URI de conteúdo da nova linha. Para construir isso, anexe o valor _ID da nova linha (ou outra chave principal) ao URI de conteúdo da tabela usando withAppendedId().

Implementação do método delete()

O método delete() não precisa excluir linhas fisicamente do armazenamento de dados. Se você estiver usando um adaptador de sincronização com o provedor, considere marcar uma linha excluída com um sinalizador “excluir” em vez de removê-la totalmente. O adaptador de sincronização pode verificar se há linhas excluídas e removê-las do servidor antes de excluí-las do provedor.

Implementação do método update()

O método update() leva o mesmo argumento ContentValues usado por insert() e os mesmos argumentos selection e selectionArgs usados por delete() e ContentProvider.query(). Isso permitirá a reutilização do código entre esses métodos.

Implementação do método onCreate()

O sistema Android chama onCreate() quando inicia o provedor. Nesse método, será necessário realizar somente tarefas de inicialização de execução rápida e adiar a criação do banco de dados e o carregamento dos dados até que o provedor receba efetivamente uma solicitação de acesso aos dados. Se houver tarefas longas em onCreate(), a inicialização do provedor ficará lenta. Consequentemente, isso diminuirá a rapidez da resposta do provedor a outros aplicativos.

Os dois snippets a seguir demonstram a interação entre ContentProvider.onCreate() e Room.databaseBuilder(). Esse snippet mostra a implementação de ContentProvider.onCreate() em que o objeto do banco de dados e identificadores dos objetos de acesso aos dados são criados.

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.
    }
}

Implementação de tipos MIME de ContentProvider

A classe ContentProvider tem dois métodos para retornar tipos MIME:

getType()
Um dos métodos obrigatórios que precisam ser implementados em qualquer provedor.
getStreamTypes()
Um método que precisará ser implementado se o provedor fornecer arquivos.

Tipos MIME para tabelas

O método getType() retorna uma String em formato MIME que descreve o tipo de dados retornado pelo argumento do URI de conteúdo. O argumento Uri pode ser um padrão em vez de um URI específico. Nesse caso, precisa retornar o tipo de dados associado a URIs de conteúdo que correspondam ao padrão.

Para tipos de dados comuns como texto, HTML ou JPEG, getType() precisa retornar o tipo MIME padrão daqueles dados. Há uma lista completa desse tipos de padrão no site de Tipos de mídia MIME IANA.

Para ter URIs de conteúdo que apontam para uma linha ou linhas de dados de tabela, getType() precisa retornar um tipo MIME no formato MIME específico do fornecedor do Android:

  • Parte do tipo: vnd
  • Parte do subtipo:
    • Se um padrão de URI se destinar a uma única linha: android.cursor.item/
    • Se um padrão de URI se destinar a mais de uma linha: android.cursor.dir/
  • Parte específica do provedor: vnd.<name>.<type>

    Você fornece o <name> e o <type>. O valor <name> precisa ser globalmente exclusivo e <type>, exclusivo para o padrão de URI correspondente. Uma boa escolha para <name> é o nome da empresa ou alguma parte do nome do pacote do Android do seu aplicativo. Uma boa escolha para <type> é uma string que identifique a tabela associada ao URI.

Por exemplo, se a autoridade de um provedor for com.example.app.provider e ele expuser uma tabela chamada table1, o tipo MIME de diversas linhas em table1 será:

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

Para uma única linha de table1, o tipo MIME será:

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

Tipos MIME para arquivos

Se o provedor oferecer arquivos, implemente getStreamTypes(). O método retorna uma matriz String de tipos MIME para os arquivos que o provedor pode retornar de um URI de conteúdo. É preciso filtrar os tipos MIME oferecidos pelo argumento do filtro de tipo MIME para retornar somente os tipos MIME que o cliente quer processar.

Por exemplo, considere um provedor que ofereça imagens de foto como arquivos em formatos .jpg, .png e .gif. Se um aplicativo chama ContentResolver.getStreamTypes() com a string de filtro image/* (algo que seja uma “imagem”), o método ContentProvider.getStreamTypes() retornará a matriz:

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

Se o aplicativo estiver interessado somente em arquivos .jpg, ele pode chamar ContentResolver.getStreamTypes() com a string de filtro *\/jpeg, e ContentProvider.getStreamTypes() retornará:

{"image/jpeg"}

Se o provedor não oferecer nenhum tipo MIME solicitado na string de filtro, getStreamTypes() retornará null.

Implementação de uma classe de contrato

Uma classe de contrato é uma classe public final que contém definições de constantes para os nomes de coluna dos URIs, dos tipos MIME e de outros metadados que pertencem ao provedor. A classe estabelece um contrato entre o provedor e outros aplicativos, garantindo que o provedor possa ser corretamente acessado mesmo se houver mudanças nos valores atuais de URIs, nomes de coluna etc.

As classes de contrato também ajudam os desenvolvedores porque normalmente suas constantes têm nomes mnemônicos e, por isso, os desenvolvedores têm menos chance de usar valores incorretos para nomes de coluna ou URIs. Já que é uma classe, ela pode conter documentação Javadoc. Ambientes de desenvolvimento integrados, como o Android Studio, podem preencher automaticamente o nome de constantes da classe de contrato e exibir Javadoc para elas.

Os desenvolvedores não podem acessar o arquivo de classe da classe de contrato do aplicativo, mas podem compilá-lo estaticamente no aplicativo a partir de um arquivo .jar que você fornece.

A classe ContactsContract e classes aninhadas são exemplos de classes de contrato.

Implementação de permissões do provedor de conteúdo

Permissões e acesso, para todos os aspectos do sistema Android, são descritos detalhadamente no tópico Permissões e segurança. O tópico Armazenamento de dados também descreve segurança e permissões em vigor para diversos tipos de armazenamento. Em resumo, os pontos importantes são os seguintes:

  • Por padrão, arquivos de dados armazenados no armazenamento interno do dispositivo são privados em relação ao aplicativo e ao provedor.
  • Os bancos de dados SQLiteDatabase criados são privados em relação ao aplicativo e ao provedor.
  • Por padrão, arquivos de dados salvos em armazenamentos externos são públicos e legíveis para todos. Não é possível usar um provedor de conteúdo para restringir o acesso a arquivos em um armazenamento externo porque outros aplicativos podem usar outras chamadas de API para lê-los e gravar neles.
  • O método que chama a abertura ou criação de arquivos ou bancos de dados SQLite no armazenamento interno do seu dispositivo pode fornecer acesso de leitura e gravação a todos os outros aplicativos. Se você usar um arquivo ou banco de dados interno como o repositório do provedor e conceder-lhe acesso “legível para todos” ou “gravável por todos”, as permissões definidas para o provedor no manifesto não protegerão os dados. O acesso padrão de arquivos e bancos de dados no armazenamento interno é “privado” e, para o repositório do provedor, não é recomendável alterar isso.

Se você quiser usar permissões do provedor de conteúdo para controlar o acesso aos dados, precisa armazená-los em arquivos internos, bancos de dados SQLite ou em “nuvem” (por exemplo, em um servidor remoto) e mantê-los privados em relação ao aplicativo.

Implementação de permissões

Todos os aplicativos podem ler ou gravar no provedor, mesmo que os dados em questão sejam privados, porque, por padrão, o provedor não tem permissões definidas. Para mudar isso, defina permissões do provedor no arquivo de manifesto por meio de atributos ou elementos filhos do elemento <provider>. É possível definir permissões que se apliquem a todo o provedor, a determinadas tabelas, a registros específicos ou aos três.

As permissões são definidas para o provedor com um ou mais elementos <permission> no arquivo de manifesto. Para tornar a permissão exclusiva para o provedor, use escopo de estilo Java para o atributo android:name. Por exemplo, nomeie a permissão de leitura com.example.app.provider.permission.READ_PROVIDER.

A lista a seguir descreve o escopo de permissões do provedor, começando com as permissões que se aplicam a todo o provedor e seguindo para as mais específicas. Permissões mais específicas têm precedência sobre as de escopo maior:

Única permissão de leitura e gravação no nível do provedor
É uma permissão que controla os acessos de leitura e gravação a todo o provedor, especificada com o atributo android:permission do elemento <provider>.
Permissões de leitura e gravação separadas no nível do provedor
É uma permissão de leitura e uma permissão de gravação para todo o provedor. São especificadas com os atributos android:readPermission e android:writePermission do elemento <provider>. Elas têm precedência sobre a permissão exigida por android:permission.
Permissão no nível do caminho
É uma permissão de leitura, gravação ou leitura/gravação para um URI de conteúdo no provedor. Especifica-se cada URI que se quer controlar com um elemento filho <path-permission> do elemento <provider>. Para cada URI de conteúdo, pode-se especificar uma permissão de leitura/gravação, uma permissão de leitura, uma permissão de gravação ou as três. As permissões de leitura e gravação têm precedência sobre a permissão de leitura/gravação. Além disso, permissões no nível do caminho têm precedência sobre permissões no nível do provedor.
Permissão temporária
é um nível de permissão que concede acesso temporário a um aplicativo, mesmo que ele não tenha as permissões normalmente exigidas. O recurso de acesso temporário reduz o número de permissões que um aplicativo precisa solicitar no manifesto. Ao ativar permissões temporárias, os únicos aplicativos que precisam de permissões “permanentes” para seu provedor são os que têm acesso contínuo a todos os dados.

Considere as permissões necessárias para implementar um provedor e aplicativo de e-mail ao permitir que um aplicativo visualizador de imagens externas exiba anexos de fotos do provedor. Para conceder o acesso necessário ao visualizador de imagens sem as permissões exigidas, configure permissões temporárias dos URIs de conteúdo de fotos. Projete o app de e-mails para que, quando o usuário quiser exibir uma foto, o aplicativo envie um intent com o URI de conteúdo da foto e sinalizadores de permissão para o visualizador de imagens. O visualizador de imagens poderá, então, consultar o provedor de e-mail para recuperar a foto, mesmo que ele não tenha a permissão normal de leitura para o provedor.

Para ativar permissões temporárias, defina o atributo android:grantUriPermissions do elemento <provider> ou adicione um ou mais elementos filhos <grant-uri-permission> ao elemento <provider>. Se forem usadas permissões temporárias, será necessário chamar Context.revokeUriPermission() sempre que você remover o suporte a URI de conteúdo do provedor e o URI de conteúdo for associado a uma permissão temporária.

O valor do atributo determina o nível de acessibilidade do provedor. Se o atributo estiver definido como true, o sistema concederá permissão temporária a todo o provedor, modificando todas as outras permissões exigidas pelas permissões no nível do provedor ou no nível do caminho.

Se esse sinalizador estiver definido como false, será necessário adicionar elementos filhos <grant-uri-permission> ao elemento <provider>. Cada elemento filho especifica o URI ou URIs de conteúdo para as quais o acesso temporário é concedido.

Para delegar o acesso temporário a um aplicativo, o intent precisa conter os sinalizadores FLAG_GRANT_READ_URI_PERMISSION, FLAG_GRANT_WRITE_URI_PERMISSION ou ambos. Eles são definidos com o método setFlags().

Se o atributo android:grantUriPermissions não estiver presente, presume-se que ele seja false.

O elemento <provider>

Como os componentes Activity e Service, a subclasse de ContentProvider precisa ser definida no arquivo de manifesto do aplicativo pelo elemento <provider>. O sistema Android recebe as seguintes informações do elemento:

Autoridade (android:authorities)
São nomes simbólicos que identificam todo o provedor dentro do sistema. Esse atributo é descrito com mais detalhes na seção Projeto de URIs de conteúdo.
Nome da classe do provedor ( android:name )
É a classe que implementa ContentProvider. Essa classe é abordada com mais detalhes na seção Implementação da classe ContentProvider.
Permissões
São atributos que especificam as permissões que outros aplicativos precisam ter para acessar os dados do provedor:

As permissões e os atributos correspondentes são abordados com mais detalhes na seção Implementação de permissões do Provedor de conteúdo.

Atributos de início e controle
Esses atributos determinam como e quando o sistema Android inicia o provedor, as características do processo do provedor e outras configurações de tempo de execução:
  • android:enabled: Sinalizador que permite ao sistema iniciar o provedor.
  • android:exported: Sinalizador que permite a outros aplicativos usarem esse provedor.
  • android:initOrder: é a ordem em que esse provedor precisa ser iniciado, relativa a outros provedores no mesmo processo.
  • android:multiProcess: é um sinalizador que permite ao sistema iniciar o provedor no mesmo processo que o cliente chama.
  • android:process: é o nome do processo em que o provedor será executado.
  • android:syncable: é um sinalizador que indica que os dados do provedor precisam ser sincronizados com os dados em um servidor.

Os atributos estão totalmente documentados no tópico do guia de desenvolvimento do elemento <provider>.

Atributos informacionais
Um ícone e um rótulo opcionais para o provedor:
  • android:icon: é um recurso drawable contendo um ícone para o provedor. O ícone aparece próximo ao rótulo do provedor na lista de aplicativos em Settings > Apps > All.
  • android:label: é um rótulo informacional que descreve o provedor, seus dados ou ambos. O rótulo aparece na lista de aplicativos em Settings > Apps > All.

Os atributos estão totalmente documentados no tópico do guia de desenvolvimento do elemento <provider>.

Intents e acesso a dados

Os aplicativos podem acessar um provedor de conteúdo indiretamente com um Intent. O aplicativo não chama nenhum método de ContentResolver nem de ContentProvider. Em vez disso, ele envia um intent que inicia uma atividade que, em geral, é parte do aplicativo do próprio provedor. A atividade de destino é responsável pela recuperação e exibição dos dados na IU. Dependendo da ação no intent, a atividade de destino também pode solicitar ao usuário que realize modificações nos dados do provedor. Um intent também pode conter dados “extras” que a atividade de destino exibe na IU. O usuário terá, então, a opção de alterar esses dados antes de usá-los para modificar os dados no provedor.

Pode ser necessário usar acesso de intents para ajudar a garantir a integridade dos dados. O provedor pode depender de ter dados inseridos, atualizados e excluídos de acordo com a lógica de negócios rigorosamente definida. Se for o caso, a permissão para que outros aplicativos modifiquem os dados diretamente pode levar à invalidação dos dados. Se você quiser que os desenvolvedores usem o acesso via intents, certifique-se de documentá-lo minuciosamente. Explique por que o acesso via intents pela IU do aplicativo é melhor do que tentar modificar os dados com códigos.

O processamento dos intents recebidos com a intenção de modificar os dados do provedor não é diferente de processar outros intents. Para saber mais sobre o uso de intents, leia o tópico Intents e filtros de intents.

Para códigos de amostra relacionados a essa página, consulte o Aplicativo de exemplo do Bloco de Notas.

Para outras informações relacionadas, consulte o Provedor de agenda.