lightbulb_outline Help shape the future of the Google Play Console, Android Studio, and Firebase. Start survey

Estrutura de acesso ao armazenamento

O Android 4.4 (API de nível 19) introduz a Estrutura de Acesso ao Armazenamento (SAF). A SAF simplifica para os usuários a busca e a abertura de documentos, imagens e outros arquivos dentre todos os provedores de armazenamento de documentos de preferência. A interface gráfica padrão é fácil de usar e permite aos usuários buscar arquivos e acessar arquivos recentes de modo coerente em todos os aplicativos e provedores.

Serviços de armazenamento local ou em nuvem podem participar desse ecossistema por meio da implementação de um DocumentsProvider que encapsula os serviços. Aplicativos clientes que precisam acessar documentos de um provedor podem fazer a integração com a SAF com apenas algumas linhas de código.

A SAF contém:

  • Provedor de documentos — Provedor de conteúdo que oferece um serviço de armazenamento (como o Google Drive) para exibir os arquivos que gerencia. O provedor de documentos é implementado como uma subclasse da classe DocumentsProvider. O esquema do provedor de documento se baseia em uma hierarquia de arquivo tradicional, embora o modo de armazenamento físico de dados do provedor de documentos seja definido pelo programador. A plataforma do Android contém diversos provedores de documento embutidos, como Downloads, Imagens e Vídeos.
  • Aplicativo cliente — Aplicativo personalizado que chama o intent ACTION_OPEN_DOCUMENT e/ou ACTION_CREATE_DOCUMENT e recebe os arquivos retornados pelos provedores de documentos.
  • Seletor — IU de sistema que permite aos usuários acessar documentos de todos os provedores de documentos que satisfazem os critérios de pesquisa do aplicativo cliente.

Estes são alguns dos recursos oferecidos pela SAF:

  • Permitir que usuários busquem conteúdo de todos os provedores de documentos, não somente de um único aplicativo.
  • Possibilitar ao aplicativo a obtenção de acesso persistente e de longo prazo a documentos de propriedade de um provedor de documentos. Por meio desse acesso, os usuários podem adicionar, editar, salvar e excluir arquivos no provedor.
  • É compatível com diversas contas de usuário e raízes transitórias como provedores de armazenamento USB, que só aparecem se o dispositivo estiver conectado.

Visão geral

A SAF consiste em um provedor de conteúdo que é uma subclasse da classe DocumentsProvider. Dentro de um provedor de documentos, os dados são estruturados como uma hierarquia de arquivo tradicional:

modelo de dados

Imagem 1. Modelo de dados do provedor de documentos. Uma Raiz aponta para um único Documento, que então inicia o percurso em toda a árvore.

Observe o seguinte:

  • Cada provedor de documentos relata uma ou mais "raízes", que são pontos de partida na exploração de uma árvore de documentos. Cada raiz tem um COLUMN_ROOT_ID exclusivo e ele aponta para um documento (um diretório) representando o conteúdo sob essa raiz. As raízes têm um projeto dinâmico para oferecer compatibilidade a casos de uso como diversas contas, dispositivos de armazenamento USB transitórios ou login/logout do usuário.
  • Sob cada raiz há um documento único. Esse documento indica 1 a N documentos, cada um dos quais, por sua vez, podem indicar 1 a N documentos.
  • Cada back-end de armazenamento apresenta arquivos e diretórios individuais referenciando-os com um COLUMN_DOCUMENT_ID exclusivo. IDs de documentos devem ser exclusivos e não podem mudar depois de emitidos, pois são usados para concessões persistentes da URI em reinicializações do dispositivo.
  • Documentos podem ser um arquivo ou um diretório que pode ser aberto (com um tipo MIME específico) contendo documentos adicionais (com o tipo MIMEMIME_TYPE_DIR).
  • Cada documento tem diferentes recursos, como descrito por COLUMN_FLAGS. Por exemplo, FLAG_SUPPORTS_WRITE, FLAG_SUPPORTS_DELETE e FLAG_SUPPORTS_THUMBNAIL. O mesmo COLUMN_DOCUMENT_ID pode ser incluído em diversos diretórios.

Controle de fluxo

Como indicado anteriormente, o modelo de dados do provedor de documentos se baseia em uma hierarquia de arquivo tradicional. Contudo, é possível armazenar os dados fisicamente como quiser, desde que eles possam ser acessados usando a API DocumentsProvider. Por exemplo, seria possível usar armazenamento em nuvem com base em tag para os dados.

A figura 2 ilustra um exemplo de um aplicativo de fotos que usa a SAF para acessar dados armazenados:

aplicativo

Imagem 2. Fluxo da estrutura de acesso ao armazenamento

Observe o seguinte:

  • Na SAF, provedores e clientes não interagem diretamente. O cliente solicita permissão para interagir com arquivos (ou seja, para ler, editar, criar ou excluir arquivos).
  • A interação começa quando um aplicativo (neste exemplo, um aplicativo de foto) dispara o intent ACTION_OPEN_DOCUMENT ou ACTION_CREATE_DOCUMENT. O intent pode conter filtros para refinar ainda mais os critérios — por exemplo, "quero todos os arquivos que podem ser abertos e que tenham o tipo MIME de tal imagem".
  • Ao disparar o intent, o seletor do sistema contata cada provedor registrado e exibe as raízes de conteúdo correspondentes ao usuário.
  • O seletor fornece aos usuários uma interface padrão para acessar documentos, embora os provedores de documentos subjacentes possam ser bem diferentes. Por exemplo, a figura 2 exibe um provedor do Google Drive, um provedor USB e um provedor de nuvem.

A figura 3 exibe um seletor em que um usuário em pesquisa de imagens selecionou uma conta do Google Drive:

seletor

Imagem 3. Seletor

Quando o usuário seleciona o Google Drive, as imagens são exibidas como ilustrado na imagem 4. Desse momento em diante, o usuário pode interagir com eles de todas as formas compatíveis com o provedor e o aplicativo cliente.

seletor

Imagem 4. Imagens

Programação de um aplicativo cliente

No Android 4.3 e em versões anteriores, para o aplicativo recuperar um arquivo de outro aplicativo, ele precisa chamar um intent como ACTION_PICK ou ACTION_GET_CONTENT. Em seguida, o usuário deve selecionar um único aplicativo do qual deseja retirar um arquivo e o aplicativo selecionado deve fornecer uma interface do usuário para a busca e a seleção dos arquivos disponíveis.

No Android 4.4 e em versões posteriores, existe a opção adicional de usar o intent ACTION_OPEN_DOCUMENT, que exibe uma IU do seletor controlada pelo sistema que permite ao usuário buscar todos os arquivos que outros aplicativos disponibilizaram. Nessa IU exclusiva, o usuário pode selecionar um arquivo de qualquer aplicativo compatível.

ACTION_OPEN_DOCUMENT não substitui ACTION_GET_CONTENT. O uso de cada um deles depende da necessidade do aplicativo:

  • Use ACTION_GET_CONTENT se deseja que o aplicativo simplesmente leia ou importe dados. Nessa abordagem, o aplicativo importa uma cópia dos dados, assim como um arquivo de imagem.
  • Use ACTION_OPEN_DOCUMENT se deseja que o aplicativo tenha acesso persistente e de longo prazo a documentos de propriedade de um provedor de documentos. Um exemplo seria um aplicativo de edição de fotos que permite aos usuários editar imagens armazenadas em um provedor de documentos.

Esta seção descreve como programar aplicativos clientes com base nos intents ACTION_OPEN_DOCUMENT e ACTION_CREATE_DOCUMENT.

O snippet a seguir usa ACTION_OPEN_DOCUMENT para pesquisar provedores de documentos que contenham arquivos de imagem:

private static final int READ_REQUEST_CODE = 42;
...
/**
 * Fires an intent to spin up the "file chooser" UI and select an image.
 */
public void performFileSearch() {

    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file
    // browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones)
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only images, using the image MIME data type.
    // If one wanted to search for ogg vorbis files, the type would be "audio/ogg".
    // To search for all documents available via installed storage providers,
    // it would be "*/*".
    intent.setType("image/*");

    startActivityForResult(intent, READ_REQUEST_CODE);
}

Observe o seguinte:

  • Quando o aplicativo dispara o intent ACTION_OPEN_DOCUMENT, ele aciona um seletor que exibe todos os provedores de documentos compatíveis.
  • A adição da categoria CATEGORY_OPENABLE ao intent filtra os resultados, que exibem somente documentos que podem ser abertos, como os arquivos de imagem.
  • A declaração intent.setType("image/*") filtra ainda mais para exibir somente documentos que tenham o tipo de dados MIME da imagem.

Processamento de resultados

Quando o usuário seleciona um documento no seletor, onActivityResult() é chamado. O URI direcionada ao documento selecionado está presente no parâmetro resultData. Extraia a URI usando getData(). Depois, será possível usá-la para recuperar o documento que o usuário deseja. Por exemplo:

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {

    // The ACTION_OPEN_DOCUMENT intent was sent with the request code
    // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the
    // response to some other intent, and the code below shouldn't run at all.

    if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
        // The document selected by the user won't be returned in the intent.
        // Instead, a URI to that document will be contained in the return intent
        // provided to this method as a parameter.
        // Pull that URI using resultData.getData().
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            Log.i(TAG, "Uri: " + uri.toString());
            showImage(uri);
        }
    }
}

Examinação de metadados de documentos

Quando tiver a URI de um documento, você terá acesso aos seus metadados. Esse snippet apanha os metadados de um documento especificado pela URI e registra-os:

public void dumpImageMetaData(Uri uri) {

    // The query, since it only applies to a single document, will only return
    // one row. There's no need to filter, sort, or select fields, since we want
    // all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
    // moveToFirst() returns false if the cursor has 0 rows.  Very handy for
    // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name".  This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null.  But since an
            // int can't be null in Java, the behavior is implementation-specific,
            // which is just a fancy term for "unpredictable".  So as
            // a rule, check if it's null before assigning to an int.  This will
            // happen often:  The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

Abertura de um documento

Assim que tiver a URI de um documento, você poderá abri-lo ou fazer o que quiser com ele.

Bitmap

Eis um exemplo de como abrir um Bitmap:

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
    ParcelFileDescriptor parcelFileDescriptor =
            getContentResolver().openFileDescriptor(uri, "r");
    FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
    Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    parcelFileDescriptor.close();
    return image;
}

Observe que não se deve realizar essa operação no encadeamento da IU. Faça isso em segundo plano usando AsyncTask. Assim que abrir o bitmap, você poderá exibi-lo em uma ImageView.

Obtenção de uma InputStream

Abaixo há um exemplo de como obter uma InputStream dessa URI. Neste snippet, as linhas do arquivo estão sendo lidas em uma string:

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

Criação de um novo documento

O aplicativo pode criar um novo documento em um provedor de documentos usando o intent ACTION_CREATE_DOCUMENT. Para criar um arquivo, deve-se fornecer um tipo MIME e um nome para o arquivo ao intent e ativá-la com um código de solicitação exclusivo. O resto você não precisa fazer:

// Here are some examples of how you might call this method.
// The first parameter is the MIME type, and the second parameter is the name
// of the file you are creating:
//
// createFile("text/plain", "foobar.txt");
// createFile("image/png", "mypicture.png");

// Unique request code.
private static final int WRITE_REQUEST_CODE = 43;
...
private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);

    // Filter to only show results that can be "opened", such as
    // a file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Create a file with the requested MIME type.
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

Ao criar um novo documento, é possível obter a URI em onActivityResult() para que seja possível continuar a gravar nele.

Exclusão de um documento

Se você tem uma URI de um documento e os Document.COLUMN_FLAGS do documento contêm SUPPORTS_DELETE, você pode excluir o documento. Por exemplo:

DocumentsContract.deleteDocument(getContentResolver(), uri);

Edição de um documento

Você pode usar a SAF para editar o documento de texto no local em que está armazenado. Esse snippet dispara o intent ACTION_OPEN_DOCUMENT e usa a categoria CATEGORY_OPENABLE para exibir somente documentos que possam ser abertos. Ela filtra ainda mais para exibir somente arquivos de texto:

private static final int EDIT_REQUEST_CODE = 44;
/**
 * Open a file for writing and append some text to it.
 */
 private void editDocument() {
    // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's
    // file browser.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);

    // Filter to only show results that can be "opened", such as a
    // file (as opposed to a list of contacts or timezones).
    intent.addCategory(Intent.CATEGORY_OPENABLE);

    // Filter to show only text files.
    intent.setType("text/plain");

    startActivityForResult(intent, EDIT_REQUEST_CODE);
}

Em seguida, de onActivityResult() (consulte Processamento de resultados), você pode chamar o código para realizar a edição. O snippet a seguir obtém um FileOutputStream do ContentResolver. Por padrão, ele usa o modo de "gravação". Recomenda-se solicitar a menor quantidade de acesso necessária, portanto, não solicite acesso de leitura e programação se só for necessária a programação:

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten by MyCloud at " +
                System.currentTimeMillis() + "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Persistência de permissões

Quando o aplicativo abre um arquivo para leitura ou gravação, o sistema fornece ao seu aplicativo uma concessão de permissão da URI para aquele arquivo. Ela vigora até a reinicialização do dispositivo do usuário. Contudo, suponhamos que seu aplicativo seja de edição de imagens e você queira que os usuários acessem as últimas 5 imagens que editaram diretamente do aplicativo. Se o dispositivo do usuário foi reiniciado, você teria que enviar o usuário de volta ao seletor do sistema para encontrar os arquivos, o que, obviamente, não é o ideal.

Para evitar que isso aconteça, você pode manter as permissões que o sistema dá ao aplicativo. Efetivamente, o aplicativo "toma" a concessão de permissão da URI persistente que o sistema está oferecendo. Isso concede ao usuário um acesso contínuo aos arquivos por meio do aplicativo, mesmo se o dispositivo for reiniciado:

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

Há um último passo. Você pode ter salvo as URIs mais recentes acessadas pelo seu aplicativo, mas elas não serão mais válidas — outro aplicativo pode ter excluído ou modificado um documento. Portanto, deve-se sempre chamar getContentResolver().takePersistableUriPermission() para verificar se há dados mais recentes.

Criação de um provedor de documentos personalizado

Se você está desenvolvimento um aplicativo que fornece serviços de armazenamento para arquivos (como um serviço de armazenamento em nuvem), é possível disponibilizar os arquivos pela SAF criando um provedor de documentos personalizado. Esta seção mostra como fazê-lo.

Manifesto

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

  • Um alvo de API de nível 19 ou posterior.
  • Um elemento <provider> que declare o provedor de armazenamento personalizado.
  • O nome do provedor, que é o nome da classe, inclusive o nome do pacote. Por exemplo, com.example.android.storageprovider.MyCloudProvider.
  • O nome da autoridade, que é o nome do pacote (nesse exemplo, com.example.android.storageprovider) e o tipo de provedor de conteúdo (documents). Por exemplo, com.example.android.storageprovider.documents.
  • O atributo android:exported definido como "true". É necessário exportar o provedor para que outros aplicativos possam vê-lo.
  • O atributo android:grantUriPermissions definido como "true". Essa configuração permite ao sistema conceder acesso ao conteúdo no provedor a outros aplicativos. Para ver como manter uma concessão para determinado documento, consulte Manutenção de permissões.
  • A permissão MANAGE_DOCUMENTS. Por padrão, um provedor está disponível para todos. A adição dessa permissão restringirá o provedor em relação ao sistema. Essa restrição é importante em termos de segurança.
  • O atributo android:enabled definido como um valor booleano estabelecido em um arquivo de recursos. Esse atributo visa desativar o provedor em dispositivos com Android 4.3 ou versões anteriores. Por exemplo: android:enabled="@bool/atLeastKitKat". Além disso, para incluir esse atributo no manifesto, deve-se fazer o seguinte:
    • No arquivo de recursos bool.xml em res/values/, adicione esta linha:
      <bool name="atLeastKitKat">false</bool>
    • No arquivo de recursos bool.xml em res/values-v19/, adicione esta linha:
      <bool name="atLeastKitKat">true</bool>
  • Um filtro de intents que contenha a ação android.content.action.DOCUMENTS_PROVIDER para que o provedor apareça no seletor quando o sistema procurar provedores.

Eis alguns excertos de amostra de um manifesto que contém 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"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

Compatibilidade com dispositivos que executam Android 4.3 ou anterior

O intent ACTION_OPEN_DOCUMENT está disponível somente em dispositivos que executam o Android 4.4 ou posteriores. Se você deseja que o aplicativo seja compatível com ACTION_GET_CONTENT para se adaptar a dispositivos que executam o Android 4.3 ou versões anteriores, é necessário desativar o filtro de intent ACTION_GET_CONTENT no manifesto para dispositivos que executam Android 4.4 ou versões posteriores. Um provedor de documentos e ACTION_GET_CONTENT devem ser considerados de forma mutuamente exclusiva. Se houver compatibilidade com ambos simultaneamente, o aplicativo aparecerá duas vezes na IU do seletor do sistema, oferecendo dois meios de acesso diferentes aos dados armazenados. Isso pode confundir os usuários.

Eis a forma recomendada de desativar o filtro de intents ACTION_GET_CONTENT para dispositivos que executam o Android 4.4 ou versões posteriores:

  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 alias de atividade para desativar o filtro de intents ACTION_GET_CONTENT para versões 4.4 (API de nível 19) e posteriores. 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, como descrito no guia dos desenvolvedores de Provedores de conteúdo. Classe de contrato é uma classe public final que contém definições de constante para os URIs, nomes de coluna, tipos MIME e outros metadados que pertencem ao provedor. A SAF fornece essas classes de contrato, portanto, não é necessário programá-las:

Por exemplo, eis as colunas que podem retornar em um cursor ao consultar documentos ou a raiz do provedor de documentos:

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,};

Subclasse DocumentsProvider

A próxima etapa na criação de um provedor de documentos personalizado é atribuir uma subclasse à classe DocumentsProvider abstrata. No mínimo, é necessário implementar os seguintes métodos:

Esses são os únicos métodos que obrigatoriamente devem ser implementados, mas há muitos outros que podem ser usados. Consulte DocumentsProvider para ver mais detalhes.

Implementação de queryRoots

A implementação de queryRoots() deve retornar um Cursor que aponta para todos os diretórios raiz dos provedores 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 maioria dos provedores tem somente uma raiz. É possível ter mais de uma, por exemplo, no caso de diversas contas de usuário. Nesse caso, basta adicionar uma segunda linha ao cursor.

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

    // Create 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.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    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 once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

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

    return result;
}

Implementação de queryChildDocuments

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

Esse método é chamado quando uma raiz do aplicativo é escolhida na IU do seletor. Ele coleta os documentos filhos de um diretório abaixo da raiz. Ele pode ser chamado em qualquer nível na hierarquia de arquivos, não somente na raiz. Esse snippet cria um novo cursor com as colunas solicitadas e, em seguida, adiciona informações ao cursor sobre cada filho imediato no diretório pai. O filho pode ser uma imagem, outro diretório — qualquer arquivo:

@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;
}

Implementação de queryDocument

A implementação de queryDocument() deve retornar um Cursor que aponta para o arquivo especificado com colunas definidas em DocumentsContract.Document.

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

@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;
}

Implementação de openDocument

Deve-se implementar openDocument() para retornar um ParcelFileDescriptor que represente o arquivo especificado. Outros aplicativos podem usar o ParcelFileDescriptor retornado para transmitir dados. O sistema chama esse método quando o usuário seleciona um arquivo e o aplicativo cliente solicita acesso a ele chamando openFileDescriptor(). Por exemplo:

@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 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);
    }
}

Segurança

Suponha que o provedor de documentos seja um serviço de armazenamento em nuvem protegido por senha e que você queira se certificar de que os usuários estejam conectados antes de iniciar o compartilhamento dos arquivos. O que o aplicativo deve fazer se o usuário não estiver conectado? A solução é retornar zero raiz na implementação de queryRoots(), ou seja, um cursor de raiz vazio:

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(). Lembra do DocumentsContract? Nós o usamos para fazer essa URI. O snippet a seguir pede ao sistema que consulte 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 de queryRoots() retornará um cursor vazio, como mostrado acima. Isso garante que os documentos do provedor estejam disponíveis somente se o usuário tiver acesso ao provedor.

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