Criar um provedor de documentos personalizado

Se você estiver desenvolvendo um aplicativo que fornece serviços de armazenamento para arquivos (como um serviço de salvamento na nuvem), é possível disponibilizar seus arquivos pelo 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 mais informações sobre como o framework de acesso ao armazenamento funciona, consulte a Visão geral do framework de acesso ao armazenamento.

Manifesto

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

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

    com.example.android.storageprovider.MyCloudProvider.

  • O atributo android:authority, que é o nome do seu pacote (neste exemplo, com.example.android.storageprovider). além do 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 foi definido como "true". Esta configuração permite que o sistema conceda acesso a outros apps ao conteúdo no seu provedor. Para uma discussão sobre como esses outros aplicativos podem manter o acesso ao conteúdo do seu provedor, consulte Persistir do Google Cloud.
  • 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 inclui o android.content.action.DOCUMENTS_PROVIDER, para que seu provedor aparece no seletor quando o sistema procura 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 A intent ACTION_OPEN_DOCUMENT só está disponível em dispositivos com Android 4.4 ou superior. Se você quiser que seu aplicativo seja compatível com ACTION_GET_CONTENT para acomodar dispositivos que executam o Android 4.3 e versões anteriores, desativar o filtro de intent ACTION_GET_CONTENT em seu manifesto para dispositivos com Android 4.4 ou superior. Um provedor de documentos e ACTION_GET_CONTENT precisam ser considerados mutuamente exclusivos. Se você oferecer suporte aos dois ao mesmo tempo, seu app aparece duas vezes na interface do seletor do sistema, oferecendo duas maneiras diferentes de acessar seus dados armazenados. Isso é confuso para os usuários.

Esta é a forma recomendada de desativar o Filtro de intent ACTION_GET_CONTENT para dispositivos com o Android 4.4 ou superior:

  1. No arquivo de recursos bool.xml em res/values/, adicione esta linha:
    <bool name="atMostJellyBeanMR2">true</bool>
  2. No arquivo de recursos bool.xml em res/values-v19/, adicione esta linha:
    <bool name="atMostJellyBeanMR2">false</bool>
  3. Adicione um atividade alias para desativar a intent ACTION_GET_CONTENT filtro 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 nas 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 colunas, tipos MIME e outros metadados que pertençam ao provedor. A SAF fornece essas classes de contrato para que você não precise escrever seu por conta própria:

Por exemplo, aqui estão as colunas que você pode retornar em um cursor quando seu provedor de documentos é 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 criação de um provedor de documentos personalizado é criar uma subclasse para classe abstrata DocumentsProvider. No mínimo, você precisa implemente estes métodos:

Esses são os únicos métodos que você precisa implementar, mas há há muitas outras que você pode querer. Consulte DocumentsProvider para mais detalhes.

Definir uma raiz

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

No snippet a seguir, o parâmetro projection representa 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. Você pode ter mais de um, por exemplo, no caso de várias contas de usuário. Nesse caso, basta adicionar um 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;
}

Caso seu provedor de documentos se conecte a um conjunto dinâmico de raízes, por exemplo, a uma conexão USB um dispositivo desconectado ou uma conta da qual o usuário possa sair. pode atualizar a interface do documento para ficar em sincronia com essas alterações usando o 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 do queryChildDocuments() precisa retornar um Cursor que aponte para todos os arquivos em 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 pelo COLUMN_DOCUMENT_ID: Em seguida, o sistema chama esse método sempre que o usuário seleciona um em seu provedor de documentos.

Esse snippet cria um novo cursor com as colunas solicitadas e adiciona informações sobre cada filho imediato no diretório pai ao cursor. 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 do queryDocument() precisa retornar um Cursor que aponte para o arquivo especificado, usando colunas definidas em DocumentsContract.Document.

O queryDocument() retorna as mesmas informações que foram passadas 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 para um documento ao substituindo DocumentsProvider.openDocumentThumbnail() e adicionar o FLAG_SUPPORTS_THUMBNAIL aos arquivos compatíveis. O snippet de código a seguir fornece um exemplo de como implementar a 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);
}

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

Abrir um documento

Implemente openDocument() para retornar um ParcelFileDescriptor que represente no arquivo especificado. Outros apps podem usar o ParcelFileDescriptor retornado. para transmitir dados. O sistema chama esse método depois que o usuário seleciona um arquivo, e o app cliente solicita acesso chamando openFileDescriptor(): 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);
    }
}

Caso seu provedor de documentos transmita arquivos ou processe arquivos estruturas de dados, considere implementar createReliablePipe() ou createReliableSocketPair(). Esses métodos permitem criar um par de Objetos ParcelFileDescriptor, em que é possível retornar um e enviar a outra por uma ParcelFileDescriptor.AutoCloseOutputStream ou ParcelFileDescriptor.AutoCloseInputStream

Oferecer compatibilidade com documentos recentes e pesquisa

É possível fornecer uma lista de documentos modificados recentemente na raiz da sua provedor de documentos substituindo o arquivo queryRecentDocuments() e retornando FLAG_SUPPORTS_RECENTS, O snippet de código a seguir mostra um exemplo de como implementar a 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 obter o código completo para o snippet acima fazendo download do StorageProvider (link em inglês) exemplo de código.

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 ACTION_CREATE_DOCUMENT seu provedor de documentos pode permitir que esse app cliente crie novos documentos no provedor de documentos.

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

Seu provedor de documentos também precisa implementar a createDocument(). Quando um usuário seleciona um diretório em seu provedor de documentos salvar um novo arquivo, ele receberá uma chamada para createDocument(): Dentro da implementação createDocument(), você retorna uma nova COLUMN_DOCUMENT_ID para o . 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 obter o código completo para o snippet acima fazendo download do StorageProvider (link em inglês) exemplo de código.

Oferecer compatibilidade com recursos de gerenciamento de documentos

Além de abrir, criar e visualizar arquivos, seu provedor de documentos também permite que apps clientes renomeiem, copiem, movam e excluam . Para adicionar a funcionalidade de gerenciamento de documentos ao seu provedor de documentos, adicione uma sinalização COLUMN_FLAGS coluna para indicar a funcionalidade compatível. Você também precisa implementar O método correspondente do DocumentsProvider .

A tabela a seguir mostra Sinalização COLUMN_FLAGS e DocumentsProvider que um documento provedor 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

Arquivos virtuais, um recurso introduzido no Android 7.0 (API de nível 24), que permite aos provedores de documentos para fornecer acesso de visualização a arquivos representação direta de bytecode. Para permitir que outros apps visualizem arquivos virtuais, faça o seguinte: o provedor de documentos precisar produzir um arquivo alternativo que possa ser aberto representação para os arquivos virtuais.

Por exemplo, imagine que um provedor de documentos contém um arquivo que outros aplicativos não possam abrir diretamente, essencialmente um arquivo virtual. Quando um app cliente envia uma intent ACTION_VIEW sem a categoria CATEGORY_OPENABLE, os usuários poderão selecionar esses arquivos virtuais no provedor de documentos para visualização. 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 o FLAG_VIRTUAL_DOCUMENT ao arquivo retornado pelo queryDocument() . Essa sinalização alerta os aplicativos clientes de que o arquivo não tem um endereço representação 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 O tipo MIME, como uma imagem ou um PDF. O provedor de documentos declara os tipos MIME alternativos que oferece suporte para a visualização de um arquivo virtual por meio da substituição do getDocumentStreamTypes() . Quando os aplicativos clientes chamam o método getStreamTypes(android.net.Uri, java.lang.String) método, o sistema chama o método getDocumentStreamTypes() do provedor de documentos. A getDocumentStreamTypes() retorna uma matriz de tipos MIME alternativos que o que o provedor de documentos oferece para o arquivo.

Depois que o cliente determinar de que o provedor possa produzir o documento em um arquivo visualizável formato, o aplicativo cliente chama o openTypedAssetFileDescriptor() , que chama internamente o método openTypedDocument() . O provedor de documentos retorna o arquivo para o app cliente em o formato de arquivo solicitado.

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

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 quer ter certeza de que os usuários estejam conectados antes de começar a compartilhar arquivos. O que seu app deverá fazer se o usuário não estiver conectado? A solução é retornar raízes zero 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? Usamos isso para tornar esse URI. O snippet a seguir pede que o sistema consulte as raízes dos provedor de documentos sempre que o status de login do usuário mudar. Se o usuário não for tiver feito login, uma chamada para queryRoots() retornará um cursor vazio, como mostrado acima. Isso garante que os documentos de um provedor sejam disponível se o usuário estiver conectado ao provedor.

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: