Criar um provedor de documentos personalizado

Se você estiver desenvolvendo um app que oferece serviços de armazenamento de arquivos (por exemplo, um serviço de salvamento na nuvem), é possível disponibilizar seus arquivos com o framework de acesso ao armazenamento (SAF, na sigla em inglês) gravando um provedor de documentos personalizado. Esta página descreve como criar um provedor de documentos personalizado.

Para saber mais sobre como funciona o Framework de acesso ao armazenamento, consulte a Visão geral do Framework de acesso ao armazenamento.

Manifest

Para implementar um provedor de documentos personalizado, adicione o seguinte ao manifesto do aplicativo:

  • Uma segmentação de API nível 19 ou posterior.
  • Um elemento <provider> que declara seu provedor de armazenamento personalizado.
  • O atributo android:name definido como o nome da subclasse DocumentsProvider, que é o nome da classe, incluindo o nome do pacote:

    com.example.android.storageprovider.MyCloudProvider.

  • O atributo android:authority, que é o nome do pacote (neste exemplo, com.example.android.storageprovider) mais o tipo de provedor de conteúdo (documents).
  • O atributo android:exported definido como "true". É necessário exportar seu provedor para que outros apps possam vê-lo.
  • O atributo android:grantUriPermissions definido como "true". Essa configuração permite que o sistema conceda a outros apps acesso ao conteúdo do seu provedor. Para conferir uma discussão sobre como esses outros apps podem manter o acesso ao conteúdo do provedor, consulte Persistir permissões.
  • a permissão MANAGE_DOCUMENTS; Por padrão, um provedor está disponível para todos. O acréscimo dessa permissão restringe seu provedor ao sistema. Essa restrição é importante para a segurança.
  • Um filtro de intent que inclua a ação android.content.action.DOCUMENTS_PROVIDER para que seu provedor apareça no seletor quando o sistema procurar por provedores.

Veja alguns trechos de uma amostra de manifesto que inclui um provedor:

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Oferecer compatibilidade com dispositivos com o Android 4.3 e versões anteriores

A intent ACTION_OPEN_DOCUMENT só está disponível em dispositivos com o Android 4.4 e versões mais recentes. Se você quer que seu aplicativo ofereça suporte a ACTION_GET_CONTENT para acomodar dispositivos que executam o Android 4.3 e versões anteriores, desative o filtro de intent ACTION_GET_CONTENT no seu manifesto para dispositivos com o Android 4.4 ou mais recente. Um provedor de documentos e ACTION_GET_CONTENT precisam ser considerados mutuamente exclusivos. Se o app tiver suporte para os dois simultaneamente, ele vai aparecer duas vezes na interface do seletor do sistema, oferecendo duas maneiras diferentes de acessar os dados armazenados. Isso é confuso para os usuários.

Esta é a maneira recomendada de desativar o filtro de intent ACTION_GET_CONTENT para dispositivos com o Android 4.4 ou versão mais recente:

  1. No seu arquivo de recursos bool.xml em res/values/, adicione esta linha:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. No seu arquivo de recursos bool.xml em res/values-v19/, adicione esta linha:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Adicione um alias de atividade para desativar o filtro de intent ACTION_GET_CONTENT para as versões 4.4 (nível 19 da API) e mais recentes. Por exemplo:
    <!-- This activity alias is added so that GET_CONTENT intent-filter
         can be disabled for builds on API level 19 and higher. -->
    <activity-alias android:name="com.android.example.app.MyPicker"
            android:targetActivity="com.android.example.app.MyActivity"
            ...
            android:enabled="@bool/atMostJellyBeanMR2">
        <intent-filter>
            <action android:name="android.intent.action.GET_CONTENT" />
            <category android:name="android.intent.category.OPENABLE" />
            <category android:name="android.intent.category.DEFAULT" />
            <data android:mimeType="image/*" />
            <data android:mimeType="video/*" />
        </intent-filter>
    </activity-alias>
    

Contratos

Normalmente, ao criar um provedor de conteúdo personalizado, uma das tarefas é implementar classes de contrato, conforme descrito no guia para desenvolvedores sobre Provedores de conteúdo. Uma classe de contrato é uma classe public final que contém definições de constantes para os URIs, nomes de coluna, tipos MIME e outros metadados pertencentes ao provedor. A SAF fornece essas classes de contrato, para que você não precise escrever as próprias classes:

Por exemplo, veja as colunas que você pode retornar em um cursor quando o provedor de documentos for consultado em busca de documentos ou da raiz:

Kotlin

private val DEFAULT_ROOT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Root.COLUMN_ROOT_ID,
        DocumentsContract.Root.COLUMN_MIME_TYPES,
        DocumentsContract.Root.COLUMN_FLAGS,
        DocumentsContract.Root.COLUMN_ICON,
        DocumentsContract.Root.COLUMN_TITLE,
        DocumentsContract.Root.COLUMN_SUMMARY,
        DocumentsContract.Root.COLUMN_DOCUMENT_ID,
        DocumentsContract.Root.COLUMN_AVAILABLE_BYTES
)
private val DEFAULT_DOCUMENT_PROJECTION: Array<String> = arrayOf(
        DocumentsContract.Document.COLUMN_DOCUMENT_ID,
        DocumentsContract.Document.COLUMN_MIME_TYPE,
        DocumentsContract.Document.COLUMN_DISPLAY_NAME,
        DocumentsContract.Document.COLUMN_LAST_MODIFIED,
        DocumentsContract.Document.COLUMN_FLAGS,
        DocumentsContract.Document.COLUMN_SIZE
)

Java

private static final String[] DEFAULT_ROOT_PROJECTION =
        new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES,
        Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE,
        Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID,
        Root.COLUMN_AVAILABLE_BYTES,};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new
        String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
        Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
        Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};

Seu cursor para a raiz precisa incluir algumas colunas obrigatórias. As colunas são:

O cursor para documentos precisa incluir as seguintes colunas obrigatórias:

Criar uma subclasse de DocumentsProvider

A próxima etapa na gravação de um provedor de documentos personalizado é criar uma subclasse para a classe abstrata DocumentsProvider. No mínimo, é necessário implementar os métodos abaixo:

Esses são os únicos métodos que você precisa implementar, mas há muitos outros que podem ser usados. Consulte DocumentsProvider para ver mais detalhes.

Definir uma raiz

A implementação de queryRoots() precisa retornar um Cursor que aponta para todos os diretórios raiz do provedor de documentos, usando colunas definidas em DocumentsContract.Root.

No snippet a seguir, o parâmetro projection representa os campos específicos que o autor da chamada quer recuperar. O snippet cria um novo cursor e adiciona uma linha a ele: uma raiz, um diretório de nível superior, como Downloads ou Imagens. A maior parte dos provedores só tem uma raiz. É possível ter mais de uma, por exemplo, no caso de várias contas de usuário. Nesse caso, basta adicionar uma segunda linha ao cursor.

Kotlin

override fun queryRoots(projection: Array<out String>?): Cursor {
    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    val result = MatrixCursor(resolveRootProjection(projection))

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    result.newRow().apply {
        add(DocumentsContract.Root.COLUMN_ROOT_ID, ROOT)

        // You can provide an optional summary, which helps distinguish roots
        // with the same title. You can also use this field for displaying an
        // user account name.
        add(DocumentsContract.Root.COLUMN_SUMMARY, context.getString(R.string.root_summary))

        // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
        // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
        // recently used documents will show up in the "Recents" category.
        // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
        // shares.
        add(
            DocumentsContract.Root.COLUMN_FLAGS,
            DocumentsContract.Root.FLAG_SUPPORTS_CREATE or
                DocumentsContract.Root.FLAG_SUPPORTS_RECENTS or
                DocumentsContract.Root.FLAG_SUPPORTS_SEARCH
        )

        // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
        add(DocumentsContract.Root.COLUMN_TITLE, context.getString(R.string.title))

        // This document id cannot change after it's shared.
        add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir))

        // The child MIME types are used to filter the roots and only present to the
        // user those roots that contain the desired type somewhere in their file hierarchy.
        add(DocumentsContract.Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir))
        add(DocumentsContract.Root.COLUMN_AVAILABLE_BYTES, baseDir.freeSpace)
        add(DocumentsContract.Root.COLUMN_ICON, R.drawable.ic_launcher)
    }

    return result
}

Java

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Use a MatrixCursor to build a cursor
    // with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);

    // You can provide an optional summary, which helps distinguish roots
    // with the same title. You can also use this field for displaying an
    // user account name.
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change after it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(baseDir));

    // The child MIME types are used to filter the roots and only present to the
    // user those roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(baseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, baseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

Se o provedor de documentos se conectar a um conjunto dinâmico de raízes (por exemplo, a um dispositivo USB que possa ser desconectado ou a uma conta de onde o usuário possa sair), será possível atualizar a IU do documento para manter a sincronia com essas alterações usando o método ContentResolver.notifyChange(), conforme mostrado no snippet de código a seguir.

Kotlin

val rootsUri: Uri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY)
context.contentResolver.notifyChange(rootsUri, null)

Java

Uri rootsUri = DocumentsContract.buildRootsUri(BuildConfig.DOCUMENTS_AUTHORITY);
context.getContentResolver().notifyChange(rootsUri, null);

Listar documentos no provedor

Sua implementação de queryChildDocuments() precisa retornar um Cursor que aponte para todos os arquivos no diretório especificado, usando colunas definidas em DocumentsContract.Document.

Esse método é chamado quando o usuário escolhe sua raiz na IU do seletor. O método recupera os filhos do ID do documento especificado por COLUMN_DOCUMENT_ID. Em seguida, o sistema chamará esse método sempre que o usuário selecionar um subdiretório no seu provedor de documentos.

Esse snippet cria um novo cursor com as colunas solicitadas e, em seguida, adiciona ao cursor informações sobre cada filho imediato no diretório pai. Um filho pode ser uma imagem, outro diretório, qualquer arquivo:

Kotlin

override fun queryChildDocuments(
        parentDocumentId: String?,
        projection: Array<out String>?,
        sortOrder: String?
): Cursor {
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        val parent: File = getFileForDocId(parentDocumentId)
        parent.listFiles()
                .forEach { file ->
                    includeFile(this, null, file)
                }
    }
}

Java

@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

Receber informações de documentos

Sua implementação de queryDocument() precisa retornar um Cursor que aponte para o arquivo especificado, usando colunas definidas em DocumentsContract.Document.

O método queryDocument() retorna as mesmas informações que foram transmitidas em queryChildDocuments(), mas para um arquivo específico:

Kotlin

override fun queryDocument(documentId: String?, projection: Array<out String>?): Cursor {
    // Create a cursor with the requested projection, or the default projection.
    return MatrixCursor(resolveDocumentProjection(projection)).apply {
        includeFile(this, documentId, null)
    }
}

Java

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

Seu provedor de documentos também pode fornecer miniaturas de um documento modificando o método DocumentsProvider.openDocumentThumbnail() e adicionando a sinalização FLAG_SUPPORTS_THUMBNAIL aos arquivos compatíveis. O snippet de código abaixo oferece um exemplo de como implementar o DocumentsProvider.openDocumentThumbnail().

Kotlin

override fun openDocumentThumbnail(
        documentId: String?,
        sizeHint: Point?,
        signal: CancellationSignal?
): AssetFileDescriptor {
    val file = getThumbnailFileForDocId(documentId)
    val pfd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
    return AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
}

Java

@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, Point sizeHint,
                                                     CancellationSignal signal)
        throws FileNotFoundException {

    final File file = getThumbnailFileForDocId(documentId);
    final ParcelFileDescriptor pfd =
        ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
    return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
}

Cuidado: um provedor de documentos não deve retornar imagens em miniatura com mais do que o dobro do tamanho especificado pelo parâmetro sizeHint.

Abrir um documento

Implemente openDocument() para retornar um ParcelFileDescriptor que represente o arquivo especificado. Outros apps podem usar o ParcelFileDescriptor retornado para fazer streaming de dados. O sistema chama esse método depois que o usuário seleciona um arquivo e o app cliente solicita acesso chamando openFileDescriptor(). Por exemplo:

Kotlin

override fun openDocument(
        documentId: String,
        mode: String,
        signal: CancellationSignal
): ParcelFileDescriptor {
    Log.v(TAG, "openDocument, mode: $mode")
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    val file: File = getFileForDocId(documentId)
    val accessMode: Int = ParcelFileDescriptor.parseMode(mode)

    val isWrite: Boolean = mode.contains("w")
    return if (isWrite) {
        val handler = Handler(context.mainLooper)
        // Attach a close listener if the document is opened in write mode.
        try {
            ParcelFileDescriptor.open(file, accessMode, handler) {
                // Update the file with the cloud server. The client is done writing.
                Log.i(TAG, "A file with id $documentId has been closed! Time to update the server.")
            }
        } catch (e: IOException) {
            throw FileNotFoundException(
                    "Failed to open document with id $documentId and mode $mode"
            )
        }
    } else {
        ParcelFileDescriptor.open(file, accessMode)
    }
}

Java

@Override
public ParcelFileDescriptor openDocument(final String documentId,
                                         final String mode,
                                         CancellationSignal signal) throws
        FileNotFoundException {
    Log.v(TAG, "openDocument, mode: " + mode);
    // It's OK to do network operations in this method to download the document,
    // as long as you periodically check the CancellationSignal. If you have an
    // extremely large file to transfer from the network, a better solution may
    // be pipes or sockets (see ParcelFileDescriptor for helper methods).

    final File file = getFileForDocId(documentId);
    final int accessMode = ParcelFileDescriptor.parseMode(mode);

    final boolean isWrite = (mode.indexOf('w') != -1);
    if(isWrite) {
        // Attach a close listener if the document is opened in write mode.
        try {
            Handler handler = new Handler(getContext().getMainLooper());
            return ParcelFileDescriptor.open(file, accessMode, handler,
                        new ParcelFileDescriptor.OnCloseListener() {
                @Override
                public void onClose(IOException e) {

                    // Update the file with the cloud server. The client is done
                    // writing.
                    Log.i(TAG, "A file with id " +
                    documentId + " has been closed! Time to " +
                    "update the server.");
                }

            });
        } catch (IOException e) {
            throw new FileNotFoundException("Failed to open document with id"
            + documentId + " and mode " + mode);
        }
    } else {
        return ParcelFileDescriptor.open(file, accessMode);
    }
}

Se o provedor de documentos transmitir arquivos ou processar estruturas de dados complicadas, implemente os métodos createReliablePipe() ou createReliableSocketPair(). Esses métodos permitem criar um par de objetos ParcelFileDescriptor, em que você pode retornar um e enviar o outro por meio de um ParcelFileDescriptor.AutoCloseOutputStream ou ParcelFileDescriptor.AutoCloseInputStream.

Oferecer compatibilidade com documentos recentes e pesquisa

Você pode fornecer uma lista de documentos modificados recentemente na raiz do seu provedor de documentos substituindo o método queryRecentDocuments() e retornando FLAG_SUPPORTS_RECENTS. O snippet de código a seguir mostra um exemplo de como implementar os métodos queryRecentDocuments().

Kotlin

override fun queryRecentDocuments(rootId: String?, projection: Array<out String>?): Cursor {
    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    val result = MatrixCursor(resolveDocumentProjection(projection))

    val parent: File = getFileForDocId(rootId)

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    val lastModifiedFiles = PriorityQueue(
            5,
            Comparator<File> { i, j ->
                Long.compare(i.lastModified(), j.lastModified())
            }
    )

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    val pending : MutableList<File> = mutableListOf()

    // Start by adding the parent to the list of files to be processed
    pending.add(parent)

    // Do while we still have unexamined files
    while (pending.isNotEmpty()) {
        // Take a file from the list of unprocessed files
        val file: File = pending.removeAt(0)
        if (file.isDirectory) {
            // If it's a directory, add all its children to the unprocessed list
            pending += file.listFiles()
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file)
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (i in 0 until Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size)) {
        val file: File = lastModifiedFiles.remove()
        includeFile(result, null, file)
    }
    return result
}

Java

@Override
public Cursor queryRecentDocuments(String rootId, String[] projection)
        throws FileNotFoundException {

    // This example implementation walks a
    // local file structure to find the most recently
    // modified files.  Other implementations might
    // include making a network call to query a
    // server.

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result =
        new MatrixCursor(resolveDocumentProjection(projection));

    final File parent = getFileForDocId(rootId);

    // Create a queue to store the most recent documents,
    // which orders by last modified.
    PriorityQueue lastModifiedFiles =
        new PriorityQueue(5, new Comparator() {

        public int compare(File i, File j) {
            return Long.compare(i.lastModified(), j.lastModified());
        }
    });

    // Iterate through all files and directories
    // in the file structure under the root.  If
    // the file is more recent than the least
    // recently modified, add it to the queue,
    // limiting the number of results.
    final LinkedList pending = new LinkedList();

    // Start by adding the parent to the list of files to be processed
    pending.add(parent);

    // Do while we still have unexamined files
    while (!pending.isEmpty()) {
        // Take a file from the list of unprocessed files
        final File file = pending.removeFirst();
        if (file.isDirectory()) {
            // If it's a directory, add all its children to the unprocessed list
            Collections.addAll(pending, file.listFiles());
        } else {
            // If it's a file, add it to the ordered queue.
            lastModifiedFiles.add(file);
        }
    }

    // Add the most recent files to the cursor,
    // not exceeding the max number of results.
    for (int i = 0; i < Math.min(MAX_LAST_MODIFIED + 1, lastModifiedFiles.size()); i++) {
        final File file = lastModifiedFiles.remove();
        includeFile(result, null, file);
    }
    return result;
}

Você pode ver o código completo do snippet acima fazendo o download do exemplo de código StorageProvider (link em inglês).

Oferecer compatibilidade com a criação de documentos

Você pode permitir que apps clientes criem arquivos no seu provedor de documentos. Se um app cliente enviar um intent ACTION_CREATE_DOCUMENT, seu provedor de documentos poderá permitir que esse app cliente crie novos documentos no provedor.

Para oferecer suporte à criação de documentos, sua raiz precisa ter a sinalização FLAG_SUPPORTS_CREATE. Os diretórios que permitem a criação de novos arquivos precisam ter a flag FLAG_DIR_SUPPORTS_CREATE.

Seu provedor de documentos também precisa implementar o método createDocument(). Quando um usuário seleciona um diretório no seu provedor de documentos para salvar um novo arquivo, o provedor recebe uma chamada para createDocument(). Dentro da implementação do método createDocument(), um novo COLUMN_DOCUMENT_ID é retornado para o arquivo. O app cliente pode usar esse ID para conseguir um identificador para o arquivo e, por fim, chamar openDocument() para gravar no novo arquivo.

O snippet de código a seguir demonstra como criar um novo arquivo em um provedor de documentos.

Kotlin

override fun createDocument(documentId: String?, mimeType: String?, displayName: String?): String {
    val parent: File = getFileForDocId(documentId)
    val file: File = try {
        File(parent.path, displayName).apply {
            createNewFile()
            setWritable(true)
            setReadable(true)
        }
    } catch (e: IOException) {
        throw FileNotFoundException(
                "Failed to create document with name $displayName and documentId $documentId"
        )
    }

    return getDocIdForFile(file)
}

Java

@Override
public String createDocument(String documentId, String mimeType, String displayName)
        throws FileNotFoundException {

    File parent = getFileForDocId(documentId);
    File file = new File(parent.getPath(), displayName);
    try {
        file.createNewFile();
        file.setWritable(true);
        file.setReadable(true);
    } catch (IOException e) {
        throw new FileNotFoundException("Failed to create document with name " +
                displayName +" and documentId " + documentId);
    }
    return getDocIdForFile(file);
}

Você pode ver o código completo do snippet acima fazendo o download do exemplo de código StorageProvider (link em inglês).

Oferecer compatibilidade com recursos de gerenciamento de documentos

Além de abrir, criar e visualizar arquivos, o provedor de documentos também pode permitir que apps clientes renomeiem, copiem, movam e excluam arquivos. Para adicionar a funcionalidade de gerenciamento de documentos ao seu provedor, adicione uma sinalização à coluna COLUMN_FLAGS do documento para indicar a funcionalidade compatível. Também é necessário implementar o método correspondente da classe DocumentsProvider.

A tabela a seguir fornece a flag COLUMN_FLAGS e o método DocumentsProvider que um provedor de documentos precisa implementar para expor recursos específicos.

Recurso Sinalização Método
Excluir um arquivo FLAG_SUPPORTS_DELETE deleteDocument()
Renomear um arquivo FLAG_SUPPORTS_RENAME renameDocument()
Copiar um arquivo para um novo diretório pai no provedor de documentos FLAG_SUPPORTS_COPY copyDocument()
Mover um arquivo de um diretório para outro no provedor de documentos FLAG_SUPPORTS_MOVE moveDocument()
Remover um arquivo do diretório pai FLAG_SUPPORTS_REMOVE removeDocument()

Oferecer compatibilidade com arquivos virtuais e formatos de arquivos alternativos

Os arquivos virtuais, um recurso introduzido no Android 7.0 (API de nível 24), permitem que os provedores de documentos ofereçam acesso de visualização a arquivos que não têm uma representação direta do bytecode. Para permitir que outros apps vejam arquivos virtuais, seu provedor de documentos precisa produzir uma representação alternativa de arquivo que possa ser aberta para os arquivos virtuais.

Por exemplo, imagine que um provedor de documentos contém um formato de arquivo que outros apps não podem abrir diretamente, basicamente um arquivo virtual. Quando um app cliente envia um intent ACTION_VIEW sem a categoria CATEGORY_OPENABLE, os usuários podem selecionar esses arquivos virtuais no provedor de documentos para visualização. Em seguida, o provedor de documentos retorna o arquivo virtual em um formato de arquivo diferente, mas que pode ser aberto, como uma imagem. Assim, o app cliente pode abrir o arquivo virtual para que o usuário o veja.

Para declarar que um documento no provedor é virtual, adicione a sinalização FLAG_VIRTUAL_DOCUMENT ao arquivo retornado pelo método queryDocument(). Essa sinalização alerta os apps clientes de que o arquivo não tem uma representação direta de bytecode e não pode ser aberto diretamente.

Se você declarar que um arquivo no seu provedor de documentos é virtual, é altamente recomendável disponibilizá-lo em outro tipo MIME, como uma imagem ou um PDF. O provedor de documentos declara os tipos MIME alternativos com suporte para a visualização de um arquivo virtual modificando o método getDocumentStreamTypes(). Quando os apps clientes chamam o método getStreamTypes(android.net.Uri, java.lang.String), o sistema chama o método getDocumentStreamTypes() do provedor de documentos. Depois, o método getDocumentStreamTypes() retorna uma matriz de tipos MIME alternativos que são compatíveis com o provedor de documentos para o arquivo.

Depois que o cliente determina que o provedor de documentos pode produzir o documento em um formato de arquivo visível, o app cliente chama o método openTypedAssetFileDescriptor(), que chama internamente o método openTypedDocument() do provedor de documentos. O provedor de documentos retorna o arquivo ao app cliente no formato de arquivo solicitado.

O snippet de código a seguir demonstra uma implementação simples dos métodos getDocumentStreamTypes() e openTypedDocument().

Kotlin

var SUPPORTED_MIME_TYPES : Array<String> = arrayOf("image/png", "image/jpg")
override fun openTypedDocument(
        documentId: String?,
        mimeTypeFilter: String,
        opts: Bundle?,
        signal: CancellationSignal?
): AssetFileDescriptor? {
    return try {
        // Determine which supported MIME type the client app requested.
        when(mimeTypeFilter) {
            "image/jpg" -> openJpgDocument(documentId)
            "image/png", "image/*", "*/*" -> openPngDocument(documentId)
            else -> throw IllegalArgumentException("Invalid mimeTypeFilter $mimeTypeFilter")
        }
    } catch (ex: Exception) {
        Log.e(TAG, ex.message)
        null
    }
}

override fun getDocumentStreamTypes(documentId: String, mimeTypeFilter: String): Array<String> {
    return when (mimeTypeFilter) {
        "*/*", "image/*" -> {
            // Return all supported MIME types if the client app
            // passes in '*/*' or 'image/*'.
            SUPPORTED_MIME_TYPES
        }
        else -> {
            // Filter the list of supported mime types to find a match.
            SUPPORTED_MIME_TYPES.filter { it == mimeTypeFilter }.toTypedArray()
        }
    }
}

Java


public static String[] SUPPORTED_MIME_TYPES = {"image/png", "image/jpg"};

@Override
public AssetFileDescriptor openTypedDocument(String documentId,
    String mimeTypeFilter,
    Bundle opts,
    CancellationSignal signal) {

    try {

        // Determine which supported MIME type the client app requested.
        if ("image/png".equals(mimeTypeFilter) ||
            "image/*".equals(mimeTypeFilter) ||
            "*/*".equals(mimeTypeFilter)) {

            // Return the file in the specified format.
            return openPngDocument(documentId);

        } else if ("image/jpg".equals(mimeTypeFilter)) {
            return openJpgDocument(documentId);
        } else {
            throw new IllegalArgumentException("Invalid mimeTypeFilter " + mimeTypeFilter);
        }

    } catch (Exception ex) {
        Log.e(TAG, ex.getMessage());
    } finally {
        return null;
    }
}

@Override
public String[] getDocumentStreamTypes(String documentId, String mimeTypeFilter) {

    // Return all supported MIME tyupes if the client app
    // passes in '*/*' or 'image/*'.
    if ("*/*".equals(mimeTypeFilter) ||
        "image/*".equals(mimeTypeFilter)) {
        return SUPPORTED_MIME_TYPES;
    }

    ArrayList requestedMimeTypes = new ArrayList&lt;&gt;();

    // Iterate over the list of supported mime types to find a match.
    for (int i=0; i &lt; SUPPORTED_MIME_TYPES.length; i++) {
        if (SUPPORTED_MIME_TYPES[i].equals(mimeTypeFilter)) {
            requestedMimeTypes.add(SUPPORTED_MIME_TYPES[i]);
        }
    }
    return (String[])requestedMimeTypes.toArray();
}

Segurança

Suponha que seu provedor de documentos seja um serviço de armazenamento em nuvem protegido por senha e você queira garantir que os usuários estejam conectados antes de começar a compartilhar os arquivos. O que seu app deverá fazer se o usuário não estiver conectado? A solução é retornar zero raízes na implementação de queryRoots(). Ou seja, um cursor de raiz vazio:

Kotlin

override fun queryRoots(projection: Array<out String>): Cursor {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result
    }

Java

public Cursor queryRoots(String[] projection) throws FileNotFoundException {
...
    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
}

A outra etapa é chamar getContentResolver().notifyChange(). Você se lembra do DocumentsContract? Nós o usamos para criar esse URI. O snippet a seguir instrui o sistema a consultar as raízes do provedor de documentos sempre que o status de login do usuário mudar. Se o usuário não estiver conectado, uma chamada para queryRoots() retornará um cursor vazio, como mostrado acima. Isso garante que os documentos de um provedor só estejam disponíveis se o usuário estiver conectado a ele.

Kotlin

private fun onLoginButtonClick() {
    loginOrLogout()
    getContentResolver().notifyChange(
        DocumentsContract.buildRootsUri(AUTHORITY),
        null
    )
}

Java

private void onLoginButtonClick() {
    loginOrLogout();
    getContentResolver().notifyChange(DocumentsContract
            .buildRootsUri(AUTHORITY), null);
}

Para ver a amostra de código relacionada a esta página, consulte:

Para ver vídeos relacionados a esta página, consulte:

Para ver mais informações relacionadas, consulte: